-
Notifications
You must be signed in to change notification settings - Fork 829
feat: Add Global Form Auto-Localization demo to community #1889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add Global Form Auto-Localization demo to community #1889
Conversation
📝 WalkthroughWalkthroughThis PR adds a complete Vite + React demo app under community/lingo-global-forms: form builder, live preview, language selector, Lingo.dev integration for translating form content with in-memory caching, and project configuration, docs, and tooling files. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App
participant FormBuilder
participant FormPreview
participant TranslateModule
participant LingoAPI
User->>FormBuilder: Edit form
FormBuilder->>App: onFormChange(updatedForm)
App->>FormPreview: pass updatedForm
User->>FormPreview: Select language
FormPreview->>App: handleLocaleChange(locale)
App->>TranslateModule: translateFormContent(form, locale, onProgress)
TranslateModule->>TranslateModule: check cache / flatten content
TranslateModule->>LingoAPI: localizeObject request
LingoAPI-->>TranslateModule: translated object
TranslateModule->>TranslateModule: unflatten + cache result
TranslateModule-->>App: translatedContent
App->>FormPreview: pass translatedContent / isTranslating
FormPreview->>User: render localized form
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this 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.,
treeortext).📝 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-colorson::-webkit-scrollbar-thumbmay 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 allowingundefinedfor the API key type.Declaring
VITE_LINGO_API_KEYas strictlystringmay hide cases where the environment variable is not set. Based on the AI summary,lingo.tsalready 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-600produces 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-500andtransition-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
translationCacheMap 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 withsetWithLimit(cacheKey, translatedContent).
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "tsc && vite build", | ||
| "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| "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.
| type="button" | ||
| onClick={() => addField('checkbox')} | ||
| className="flex items-center justify-center px-3 py-2 text-xs font-medium text-slate-700 bg-white border border-slate-200 rounded-md hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm" | ||
| > | ||
| <span className="mr-2">ballot_check</span> Checkbox | ||
| </button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checkbox button label renders as raw text.
“ballot_check” looks like a placeholder token rather than an icon.
🔧 Quick fix
- <span className="mr-2">ballot_check</span> Checkbox
+ <span className="mr-2">☑️</span> Checkbox📝 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.
| type="button" | |
| onClick={() => addField('checkbox')} | |
| className="flex items-center justify-center px-3 py-2 text-xs font-medium text-slate-700 bg-white border border-slate-200 rounded-md hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm" | |
| > | |
| <span className="mr-2">ballot_check</span> Checkbox | |
| </button> | |
| type="button" | |
| onClick={() => addField('checkbox')} | |
| className="flex items-center justify-center px-3 py-2 text-xs font-medium text-slate-700 bg-white border border-slate-200 rounded-md hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm" | |
| > | |
| <span className="mr-2">☑️</span> Checkbox | |
| </button> |
🤖 Prompt for AI Agents
In `@community/lingo-global-forms/src/components/FormBuilder.tsx` around lines 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.
| // Validate required fields | ||
| const errors: Record<string, boolean> = {}; | ||
| displayContent.fields.forEach((field) => { | ||
| if (field.required && !fieldValues[field.id]?.trim()) { | ||
| errors[field.id] = true; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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.
| const apiKey = import.meta.env.VITE_LINGO_API_KEY; | ||
|
|
||
| if (!apiKey) { | ||
| console.warn( | ||
| '⚠️ VITE_LINGO_API_KEY is not set. Translations will not work.\n' + | ||
| 'Create a .env file with your API key. See .env.example for reference.' | ||
| ); | ||
| } | ||
|
|
||
| export const lingoEngine = new LingoDotDevEngine({ | ||
| apiKey: apiKey || '', | ||
| apiUrl: typeof window !== 'undefined' | ||
| ? `${window.location.origin}/api/lingo` | ||
| : 'https://engine.lingo.dev', | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the file under review and explore repository structure
fd "lingo.ts" --type f | head -20Repository: lingodotdev/lingo.dev
Length of output: 150
🏁 Script executed:
#!/bin/bash
# Look for the SDK definition and apiKey validation
fd "index.ts" packages/sdk --type f
find . -name "*.ts" -path "*/sdk/*" | head -20Repository: lingodotdev/lingo.dev
Length of output: 282
🏁 Script executed:
#!/bin/bash
# Search for LingoDotDevEngine definition
rg -l "LingoDotDevEngine" --type ts | head -10Repository: lingodotdev/lingo.dev
Length of output: 397
🏁 Script executed:
#!/bin/bash
# Read the SDK implementation to check LingoDotDevEngine definition
cat -n packages/sdk/src/index.ts | head -150Repository: lingodotdev/lingo.dev
Length of output: 6093
🏁 Script executed:
#!/bin/bash
# Read the file under review
cat -n community/lingo-global-forms/src/lib/lingo.tsRepository: lingodotdev/lingo.dev
Length of output: 782
🏁 Script executed:
#!/bin/bash
# Search for how lingoEngine is used across the codebase
rg "lingoEngine" --type ts -A 2 -B 2 | head -100Repository: lingodotdev/lingo.dev
Length of output: 1209
Don't instantiate LingoDotDevEngine when API key is missing.
The SDK schema does not enforce non-empty apiKey (it only requires Z.string()), so passing an empty string won't throw during construction. However, it will fail at runtime when making API calls with an invalid Authorization header. Conditionally instantiate only when configured to avoid unexpected failures downstream.
The codebase already has isLingoConfigured() to check for valid configuration—use it to guard instantiation.
Suggested fix
-const apiKey = import.meta.env.VITE_LINGO_API_KEY;
+const apiKey = import.meta.env.VITE_LINGO_API_KEY?.trim();
if (!apiKey) {
console.warn(
'⚠️ VITE_LINGO_API_KEY is not set. Translations will not work.\n' +
'Create a .env file with your API key. See .env.example for reference.'
);
}
-export const lingoEngine = new LingoDotDevEngine({
- apiKey: apiKey || '',
- apiUrl: typeof window !== 'undefined'
- ? `${window.location.origin}/api/lingo`
- : 'https://engine.lingo.dev',
-});
+export const lingoEngine = apiKey
+ ? new LingoDotDevEngine({
+ apiKey,
+ apiUrl: typeof window !== 'undefined'
+ ? `${window.location.origin}/api/lingo`
+ : 'https://engine.lingo.dev',
+ })
+ : null;Call sites should handle lingoEngine === null when not configured.
📝 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.
| const apiKey = import.meta.env.VITE_LINGO_API_KEY; | |
| if (!apiKey) { | |
| console.warn( | |
| '⚠️ VITE_LINGO_API_KEY is not set. Translations will not work.\n' + | |
| 'Create a .env file with your API key. See .env.example for reference.' | |
| ); | |
| } | |
| export const lingoEngine = new LingoDotDevEngine({ | |
| apiKey: apiKey || '', | |
| apiUrl: typeof window !== 'undefined' | |
| ? `${window.location.origin}/api/lingo` | |
| : 'https://engine.lingo.dev', | |
| }); | |
| const apiKey = import.meta.env.VITE_LINGO_API_KEY?.trim(); | |
| if (!apiKey) { | |
| console.warn( | |
| '⚠️ VITE_LINGO_API_KEY is not set. Translations will not work.\n' + | |
| 'Create a .env file with your API key. See .env.example for reference.' | |
| ); | |
| } | |
| export const lingoEngine = apiKey | |
| ? new LingoDotDevEngine({ | |
| apiKey, | |
| apiUrl: typeof window !== 'undefined' | |
| ? `${window.location.origin}/api/lingo` | |
| : 'https://engine.lingo.dev', | |
| }) | |
| : null; |
🤖 Prompt for AI Agents
In `@community/lingo-global-forms/src/lib/lingo.ts` around lines 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).
|
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! |
|
hi @saeedsaiyed01 gentle reminder :) |
Closes #1761
🚀 New Community Demo: Global Form Auto-Localization
This PR adds a new demo application to the
/communitydirectory that showcases the power of Lingo.dev’s structured object translation in a real-world product scenario.💡 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:
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 devCloses #1761
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.