Skip to content

Conversation

@saeedsaiyed01
Copy link
Contributor

@saeedsaiyed01 saeedsaiyed01 commented Jan 27, 2026

Closes #1761

🚀 New Community Demo: Global Form Auto-Localization

This PR adds a new demo application to the /community directory that showcases the power of Lingo.dev’s structured object translation in a real-world product scenario.


image image image

💡 What this app does

This demo demonstrates a realistic use case where a form is defined once in English (schema + metadata) and is automatically localized into multiple languages (Spanish, French, German, Hindi) on the fly properly translating:

  • Form titles and descriptions
  • Labels and placeholders
  • Helper text and tooltips
  • Validation error messages

Instead of translating flat strings, the app passes a structured form object to Lingo.dev and receives a fully localized version while preserving the original schema.


🛠️ Key Features

  • Visual Form Builder
    Add and configure multiple field types including Text, Textarea, Radio, Checkbox, Select, and Date.

  • Structured Localization with Lingo.dev
    Demonstrates localizeObject() on nested form schemas rather than individual strings.

  • Live Multilingual Preview
    Instantly switch languages and preview the fully localized form UI.


🎯 Issue Reference
Closes/Related: #1761

🛠️ How to run locally


cd community/lingo-global-forms
npm install
npm run dev
cp .env.example .env
# set VITE_LINGO_API_KEY=your_api_key_here

Closes #1761

Summary by CodeRabbit

  • New Features

    • Community form localization demo with form builder, live preview, and language selection (en, es, fr, de, hi).
    • Real-time translation with progress/error feedback and caching for faster subsequent translations.
  • Documentation

    • Comprehensive README with setup, usage, and environment variable guidance.
  • Chores

    • Added development tooling and project config plus a .gitignore and sample environment placeholder for the API key.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Adds a new demo project "lingo-global-forms" under community/, including a Vite + React + TypeScript app, Tailwind and ESLint configs, Lingo.dev integration, translation/cache utilities, form editor/viewer components, types, docs, and example env/config files.

Changes

Cohort / File(s) Summary
Project config & tooling
community/lingo-global-forms/.env.example, community/lingo-global-forms/.eslintrc.cjs, community/lingo-global-forms/.gitignore, community/lingo-global-forms/package.json, community/lingo-global-forms/postcss.config.cjs, community/lingo-global-forms/tailwind.config.cjs, community/lingo-global-forms/tsconfig.json, community/lingo-global-forms/tsconfig.node.json, community/lingo-global-forms/vite.config.ts
Adds standard project files: environment placeholder for VITE_LINGO_API_KEY, ESLint, comprehensive .gitignore, package metadata and scripts, PostCSS/Tailwind configs, TS configs, and Vite dev server with /api/lingo proxy.
Docs & entry
community/lingo-global-forms/README.md, community/lingo-global-forms/index.html, .changeset/many-ends-return.md
New README describing the demo, HTML entry point, and a small changeset file.
Styling & bootstrap
community/lingo-global-forms/src/index.css, community/lingo-global-forms/src/main.tsx
Adds global Tailwind-based CSS (including custom scrollbar) and React bootstrap entry.
App orchestration
community/lingo-global-forms/src/App.tsx
New App component managing form state, locale, translation lifecycle, progress/error flags, and composition of editor/preview components.
UI components
community/lingo-global-forms/src/components/FormBuilder.tsx, community/lingo-global-forms/src/components/FormPreview.tsx, community/lingo-global-forms/src/components/LanguageSelect.tsx
Adds FormBuilder (dynamic form editor), FormPreview (live localized preview, validation, submit flow), and LanguageSelect dropdown component.
Types & env typings
community/lingo-global-forms/src/types/form.ts, community/lingo-global-forms/src/vite-env.d.ts
Introduces form-related TypeScript types, supported locales, default form content, and Vite env typing for VITE_LINGO_API_KEY.
Lingo integration & translation
community/lingo-global-forms/src/lib/lingo.ts, community/lingo-global-forms/src/lib/translateForm.ts
New lingo engine initializer reading VITE_LINGO_API_KEY and translateForm module implementing flatten/unflatten, in-memory cache, progress callbacks, and exported translate/clearCache functions.

Sequence Diagram

sequenceDiagram
    participant User
    participant FormBuilder as FormBuilder (Editor)
    participant App
    participant Translate as TranslateForm (Cache & Flatten)
    participant Lingo as Lingo Engine
    participant Preview as FormPreview (Viewer)

    User->>FormBuilder: Edit form
    FormBuilder->>App: onFormChange(updatedContent)
    App->>App: set formContent

    User->>Preview: Select locale (non-en)
    Preview->>App: onLocaleChange(locale)
    App->>App: set currentLocale

    App->>Translate: translateFormContent(content, locale)
    Translate->>Translate: compute cacheKey & flatten content

    alt Cache hit
        Translate->>App: return cached translated content (progress 100%)
    else Cache miss
        Translate->>Lingo: localizeObject(flatContent, onProgress)
        Lingo-->>Translate: translated flat content
        Translate->>Translate: unflatten & cache result
        Translate->>App: return translated FormContent
    end

    App->>Preview: pass translatedContent
    Preview->>User: render localized form
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • sumitsaurabh927
  • vrcprl

Poem

🐰 Hopping through code with a joyful squeak,
Forms learn new tongues—Spanish, French, and Greek!
Lingo whispers words from near and far,
Demo springs to life like a twinkling star.
Hooray — global forms now speak who we are!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new Global Form Auto-Localization demo to the community directory.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the summary, key features, visual demo with three screenshots, and clear local run instructions.
Linked Issues check ✅ Passed All linked issue #1761 requirements are met: demo app showcases Lingo.dev structured object translation, includes comprehensive README, and demonstrates the specific feature with live multilingual preview.
Out of Scope Changes check ✅ Passed All changes are directly related to the demo app scope: configuration files, type definitions, React components, translation logic, and documentation without unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@community/lingo-global-forms/package.json`:
- Line 9: The package.json "lint" script references eslint but ESLint is not
listed in devDependencies, so add ESLint to devDependencies (e.g., install and
save as a dev dependency) and update package.json accordingly; locate the "lint"
script entry in package.json and ensure "eslint" (and any required plugins/
configs you rely on) appear under "devDependencies" so npm run lint works
without a global install.

In `@community/lingo-global-forms/src/components/FormBuilder.tsx`:
- Around line 122-127: The Checkbox button is rendering the literal token
"ballot_check" instead of an icon; in FormBuilder replace the span containing
"ballot_check" inside the checkbox add button (the button that calls
addField('checkbox')) with the proper icon element used across the project (for
example the material icon span/class or the shared Icon/Svg component) so the
visual icon is displayed rather than raw text; ensure the replacement keeps
existing classes (e.g., mr-2) for spacing and accessibility (aria-hidden or
aria-label) as appropriate.

In `@community/lingo-global-forms/src/components/FormPreview.tsx`:
- Around line 40-46: The current required-field validation in FormPreview.tsx
treats the string 'false' as non-empty, so single-checkbox fields can bypass
validation; update the validation in the displayContent.fields loop to consider
checkbox fields special: fetch the raw value from fieldValues[field.id], and
mark as empty if it's undefined/null, an empty-trimmed string, or (when
field.type === 'checkbox') if the value is the boolean false or the string
'false'; when empty, set errors[field.id] = true. Use the existing symbols
displayContent.fields, fieldValues[field.id], and field.type to locate and
implement this fix.

In `@community/lingo-global-forms/src/lib/lingo.ts`:
- Around line 3-17: The code currently always constructs a LingoDotDevEngine
with apiKey || '' which creates an engine that will later fail; change this to
only instantiate LingoDotDevEngine when isLingoConfigured() returns true and
apiKey exists, otherwise set lingoEngine to null; update the export for
lingoEngine to be nullable and ensure call sites handle lingoEngine === null
(refer to LingoDotDevEngine, apiKey, isLingoConfigured, and lingoEngine to
locate the change).
🧹 Nitpick comments (9)
community/lingo-global-forms/README.md (2)

78-80: Add language specifier to code block.

The environment variable example would benefit from a language specifier for proper syntax highlighting.

📝 Suggested improvement
-   ```
+   ```env
    VITE_LINGO_API_KEY=your_actual_api_key_here
    ```

Based on static analysis hints.


103-121: Add language specifier to project structure block.

The project structure would be more readable with a language specifier (e.g., tree or text).

📝 Suggested improvement
-```
+```text
 lingo-global-forms/
 ├── src/

Based on static analysis hints.

community/lingo-global-forms/src/index.css (1)

11-23: WebKit-only scrollbar styling with limited transition support.

The custom scrollbar styles only apply to WebKit browsers (Chrome, Safari, Edge). Firefox users will see the default scrollbar. Additionally, transition-colors on ::-webkit-scrollbar-thumb may not animate as expected since scrollbar pseudo-elements have limited CSS property support for transitions.

This is acceptable for a demo application, just noting the cross-browser limitation.

community/lingo-global-forms/src/vite-env.d.ts (1)

3-5: Consider allowing undefined for the API key type.

Declaring VITE_LINGO_API_KEY as strictly string may hide cases where the environment variable is not set. Based on the AI summary, lingo.ts already handles missing keys with a warning, so the type should reflect that the value can be undefined.

Proposed fix
 interface ImportMetaEnv {
-  readonly VITE_LINGO_API_KEY: string;
+  readonly VITE_LINGO_API_KEY: string | undefined;
 }
community/lingo-global-forms/src/components/FormPreview.tsx (1)

50-55: Clear the submit timeout on unmount / repeat submits.
Prevents state updates after unmount and avoids stacking timeouts on rapid submissions.

♻️ Suggested cleanup
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
@@
-  const handleSubmit = (e: React.FormEvent) => {
+  const submitTimeoutRef = useRef<number | null>(null);
+
+  const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
@@
-      setTimeout(() => {
+      if (submitTimeoutRef.current) {
+        window.clearTimeout(submitTimeoutRef.current);
+      }
+      submitTimeoutRef.current = window.setTimeout(() => {
         setSubmitted(false);
         setFieldValues({});
       }, 3000);
     }
   };
+
+  useEffect(() => {
+    return () => {
+      if (submitTimeoutRef.current) {
+        window.clearTimeout(submitTimeoutRef.current);
+      }
+    };
+  }, []);
community/lingo-global-forms/src/components/FormBuilder.tsx (1)

19-23: Use a collision-resistant field id.
Date.now() can collide on rapid clicks; a UUID avoids duplicate keys.

♻️ Proposed change
-    const newField: FormField = {
-      id: `field_${Date.now()}`,
+    const newField: FormField = {
+      id: `field_${crypto.randomUUID?.() ?? `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`}`,
community/lingo-global-forms/src/App.tsx (2)

73-73: Gradient uses identical start and end colors.

The gradient from-green-600 to-green-600 produces no visual gradient effect since both colors are the same. This appears to be a copy-paste artifact or placeholder.

Suggested fix
-            <h1 className="text-lg font-semibold bg-gradient-to-r from-green-600 to-green-600 bg-clip-text text-transparent">
+            <h1 className="text-lg font-semibold text-green-600">

Or, if a gradient effect is desired:

-            <h1 className="text-lg font-semibold bg-gradient-to-r from-green-600 to-green-600 bg-clip-text text-transparent">
+            <h1 className="text-lg font-semibold bg-gradient-to-r from-green-500 to-green-700 bg-clip-text text-transparent">

78-78: Extra whitespace in className.

There are multiple spaces between hover:text-green-500 and transition-colors.

Suggested fix
-          <p className="text-sm text-slate-500 font-medium">
-            Powered by <a href="https://lingo.dev" target="_blank" rel="noopener noreferrer" className="text-green-900 hover:text-green-500    transition-colors">Lingo.dev</a>
+          <p className="text-sm text-slate-500 font-medium">
+            Powered by <a href="https://lingo.dev" target="_blank" rel="noopener noreferrer" className="text-green-900 hover:text-green-500 transition-colors">Lingo.dev</a>
community/lingo-global-forms/src/lib/translateForm.ts (1)

4-5: Unbounded cache growth could cause memory issues.

The translationCache Map can grow indefinitely as users create different forms and translate to different locales. In long-running browser sessions, this could lead to memory pressure.

Consider implementing a bounded cache with LRU eviction or a TTL-based expiration strategy:

Example: Simple LRU-style bounded cache
 // In-memory translation cache
-const translationCache = new Map<string, FormContent>();
+const MAX_CACHE_SIZE = 50;
+const translationCache = new Map<string, FormContent>();
+
+function setWithLimit(key: string, value: FormContent): void {
+  if (translationCache.size >= MAX_CACHE_SIZE) {
+    // Remove oldest entry (first key in Map iteration order)
+    const firstKey = translationCache.keys().next().value;
+    if (firstKey) translationCache.delete(firstKey);
+  }
+  translationCache.set(key, value);
+}

Then replace translationCache.set(cacheKey, translatedContent) on line 112 with setWithLimit(cacheKey, translatedContent).

"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing ESLint dependency for lint script.

The lint script references eslint, but ESLint is not declared in devDependencies. This will cause npm run lint to fail unless ESLint is installed globally.

Proposed fix
  "devDependencies": {
+   "@typescript-eslint/eslint-plugin": "^6.0.0",
+   "@typescript-eslint/parser": "^6.0.0",
    "@types/react": "^18.2.45",
    "@types/react-dom": "^18.2.18",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.23",
+   "eslint": "^8.57.0",
+   "eslint-plugin-react-hooks": "^4.6.0",
+   "eslint-plugin-react-refresh": "^0.4.5",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.19",
    "typescript": "^5.3.3",
    "vite": "^7.3.1"
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.23",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"typescript": "^5.3.3",
"vite": "^7.3.1"
}
🤖 Prompt for AI Agents
In `@community/lingo-global-forms/package.json` at line 9, The package.json "lint"
script references eslint but ESLint is not listed in devDependencies, so add
ESLint to devDependencies (e.g., install and save as a dev dependency) and
update package.json accordingly; locate the "lint" script entry in package.json
and ensure "eslint" (and any required plugins/ configs you rely on) appear under
"devDependencies" so npm run lint works without a global install.
Comment on lines +40 to +46
// Validate required fields
const errors: Record<string, boolean> = {};
displayContent.fields.forEach((field) => {
if (field.required && !fieldValues[field.id]?.trim()) {
errors[field.id] = true;
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Required validation misses unchecked single checkboxes.
For single-checkbox fields, unchecked state is stored as 'false', which passes the current trim() check, so a required checkbox can be submitted unchecked.

🐛 Proposed fix
-    displayContent.fields.forEach((field) => {
-      if (field.required && !fieldValues[field.id]?.trim()) {
-        errors[field.id] = true;
-      }
-    });
+    displayContent.fields.forEach((field) => {
+      const value = fieldValues[field.id];
+      const isEmpty =
+        field.type === 'checkbox' && (!field.options || field.options.length === 0)
+          ? value !== 'true'
+          : !value?.trim();
+      if (field.required && isEmpty) {
+        errors[field.id] = true;
+      }
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate required fields
const errors: Record<string, boolean> = {};
displayContent.fields.forEach((field) => {
if (field.required && !fieldValues[field.id]?.trim()) {
errors[field.id] = true;
}
});
// Validate required fields
const errors: Record<string, boolean> = {};
displayContent.fields.forEach((field) => {
const value = fieldValues[field.id];
const isEmpty =
field.type === 'checkbox' && (!field.options || field.options.length === 0)
? value !== 'true'
: !value?.trim();
if (field.required && isEmpty) {
errors[field.id] = true;
}
});
🤖 Prompt for AI Agents
In `@community/lingo-global-forms/src/components/FormPreview.tsx` around lines 40
- 46, The current required-field validation in FormPreview.tsx treats the string
'false' as non-empty, so single-checkbox fields can bypass validation; update
the validation in the displayContent.fields loop to consider checkbox fields
special: fetch the raw value from fieldValues[field.id], and mark as empty if
it's undefined/null, an empty-trimmed string, or (when field.type ===
'checkbox') if the value is the boolean false or the string 'false'; when empty,
set errors[field.id] = true. Use the existing symbols displayContent.fields,
fieldValues[field.id], and field.type to locate and implement this fix.
@sumitsaurabh927
Copy link
Contributor

hi @saeedsaiyed01 one of your commits is showing up as unverified and also the comments by AI agent need a look.

Please have a look, thanks!

@sumitsaurabh927
Copy link
Contributor

hi @saeedsaiyed01 gentle reminder :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saeedsaiyed01 there's no need for a patch (it will bump the package version/trigger an NPM release).

Can you please update it to an empty changeset.

Just remove this file and run:

npx changeset --empty
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have solved all the missing pieces

  • updated the changeset
  • the code rabbit suggestion

have a look @sumitsaurabh927

@sumitsaurabh927 sumitsaurabh927 merged commit bed7ea3 into lingodotdev:main Jan 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants