-
Notifications
You must be signed in to change notification settings - Fork 825
feat(community): add visual translation manager with AI assistance #1868
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(community): add visual translation manager with AI assistance #1868
Conversation
- Visual translation editor with search and filtering - Live preview component - AI-powered translation suggestions - Multi-format export (JSON, Properties, YAML) - Missing translation detection - Comprehensive documentation
📝 WalkthroughWalkthroughAdds a new community Vite + React project "Lingo.dev Translation Manager" under Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Browser as "Browser App\n(LingoDevTranslationManager)"
participant AI as "AI Assistant\n(client-side)"
participant Exporter as "Exporter / Download"
User->>Browser: Open editor / add/edit/delete/search keys
Browser->>Browser: Update local translation state
Browser->>AI: Request suggestions for missing keys
AI-->>Browser: Return generated suggestions
Browser-->>User: Display suggestions / apply selection
User->>Browser: Trigger export (json/properties/yaml)
Browser->>Exporter: Generate blob and initiate download
Exporter-->>User: Deliver file
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 7
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/package.json`:
- Around line 11-22: Update the vulnerable devDependency "vite" in package.json
from "4.3.9" to at least "4.5.14" (or to "7.3.0" if you want the latest major)
under devDependencies; after changing the "vite" version run your package
manager to refresh the lockfile (npm install / yarn install / pnpm install) and
run the dev/build/test scripts to verify nothing breaks. Ensure the change is
only to the "vite" entry in devDependencies and commit the updated lockfile so
CI uses the patched version.
In `@community/lingo-translation-manager/postcss.config.js`:
- Line 1: The file begins with a UTF-8 BOM before the "export default {" token
which can break some parsers; open the file in your editor and remove the
invisible BOM character at the start, then save the file as UTF-8 without BOM
(e.g., "UTF-8" not "UTF-8 with BOM") so the first character is the "e" of
"export"; re-run linters/build to confirm the BOM is gone.
In `@community/lingo-translation-manager/src/App.jsx`:
- Around line 207-210: The completeness percentage expression can divide by zero
when translations.en is missing or empty; update the JSX expression that
computes percentage (the Math.round(...) block using missingTranslations and
translations) to compute a safe denominator first (e.g., const totalKeys =
Object.keys(translations || {}).length * Object.keys(translations.en ||
{}).length or derive per-locale key count) and if totalKeys is 0 return 0% (or a
sensible default) instead of performing the division; replace the inline
expression with the guarded calculation so missingTranslations, translations,
and translations.en are checked before dividing.
- Around line 109-122: The exportTranslations function leaks memory by not
revoking the Object URL created with URL.createObjectURL and also doesn't
implement the 'yaml' exportFormat; update exportTranslations to (1) generate
YAML when exportFormat === 'yaml' (use a YAML serializer such as js-yaml's
safeDump/YAML.stringify on the translations object or implement a simple
serializer for the properties format when exportFormat === 'properties') and (2)
call URL.revokeObjectURL(url) after triggering the download (e.g., after
link.click() and/or in a setTimeout to ensure the download starts) and remove
the temporary anchor element to avoid leaks; refer to exportTranslations,
exportFormat, and translations to locate the code to change.
- Around line 363-366: The "Generate Missing Translations" button (the JSX
containing <RefreshCw />) has no onClick handler; add an onClick prop that calls
a new handler (e.g., handleGenerateMissingTranslations) and implement that
handler in the same component to perform the generation workflow: set a local
loading state (e.g., isGenerating via setIsGenerating), call the existing
translation generation function or API (or create generateMissingTranslations if
absent), handle success/error and clear loading, and ensure the button UI uses
isGenerating to disable/show progress while the operation runs.
In `@readme.md`:
- Around line 19-29: In the "## 🚀 Quick Start" section the opening code fence
is corrupted (shows "`�ash") so replace the malformed fence with a proper
triple-backtick fence and language tag (```bash) and ensure there is a matching
closing triple-backtick (```) after the commands; update the block under the "##
🚀 Quick Start" heading to use ```bash ... ``` so the commands render as a code
block.
- Around line 51-53: Update the README link target for the usage guide: replace
the markdown link [USAGE.md](./docs/USAGE.md) with the actual relative path or
URL to the usage guide located in the community project (i.e., edit the link
target in the existing "[USAGE.md](./docs/USAGE.md)" text so it points to the
real file under the community directory or to the correct external location).
🧹 Nitpick comments (7)
community/lingo-translation-manager/src/App.jsx (6)
47-49: Missing dependency inuseEffecthook.The
useEffecthook referencescheckMissingTranslationsbut doesn't include it in the dependency array. This can lead to stale closure issues if the function is recreated.♻️ Suggested fix
Either wrap
checkMissingTranslationsinuseCallbackand add it to the dependency array, or inline the logic:+import React, { useState, useEffect, useCallback } from 'react'; ... - const checkMissingTranslations = () => { + const checkMissingTranslations = useCallback(() => { const allKeys = new Set(); Object.values(translations).forEach(lang => { Object.keys(lang).forEach(key => allKeys.add(key)); }); const missing = []; Object.keys(translations).forEach(lang => { allKeys.forEach(key => { if (!translations[lang][key]) { missing.push({ lang, key }); } }); }); setMissingTranslations(missing); - }; + }, [translations]); useEffect(() => { checkMissingTranslations(); - }, [translations]); + }, [checkMissingTranslations]);
91-97: Mutating cloned object's nested properties directly.While
updatedis a shallow copy, the inner language objects are still references to the original state. Usingdeletemutates the original nested objects indirectly.♻️ Suggested immutable approach
const deleteTranslation = (key) => { - const updated = { ...translations }; - Object.keys(updated).forEach(lang => { - delete updated[lang][key]; - }); - setTranslations(updated); + setTranslations(prev => { + const updated = {}; + Object.keys(prev).forEach(lang => { + const { [key]: _, ...rest } = prev[lang]; + updated[lang] = rest; + }); + return updated; + }); };
129-154:DemoPreviewcomponent is redefined on every render.Defining a component inside another component causes it to be recreated on each render, losing any internal state and hurting performance. Move it outside or memoize.
♻️ Extract component outside or use useMemo
Option 1: Move outside and pass props:
// Outside LingoDevTranslationManager const DemoPreview = ({ translations, previewLang }) => { const t = translations[previewLang] || translations.en; return ( // ... existing JSX ); };Option 2: Memoize inline if you need closure access:
-const DemoPreview = () => { - const t = translations[previewLang] || translations.en; - return ( +const demoPreviewContent = useMemo(() => { + const t = translations[previewLang] || translations.en; + return ( // ... existing JSX - ); -}; + ); +}, [translations, previewLang]);
172-179: Export and Import buttons in header are non-functional.The Export button in the header doesn't trigger
exportTranslations(), and the Import button has no implementation. Consider wiring them up or removing them to avoid user confusion.♻️ Wire up the Export button
-<button className="flex items-center gap-2 bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600"> +<button + onClick={exportTranslations} + className="flex items-center gap-2 bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600" +> <Download size={18} /> Export </button>
43-44:aiSuggestionsandshowAiPanelstate variables are unused.These states are initialized but never read or displayed in the UI. The AI suggestions generated by
generateAiSuggestionare stored but not rendered anywhere.Consider either removing the unused state or implementing the UI to display AI suggestions:
// In the missing translations list, show the suggestion if available: {aiSuggestions[`${item.lang}-${item.key}`] && ( <p className="text-sm text-purple-600 mt-2"> Suggestion: {aiSuggestions[`${item.lang}-${item.key}`]} </p> )}
381-387: AI suggestion generated but not applied to translations.The "Suggest" button generates a suggestion and stores it in
aiSuggestions, but there's no way for users to accept and apply the suggestion to the actual translations.Consider adding an "Apply" button when a suggestion exists:
{aiSuggestions[`${item.lang}-${item.key}`] ? ( <button onClick={() => updateTranslation(item.lang, item.key, aiSuggestions[`${item.lang}-${item.key}`])} className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700" > Apply Suggestion </button> ) : ( <button onClick={() => generateAiSuggestion(item.key, 'en', item.lang)} ...> Suggest </button> )}community/lingo-translation-manager/package.json (1)
1-10: Consider addinglicenseandprivatefields.For a community project, it's good practice to explicitly declare the license and mark it as private if it's not intended to be published to npm.
♻️ Suggested additions
{ "name": "lingo-translation-manager", "version": "1.0.0", + "private": true, + "license": "MIT", "description": "Visual translation management tool for Lingo.dev",
|
/assign @Abhinavsharma005 |
|
Hi maintainers 👋 |
|
Thanks for the suggestion! I’ll work on this and update the PR shortly. |
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-translation-manager/src/App.jsx`:
- Line 43: The aiSuggestions state (defined as aiSuggestions and updated via
setAiSuggestions in generateAiSuggestion) is never read or rendered, so users
get no feedback when they click "Suggest"; update the UI to consume
aiSuggestions by locating where each translation row is rendered (use the
item.lang and item.key composite key used when storing suggestions) and either
display the suggestion inline under the translation input or apply it to the
input value when accepted; ensure generateAiSuggestion continues to write to
setAiSuggestions[`${item.lang}-${item.key}`] and add a conditional render near
the translation row that shows aiSuggestions[`${item.lang}-${item.key}`] (and
optionally an “Apply” action to copy it into the translation field).
- Around line 99-107: generateAiSuggestion currently reads
translations[sourceLang][key] without checking for existence, which yields
suggestions like "[AI] undefined (targetLang)"; update generateAiSuggestion to
guard against a missing sourceText by checking translations[sourceLang] and
translations[sourceLang][key] (or using a fallback like an empty string or a
descriptive placeholder) and only setAiSuggestions with a valid suggestion (or
early-return/log) so aiSuggestions and the key `${targetLang}-${key}` never
receive "undefined" as content.
- Around line 91-97: deleteTranslation mutates nested language objects because
it only shallow-copies translations; instead create a new top-level object and
new copies of each language object before removing the key so state stays
immutable. In the deleteTranslation function, build updated as a fresh object
(e.g., iterate Object.keys(translations) and assign each lang to a shallow copy
of translations[lang]), delete the key from that copied lang object, then call
setTranslations(updated) to avoid mutating the original translations state.
- Around line 199-206: The Export and Import header buttons are missing onClick
handlers; wire them to the translation handlers by adding onClick props to the
Export button to call exportTranslations() and to the Import button to call
importTranslations() (or trigger a file input and then
importTranslations(file)); ensure those functions (exportTranslations,
importTranslations) exist or are imported, handle async errors and show user
feedback (toasts or disabled/loading state) while operations run, and keep the
existing button styling and icon components (Download, Upload).
♻️ Duplicate comments (1)
community/lingo-translation-manager/src/App.jsx (1)
399-402: "Generate Missing Translations" button still lacks anonClickhandler.This was previously flagged. The button renders but performs no action when clicked.
🐛 Suggested fix
-<button className="bg-purple-600 text-white px-6 py-2 rounded-lg hover:bg-purple-700 flex items-center gap-2"> +<button + onClick={() => missingTranslations.forEach(item => generateAiSuggestion(item.key, 'en', item.lang))} + className="bg-purple-600 text-white px-6 py-2 rounded-lg hover:bg-purple-700 flex items-center gap-2" +> <RefreshCw size={18} /> Generate Missing Translations </button>
🧹 Nitpick comments (5)
community/lingo-translation-manager/src/App.jsx (5)
2-2: Remove unused icon imports.
Play,Filter, andSaveare imported fromlucide-reactbut never used in the component.♻️ Suggested fix
-import { Globe, Languages, FileText, Download, Upload, Play, CheckCircle, AlertCircle, Sparkles, Code, Eye, Settings, Search, Filter, Plus, X, Save, RefreshCw } from 'lucide-react'; +import { Globe, Languages, FileText, Download, Upload, CheckCircle, AlertCircle, Sparkles, Code, Eye, Settings, Search, Plus, X, RefreshCw } from 'lucide-react';
47-49: Missing dependency inuseEffect.
checkMissingTranslationsis called inside the effect but not listed in the dependency array. While it works here because the function is stable, this pattern triggers ESLint'sexhaustive-depsrule and can cause subtle bugs if the function is refactored later.♻️ Option 1: Inline the logic
useEffect(() => { - checkMissingTranslations(); + const allKeys = new Set(); + Object.values(translations).forEach(lang => { + Object.keys(lang).forEach(key => allKeys.add(key)); + }); + + const missing = []; + Object.keys(translations).forEach(lang => { + allKeys.forEach(key => { + if (!translations[lang][key]) { + missing.push({ lang, key }); + } + }); + }); + setMissingTranslations(missing); }, [translations]);
156-181: Consider movingDemoPreviewoutside the component.Defining
DemoPreviewinsideLingoDevTranslationManagerrecreates the function on every render. While acceptable for a demo, moving it outside (and passing props) or usingReact.memowould be cleaner.
454-456: Export preview only works for JSON format.When users select Properties or YAML, the preview shows a placeholder instead of actual content. Consider showing a preview for all formats for consistency.
♻️ Suggested enhancement
<div className="bg-gray-900 rounded-lg p-6 mb-6 text-white font-mono text-sm overflow-x-auto"> - <pre>{exportFormat === 'json' ? JSON.stringify(translations, null, 2) : 'Preview for other formats...'}</pre> + <pre> + {exportFormat === 'json' + ? JSON.stringify(translations, null, 2) + : exportFormat === 'yaml' + ? Object.entries(translations).map(([lang, keys]) => + `${lang}:\n${Object.entries(keys).map(([k, v]) => ` ${k}: "${v}"`).join('\n')}` + ).join('\n\n') + : Object.entries(translations).map(([lang, keys]) => + `# ${lang.toUpperCase()}\n${Object.entries(keys).map(([k, v]) => `${k}=${v}`).join('\n')}` + ).join('\n\n') + } + </pre> </div>
472-514: Consider adding keyboard support to the modal.The modal lacks keyboard accessibility:
- Pressing
Escapeshould close it- Pressing
Entercould submit the form- Focus should be trapped within the modal
For a quick win, add an
onKeyDownhandler:<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" onKeyDown={(e) => { if (e.key === 'Escape') setShowAddModal(false); }} >
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: 2
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/README.md`:
- Line 33: The README requires Node.js 18+ but package.json is missing the
engines field; update package.json to add an "engines" object with "node":
">=18" (i.e., add "engines": { "node": ">=18" }) so the package.json aligns with
the README and enforces the Node version requirement.
In `@community/lingo-translation-manager/src/App.jsx`:
- Around line 60-62: The current check uses a falsy test on
translations[lang][key], which treats empty string values as missing; update the
validation in the code block that builds missing (the logic referencing
translations, lang, key and pushing into missing) to only treat absent entries
as missing by checking for undefined (or null) explicitly (e.g., verify that
translations[lang] exists and that translations[lang][key] === undefined) so
intentionally empty "" translations are not flagged.
♻️ Duplicate comments (4)
community/lingo-translation-manager/src/App.jsx (4)
43-43:aiSuggestionsstate is populated but never rendered.The
generateAiSuggestionfunction stores suggestions inaiSuggestions, but this state is never displayed in the UI. Users clicking "Suggest" receive no visible feedback.Consider displaying suggestions inline in the AI Assistant tab, e.g.:
{aiSuggestions[`${item.lang}-${item.key}`] && ( <div className="mt-2 p-2 bg-purple-100 rounded"> <p className="text-sm text-purple-700"> Suggestion: {aiSuggestions[`${item.lang}-${item.key}`]} </p> <button onClick={() => updateTranslation(item.lang, item.key, aiSuggestions[`${item.lang}-${item.key}`])}> Apply </button> </div> )}
91-97: State mutation bug persists indeleteTranslation.The shallow copy
{ ...translations }preserves references to nested language objects. Usingdelete updated[lang][key]directly mutates the original state, causing potential React rendering inconsistencies.🐛 Proposed fix
const deleteTranslation = (key) => { - const updated = { ...translations }; - Object.keys(updated).forEach(lang => { - delete updated[lang][key]; - }); + const updated = {}; + Object.keys(translations).forEach(lang => { + const { [key]: _, ...rest } = translations[lang]; + updated[lang] = rest; + }); setTranslations(updated); };
99-107: Missing guard for undefined source text.If
translations[sourceLang][key]is undefined, the suggestion becomes"[AI] undefined (targetLang)". Add a guard:🐛 Proposed fix
const generateAiSuggestion = (key, sourceLang, targetLang) => { - // Simulated AI translation const sourceText = translations[sourceLang][key]; + if (!sourceText) return; + const suggestion = `[AI] ${sourceText} (${targetLang})`; setAiSuggestions(prev => ({ ...prev, [`${targetLang}-${key}`]: suggestion })); };
198-207: Header Export/Import buttons still have no handlers.These buttons appear clickable but do nothing. Wire them up or remove them to avoid user confusion.
🐛 Quick fix for Export button
-<button className="flex items-center gap-2 bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600"> +<button + onClick={exportTranslations} + className="flex items-center gap-2 bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600" +> <Download size={18} /> Export </button>For Import, you'll need to implement file input handling or remove the button if not planned.
🧹 Nitpick comments (9)
community/lingo-translation-manager/README.md (1)
1-61: Consider adding screenshots or demo GIFs to showcase the visual interface.For a visual translation management tool, the README would benefit significantly from screenshots or animated GIFs demonstrating the key features (editor, live preview, AI suggestions, export options). This is especially important for a community showcase project where visual appeal helps attract users and contributors.
📸 Suggested sections to add
Consider adding a "Screenshots" or "Demo" section after the Features section:
## 📸 Screenshots ### Translation Editor  *Visual editor with search and filtering* ### Live Preview  *See translations in real-time* ### AI Suggestions  *Automatic translation suggestions*Or add a "Live Demo" section if hosting is available:
## 🎬 Live Demo [Try the live demo →](https://your-demo-url.com)community/lingo-translation-manager/src/App.jsx (8)
2-2: UnusedSaveicon import.The
Saveicon is imported but never used in the component. Consider removing it to keep imports clean.-import { Globe, Languages, FileText, Download, Upload, Play, CheckCircle, AlertCircle, Sparkles, Code, Eye, Settings, Search, Filter, Plus, X, Save, RefreshCw } from 'lucide-react'; +import { Globe, Languages, FileText, Download, Upload, CheckCircle, AlertCircle, Sparkles, Code, Eye, Settings, Search, Plus, X, RefreshCw } from 'lucide-react';Note:
PlayandFilterare also imported but unused.
212-212: Stats grid lacks responsive breakpoints.
grid-cols-4will cause horizontal overflow or cramped cards on smaller screens. Consider responsive classes:-<div className="grid grid-cols-4 gap-4 mb-6"> +<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
325-333: Hardcoded language options in dropdowns.Language options are hardcoded while translations state is dynamic. Consider generating options from state for consistency:
{Object.keys(translations).map(lang => ( <option key={lang} value={lang}> {lang.toUpperCase()} </option> ))}This applies to all three language selects (editor, preview, and export settings).
457-459: Export preview incomplete for non-JSON formats.The preview shows actual content only for JSON; other formats display a placeholder. Consider reusing the export format logic:
<pre> {exportFormat === 'json' ? JSON.stringify(translations, null, 2) : exportFormat === 'yaml' ? Object.entries(translations).map(([lang, keys]) => `${lang}:\n${Object.entries(keys).map(([k, v]) => ` ${k}: "${v}"`).join('\n')}` ).join('\n\n') : Object.entries(translations).map(([lang, keys]) => `# ${lang.toUpperCase()}\n${Object.entries(keys).map(([k, v]) => `${k}=${v}`).join('\n')}` ).join('\n\n') } </pre>
399-405: "Generate Missing Translations" button lacks loading/disabled state.The button has a handler now, but there's no loading indicator or disabled state during generation. For bulk operations, this could lead to duplicate clicks:
const [isGenerating, setIsGenerating] = useState(false); // In handler: const handleGenerateMissing = async () => { setIsGenerating(true); missingTranslations.forEach(item => generateAiSuggestion(item.key, 'en', item.lang)); setIsGenerating(false); }; // Button: <button onClick={handleGenerateMissing} disabled={isGenerating} className={`... ${isGenerating ? 'opacity-50 cursor-not-allowed' : ''}`} > <RefreshCw size={18} className={isGenerating ? 'animate-spin' : ''} /> {isGenerating ? 'Generating...' : 'Generate Missing Translations'} </button>
413-413: Missing translations list silently truncated to 5 items.Users with many missing translations see only the first 5 with no indication that more exist. Consider showing a count or adding pagination:
{missingTranslations.length > 5 && ( <p className="text-sm text-gray-500 mt-2"> Showing 5 of {missingTranslations.length} missing translations </p> )}
156-181:DemoPreviewdefined inside component recreates on every render.While acceptable for a demo, extracting
DemoPreviewoutside or wrapping withuseMemowould prevent unnecessary re-creation:// Option 1: Extract outside component const DemoPreview = ({ translations, previewLang }) => { const t = translations[previewLang] || translations.en || {}; // ... }; // Option 2: Memoize inside const DemoPreview = useMemo(() => { const t = translations[previewLang] || translations.en; return (/* JSX */); }, [translations, previewLang]);
475-517: Modal lacks keyboard accessibility.Consider adding keyboard handlers for better UX:
- Escape key to close modal
- Enter key to submit (when inputs are focused)
useEffect(() => { const handleKeyDown = (e) => { if (e.key === 'Escape') setShowAddModal(false); }; if (showAddModal) { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); } }, [showAddModal]);
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: 1
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/README.md`:
- Line 65: The documentation link in
community/lingo-translation-manager/README.md points to an incorrect path;
update the link target from ./community/lingo-translation-manager/docs/USAGE.md
to the correct relative path ./docs/USAGE.md (or docs/USAGE.md) so it resolves
from the README's directory.
🧹 Nitpick comments (1)
community/lingo-translation-manager/README.md (1)
31-41: Add directory context to quick start commands.The quick start commands don't specify the working directory. Since this package is in a subdirectory, users might be confused about where to run these commands. Consider adding a note like "Run these commands from the
community/lingo-translation-managerdirectory" or include acdcommand.📝 Suggested improvement
## 🚀 Quick Start ```bash +# Navigate to the project directory +cd community/lingo-translation-manager + # Install dependencies npm install
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: 2
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/src/App.jsx`:
- Around line 200-206: Remove the stray plus sign before the JSX button so it
doesn't render as visible text; locate the button that calls exportTranslations
and renders the Download icon (the <button onClick={exportTranslations} ...>
block) and delete the leading '+' character so the JSX starts with the button
element only.
- Around line 207-210: The Import button currently has no click handler; add
basic import wiring in the App component by creating a hidden <input
type="file"> and a handler (e.g., handleImportClick to call
fileInputRef.current.click() and handleFileSelected to read the file) and attach
handleImportClick to the Import button's onClick and handleFileSelected to the
input's onChange; alternatively, if you intend to disable import for now, set
the button to disabled or remove it. Ensure you reference the Upload button
element (the JSX that renders <Upload /> / "Import") and the new handlers
(handleImportClick, handleFileSelected) so reviewers can find the change easily.
♻️ Duplicate comments (1)
community/lingo-translation-manager/src/App.jsx (1)
60-62: Empty string values still treated as missing.The condition
!translations[lang][key]is falsy for empty strings"", meaning intentionally empty translations will be incorrectly flagged as missing.Use a strict check if empty values are valid:
- if (!translations[lang][key]) { + if (translations[lang][key] === undefined) {
🧹 Nitpick comments (4)
community/lingo-translation-manager/package.json (2)
1-1: BOM character detected at file start.The file begins with a UTF-8 BOM (
). While this typically doesn't break JSON parsing, it can cause issues with certain build tools, linters, or CI pipelines that expect clean UTF-8 without BOM.🔧 Remove the BOM
Save the file with UTF-8 encoding without BOM, or run:
sed -i '1s/^\xEF\xBB\xBF//' community/lingo-translation-manager/package.json
7-9: Inconsistent indentation inenginesblock.The
engineskey has an extra leading space compared to other top-level keys.🔧 Fix indentation
- "engines": { + "engines": { "node": ">=18" },community/lingo-translation-manager/src/App.jsx (2)
157-182:DemoPreviewrecreated on every render.Defining
DemoPreviewas an inline function inside the component causes React to treat it as a new component on each render, which destroys and remounts the DOM unnecessarily. Extract it outside or memoize it.♻️ Extract as a separate component
// Outside LingoDevTranslationManager const DemoPreview = ({ translations, previewLang }) => { const t = translations[previewLang] || translations.en; return ( <div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-8 border-2 border-blue-200"> {/* ... rest of JSX ... */} </div> ); }; // Inside the component, use: // <DemoPreview translations={translations} previewLang={previewLang} />
483-483: Export preview only shows JSON format.The preview area displays
'Preview for other formats...'for non-JSON formats instead of the actual formatted output. Consider showing the real preview for all formats to improve UX.♻️ Show actual preview for all formats
Extract the format logic into a helper or reuse it:
<pre> {exportFormat === 'json' ? JSON.stringify(translations, null, 2) : exportFormat === 'yaml' ? Object.entries(translations) .map(([lang, keys]) => `${lang}:\n${Object.entries(keys).map(([k, v]) => ` ${k}: "${v}"`).join('\n')}` ) .join('\n\n') : Object.entries(translations) .map(([lang, keys]) => `# ${lang.toUpperCase()}\n${Object.entries(keys).map(([k, v]) => `${k}=${v}`).join('\n')}` ) .join('\n\n')} </pre>
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: 5
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/README.md`:
- Around line 69-70: The README.md currently has a broken Markdown link/line
break ("See [USAGE.md](./docs/USAGE.md) for detailed usage instructions.") split
across lines which breaks rendering; fix README.md by joining the link and its
trailing text onto a single line so the sentence reads: "See
[USAGE.md](./docs/USAGE.md) for detailed usage instructions." ensuring the link
target "./docs/USAGE.md" remains unchanged.
- Line 78: Replace the incorrect MIT license statement in README.md ("MIT - Same
as Lingo.dev repository") with the correct Apache License 2.0 wording; update
the license line to reference "Apache License 2.0" (or "Apache-2.0") and, if
present elsewhere in the file, adjust any attribution text to match the parent
repository's Apache 2.0 license; ensure the README's license string exactly
matches the canonical identifier used in the project (e.g., "Apache License 2.0"
or "Apache-2.0").
In `@community/lingo-translation-manager/src/App.jsx`:
- Around line 60-62: The condition currently uses the negation operator with the
equality check causing operator precedence issues; in the block that iterates
over translations (references: translations, lang, key, missing.push) replace
the faulty check `!translations[lang][key] === undefined` with a direct
undefined/null check such as `translations[lang][key] === undefined` or
`translations[lang][key] == null` (to catch both null and undefined) so missing
translations are correctly detected before pushing into missing.
- Around line 212-226: The import onChange handler parses the file but lacks
validation and user feedback; update the reader.onload block to validate the
parsed JSON before calling setTranslations: ensure the parsed value is a
non-null object, each top-level key maps to an object, and each nested value is
a string (or whatever schema your translations require), and only call
setTranslations(imported) if validation passes; on validation failure or JSON
parse error, set a visible error state (e.g., setImportError or setSnackbar)
with a clear message, and on success set a success state/message so the user
gets feedback; keep these checks inside the existing reader.onload/try-catch
surrounding JSON.parse and use the existing setTranslations symbol to update
state when valid.
- Around line 506-508: The preview pane is only rendering JSON and a placeholder
for other formats; update the JSX that renders the <pre> so exportFormat and
translations drive actual formatted output for 'properties' and 'yaml' as well
as 'json'. Implement or call formatting helpers (e.g.,
convertToProperties(translations) and convertToYaml(translations) or use an
existing YAML serializer) and replace the placeholder string with the result for
exportFormat === 'properties' and exportFormat === 'yaml', keeping the current
JSON behavior for exportFormat === 'json' and ensuring the component (App /
export preview block) displays the formatted string.
🧹 Nitpick comments (2)
community/lingo-translation-manager/src/App.jsx (2)
157-182: Consider extractingDemoPreviewoutside the component or memoizing it.Defining
DemoPreviewinsideLingoDevTranslationManagercauses it to be recreated on every render. For this demo, the impact is negligible, but extracting it as a separate component (receivingtranslationsandpreviewLangas props) would be cleaner.
353-361: Language options are hardcoded.The dropdown options are static (
en,es,fr) while thetranslationsstate could contain different languages after import. For a demo, this is acceptable, but dynamically generating options fromObject.keys(translations)would be more robust.♻️ Optional improvement
<select value={selectedLang} onChange={(e) => setSelectedLang(e.target.value)} className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" > - <option value="en">English</option> - <option value="es">Spanish</option> - <option value="fr">French</option> + {Object.keys(translations).map(lang => ( + <option key={lang} value={lang}>{lang.toUpperCase()}</option> + ))} </select>
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: 2
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/src/App.jsx`:
- Around line 381-389: The language <select> options are hard-coded which
prevents imported languages from being shown; change both dropdowns to build
options from Object.keys(translations) (e.g., map over Object.keys(translations)
to render <option value={lang}>{lang}</option>) and update the selectedLang
handling so imports keep a valid selection (after importing translations, if
selectedLang is not in Object.keys(translations) call setSelectedLang to a
fallback such as the first key or 'en'). Update the components referencing
selectedLang and the second selector (the other <select> using the same state)
accordingly so both become data-driven.
- Around line 160-173: DemoPreview can crash when translations[previewLang] and
translations.en are undefined; update DemoPreview to defensively handle missing
translation objects by resolving t to a safe fallback (e.g., an empty object or
a minimal default map) and/or short-circuit rendering when translations is
falsy. Locate the DemoPreview component and change the t assignment (currently
using translations[previewLang] || translations.en) to a robust check that
ensures translations and translations.en exist before accessing them, and ensure
usage of t in JSX uses safe lookups or returns a placeholder UI when no
translations are available.
♻️ Duplicate comments (1)
community/lingo-translation-manager/src/App.jsx (1)
103-106: Guard AI suggestions when the source language is missing.
translations[sourceLang]can beundefined(e.g., import withouten), which throws before your guard runs.🔧 Suggested fix
- const sourceText = translations[sourceLang][key]; - if (!sourceText) return; + const sourceText = translations[sourceLang]?.[key]; + if (!sourceText) return;
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: 2
🤖 Fix all issues with AI agents
In `@community/lingo-translation-manager/src/App.jsx`:
- Around line 8-10: The component LingoDevTranslationManager initializes
activeTab with 'editor' but never changes it, so the preview never becomes
reachable; add a simple tab switcher UI (e.g., two buttons or tabs) that calls
setActiveTab('editor') and setActiveTab('preview') to toggle state, and ensure
the preview rendering branch checks activeTab === 'preview' (or alternatively
render the preview unconditionally) — update the tab toggle implementation
referenced around activeTab/setActiveTab and the corresponding preview render
logic (also adjust the duplicated logic around lines where activeTab is used
again, e.g., the block referenced at 256-257) so the preview can be shown.
- Around line 233-251: The UI renders per-key rows but never triggers
generateAiSuggestion, so AI suggestions never appear; add a per-row "Suggest"
button in the mapped block that calls generateAiSuggestion(selectedLang, key)
(or a bulk button that iterates filteredKeys) and ensure the onClick references
the existing generateAiSuggestion function and updates aiSuggestions state;
place the button next to the "Apply" control inside the filteredKeys map where
translations[selectedLang][key], aiSuggestions[`\${selectedLang}-\${key}`],
updateTranslation, and selectedLang are already used so the function call is
wired to those symbols.
♻️ Duplicate comments (1)
community/lingo-translation-manager/src/App.jsx (1)
116-143: Guard import when no file is selected and validate nested structure.
Cancelling the file picker passesundefined, which will throw inreadAsText. Also, downstream logic assumes each language is an object of string values—malformed JSON can crashcheckMissingTranslationsor rendering.🔧 Proposed fix
const handleImport = (file) => { + if (!file) { + setImportError('No file selected'); + setImportSuccess(''); + return; + } const reader = new FileReader(); reader.onload = (e) => { try { const imported = JSON.parse(e.target.result); - if (!imported || typeof imported !== 'object') { + const isValid = + imported && + typeof imported === 'object' && + Object.values(imported).every( + (langObj) => + langObj && + typeof langObj === 'object' && + !Array.isArray(langObj) && + Object.values(langObj).every((v) => typeof v === 'string') + ); + if (!isValid) { throw new Error('Invalid translation file'); }
| const LingoDevTranslationManager = () => { | ||
| const [activeTab, setActiveTab] = useState('editor'); | ||
| const [translations, setTranslations] = useState({ |
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.
Live preview is unreachable because activeTab never changes.
There’s no UI to switch activeTab, so the preview never renders. Add a tab toggle or render the preview unconditionally.
🔧 Minimal fix (make preview reachable)
- {activeTab === 'preview' && <DemoPreview />}
+ <DemoPreview />Also applies to: 256-257
🤖 Prompt for AI Agents
In `@community/lingo-translation-manager/src/App.jsx` around lines 8 - 10, The
component LingoDevTranslationManager initializes activeTab with 'editor' but
never changes it, so the preview never becomes reachable; add a simple tab
switcher UI (e.g., two buttons or tabs) that calls setActiveTab('editor') and
setActiveTab('preview') to toggle state, and ensure the preview rendering branch
checks activeTab === 'preview' (or alternatively render the preview
unconditionally) — update the tab toggle implementation referenced around
activeTab/setActiveTab and the corresponding preview render logic (also adjust
the duplicated logic around lines where activeTab is used again, e.g., the block
referenced at 256-257) so the preview can be shown.
| {/* EDITOR */} | ||
| {filteredKeys.map(key => ( | ||
| <div key={key} className="bg-white p-4 mb-2 rounded"> | ||
| <input | ||
| value={translations[selectedLang][key] || ''} | ||
| onChange={e => updateTranslation(selectedLang, key, e.target.value)} | ||
| className="border p-2 w-full" | ||
| /> | ||
|
|
||
| {aiSuggestions[`${selectedLang}-${key}`] && ( | ||
| <div className="mt-2 text-purple-600 text-sm"> | ||
| 🤖 {aiSuggestions[`${selectedLang}-${key}`]} | ||
| <button | ||
| className="ml-2 underline" | ||
| onClick={() => updateTranslation(selectedLang, key, aiSuggestions[`${selectedLang}-${key}`])} | ||
| > | ||
| Apply | ||
| </button> | ||
| </div> |
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.
AI suggestions can’t be generated from the UI.
generateAiSuggestion is never called, so users cannot trigger suggestions. Add a “Suggest” action per key (or a bulk button) to invoke it.
🔧 Example per-row suggest button
{filteredKeys.map(key => (
<div key={key} className="bg-white p-4 mb-2 rounded">
- <input
- value={translations[selectedLang][key] || ''}
- onChange={e => updateTranslation(selectedLang, key, e.target.value)}
- className="border p-2 w-full"
- />
+ <div className="flex gap-2">
+ <input
+ value={translations[selectedLang][key] || ''}
+ onChange={e => updateTranslation(selectedLang, key, e.target.value)}
+ className="border p-2 w-full"
+ />
+ <button
+ onClick={() => generateAiSuggestion(key, 'en', selectedLang)}
+ disabled={selectedLang === 'en'}
+ className="px-3 py-2 rounded bg-purple-600 text-white disabled:opacity-50"
+ >
+ Suggest
+ </button>
+ </div>🤖 Prompt for AI Agents
In `@community/lingo-translation-manager/src/App.jsx` around lines 233 - 251, The
UI renders per-key rows but never triggers generateAiSuggestion, so AI
suggestions never appear; add a per-row "Suggest" button in the mapped block
that calls generateAiSuggestion(selectedLang, key) (or a bulk button that
iterates filteredKeys) and ensure the onClick references the existing
generateAiSuggestion function and updates aiSuggestions state; place the button
next to the "Apply" control inside the filteredKeys map where
translations[selectedLang][key], aiSuggestions[`\${selectedLang}-\${key}`],
updateTranslation, and selectedLang are already used so the function call is
wired to those symbols.
🌍 Lingo.dev Translation Manager
Overview
A visual translation management tool that demonstrates Lingo.dev integration with React, featuring AI-powered assistance and live preview capabilities.
Features
Why This Contribution?
This project showcases how Lingo.dev can be integrated into a user-friendly visual workflow, making translation management accessible to both developers and non-technical team members.
📸 Screenshots
1. Translation Editor
2. Live Preview
3. AI Assistance
4. Export Settings
Checklist
npm run dev)Testing
cd community/lingo-translation-manager npm install npm run devDemo
Live demo: [Add Vercel/Netlify link if you deployed]
Summary by CodeRabbit
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.