feat: Implement Internationalization (i18n) Support #771
feat: Implement Internationalization (i18n) Support #771kartikrautan wants to merge 3 commits intoaccordproject:mainfrom
Conversation
✅ Deploy Preview for ap-template-playground ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Hello @mttrbrts can you review this PR, making the application accessible to non-English speakers? thanks |
There was a problem hiding this comment.
Pull request overview
Implements internationalization across the Template Playground UI using react-i18next, adds language detection/persistence, and introduces a Navbar language switcher so users can toggle languages at runtime.
Changes:
- Added i18n initialization (
src/i18n.ts) and wired it into app startup (src/main.tsx). - Replaced hardcoded UI strings with
t()lookups across major components and addeden/fr/estranslation resources. - Updated component tests/snapshots to accommodate translated strings via
react-i18nextmocks.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/i18n.ts | Adds i18next + language detector initialization and resources. |
| src/main.tsx | Loads i18n initialization at app startup. |
| src/locales/en/translation.json | English translation resource bundle. |
| src/locales/fr/translation.json | French translation resource bundle. |
| src/locales/es/translation.json | Spanish translation resource bundle. |
| src/components/Navbar.tsx | Adds language switcher UI and converts Navbar labels to t(). |
| src/components/PlaygroundSidebar.tsx | Converts sidebar labels and messages to t(). |
| src/components/ProblemPanel.tsx | Converts visible strings (Problems/Line/Col/empty state) to t(). |
| src/components/Footer.tsx | Converts footer strings to t(). |
| src/components/SettingsModal.tsx | Converts modal strings to t(). |
| src/components/FullScreenModal.tsx | Converts modal title to t(). |
| src/pages/MainContainer.tsx | Converts editor/preview UI strings and error to t(). |
| src/utils/helpers/errorUtils.ts | Minor refactor (let → const) in error extraction helper. |
| src/tests/components/Navbar.test.tsx | Adds react-i18next mock for translated Navbar text. |
| src/tests/components/PlaygroundSidebar.test.tsx | Adds react-i18next mock for translated sidebar text. |
| src/tests/components/SettingsModal.test.tsx | Adds react-i18next mock for translated settings strings. |
| src/tests/components/Footer.test.tsx | Adds react-i18next mock for snapshot stability. |
| src/tests/components/snapshots/Footer.test.tsx.snap | Updates snapshot to match mocked translation output. |
| package.json | Adds i18next dependencies. |
| {/* Language Switcher */} | ||
| <div | ||
| className={`h-16 flex items-center justify-center rounded-md cursor-pointer ${ | ||
| screens.md | ||
| ? "px-5 border-l border-white border-opacity-10 pl-4 pr-4" | ||
| className={`h-16 flex items-center justify-center rounded-md cursor-pointer ${screens.md | ||
| ? "px-5 border-l border-white border-opacity-10 pl-4 pr-4" | ||
| : "px-2.5 pl-1.5 pr-1.5" | ||
| } ${ | ||
| hovered === "discord" ? "bg-white bg-opacity-10" : "bg-transparent" | ||
| }`} | ||
| } ${hovered === "language" ? "bg-white bg-opacity-10" : "bg-transparent" | ||
| }`} | ||
| onMouseEnter={() => setHovered("language")} | ||
| onMouseLeave={() => setHovered(null)} | ||
| > | ||
| <Dropdown overlay={languageMenu} trigger={["click"]}> | ||
| <Button className="bg-transparent border-none text-white h-16 flex items-center cursor-pointer"> | ||
| <MdLanguage className={`text-xl text-white ${screens.md ? "mr-1.5" : "mr-0" | ||
| }`} /> | ||
| <span className={screens.md ? "inline" : "hidden"}>{currentLang.flag} {currentLang.label}</span> | ||
| </Button> | ||
| </Dropdown> |
There was a problem hiding this comment.
New user-facing language switching (including persistence via the language detector/localStorage) isn’t covered by unit or E2E tests. Add a test that verifies selecting a language calls i18n.changeLanguage and that the selection is persisted/restored (e.g., by asserting the chosen language is read from localStorage on reload).
| .init({ | ||
| resources: { | ||
| en: { translation: enTranslation }, | ||
| fr: { translation: frTranslation }, | ||
| es: { translation: esTranslation }, | ||
| }, | ||
| fallbackLng: 'en', | ||
| interpolation: { | ||
| escapeValue: false, | ||
| }, | ||
| detection: { | ||
| order: ['localStorage', 'navigator'], | ||
| caches: ['localStorage'], | ||
| }, | ||
| }); |
There was a problem hiding this comment.
i18next is configured without supportedLngs/load: 'languageOnly' (or nonExplicitSupportedLngs). With browser detection this can resolve to en-US/fr-FR, which don’t exist in resources and can leave i18n.language set to a value your UI doesn’t recognize. Consider restricting to supported languages and loading language-only variants so the resolved language always maps cleanly to your resources and UI language list.
| vi.mock('react-i18next', () => ({ | ||
| useTranslation: () => ({ | ||
| t: (key: string) => { | ||
| const map: Record<string, string> = { | ||
| 'sidebar.editor': 'Editor', | ||
| 'sidebar.preview': 'Preview', | ||
| 'sidebar.problems': 'Problems', | ||
| 'sidebar.aiAssistant': 'AI Assistant', | ||
| 'sidebar.share': 'Share', | ||
| 'sidebar.startTour': 'Start Tour', | ||
| 'sidebar.settings': 'Settings', | ||
| 'sidebar.fullscreen': 'Fullscreen' | ||
| }; | ||
| return map[key] || key; | ||
| }, | ||
| i18n: { language: 'en', changeLanguage: vi.fn() } | ||
| }) | ||
| })); |
There was a problem hiding this comment.
react-i18next is mocked inline here. To reduce duplication and drift across tests, consider centralizing this mock in the existing Vitest setup file (src/utils/testing/setup.ts) and only overriding per-test when a specific translation behavior is needed.
| onClick={onClick} | ||
| className={`group playground-sidebar-nav-bottom-item tour-${title.toLowerCase().replace(' ', '-')}`} | ||
| > |
There was a problem hiding this comment.
Same issue for bottom nav: tour-* class names are derived from translated titles, which breaks Shepherd tour selectors (e.g., .tour-share, .tour-start-tour, .tour-settings) once the language is changed. Use stable identifiers for these class names rather than the localized title.
| const formatTimestamp = (timestamp: Date) => { | ||
| return timestamp.toLocaleTimeString('en-US', { | ||
| hour12: false, | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| second: '2-digit' | ||
| return timestamp.toLocaleTimeString('en-US', { | ||
| hour12: false, | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| second: '2-digit' | ||
| }); |
There was a problem hiding this comment.
formatTimestamp hardcodes the locale to 'en-US', so timestamps won’t follow the user-selected language/locale. Use the current i18n language (or omit the locale to use the browser default) and consider formatting via Intl.DateTimeFormat with the active locale.
src/tests/components/Navbar.test.tsx
Outdated
| "navbar.templatePlayground": "Template Playground", | ||
| "navbar.github": "Github", // the original test asserted for Github with a capital G | ||
| }; |
There was a problem hiding this comment.
Brand spelling: use “GitHub” capitalization consistently. This mock returns “Github” (and the test assertions likely follow), which won’t match the actual UI/translation resources and bakes in the wrong label—update the mock mapping and any related assertions to “GitHub”.
| className={`group playground-sidebar-nav-item ${active ? 'playground-sidebar-nav-item-active' : 'playground-sidebar-nav-item-inactive' | ||
| } tour-${title.toLowerCase().replace(' ', '-')}`} | ||
| > |
There was a problem hiding this comment.
The sidebar builds the tour-* CSS class from the translated title. When the UI is not English, this produces different (and potentially invalid) class names, so Shepherd steps in src/components/Tour.ts (which attach to .tour-editor, .tour-preview, etc.) will no longer find their elements. Use a stable, non-translated identifier for the tour class (e.g., a separate tourKey/id field on nav items) and keep translations only for the visible label/aria-label.
src/components/Navbar.tsx
Outdated
| const currentLang = LANGUAGES.find(l => l.code === i18n.language) || LANGUAGES[0]; | ||
|
|
There was a problem hiding this comment.
currentLang is derived by matching i18n.language exactly. With language detection this is often a regional tag (e.g., en-US), which won’t match any of the LANGUAGES codes and will always fall back to English even when another supported language is resolved. Normalize the language (e.g., use i18n.resolvedLanguage or i18n.language.split('-')[0]) or adjust i18next config to load languageOnly.
| key={lang.code} | ||
| onClick={() => changeLanguage(lang.code)} | ||
| className={i18n.language === lang.code ? 'bg-gray-100 dark:bg-gray-700 font-semibold' : ''} | ||
| > |
There was a problem hiding this comment.
The selected-language styling compares i18n.language === lang.code, which won’t highlight any option when i18n.language is a regional tag like en-US. Compare against a normalized/resolved language (or update i18n config to avoid explicit region codes) so the current selection is correctly indicated.
src/components/Navbar.tsx
Outdated
| <Button className="bg-transparent border-none text-white h-16 flex items-center cursor-pointer"> | ||
| <MdLanguage className={`text-xl text-white ${screens.md ? "mr-1.5" : "mr-0" | ||
| }`} /> | ||
| <span className={screens.md ? "inline" : "hidden"}>{currentLang.flag} {currentLang.label}</span> | ||
| </Button> |
There was a problem hiding this comment.
On small screens the language switcher button hides its text label (hidden) and the icon doesn’t provide an accessible name, so the button can end up unnamed for screen readers. Add an aria-label (e.g., using the existing navbar.language translation) and/or a visually-hidden label so it remains accessible in icon-only mode.
c9c20d1 to
b986f31
Compare
Signed-off-by: kartik <kartikrautan0@gmail.com>
Signed-off-by: kartik <kartikrautan0@gmail.com>
Signed-off-by: kartik <kartikrautan0@gmail.com>
b986f31 to
3ce273b
Compare
Closes #666
This pull request introduces full internationalization (i18n) support to the Template Playground, making the application accessible to non-English speakers. Initial translations include English, French, and Spanish. A new dynamic language switcher has been added to the Navbar to allow users to instantly toggle languages.
Changes
react-i18next,i18next, andi18next-browser-languagedetectordependencies to power translation routing.i18n.tsconfiguration file handling language detection andlocalStoragepersistence with English as the fallback.Navbar,MainContainer,PlaygroundSidebar,ProblemPanel,Footer,SettingsModal,FullScreenModal) and replaced them with theuseTranslation(t()) hook.translation.jsonresources foren,fr, andesunder the newsrc/locales/directory.Navbarto seamlessly select between supported languages.src/tests/**/*.test.tsx) to globally mockreact-i18next, ensuring UI string matching and DOM snapshot tests continue to pass correctly without failing due to dynamic translation rendering.Flags
vi.mock('react-i18next')so that components render static fallback keys during the continuous integration build rather than attempting to initialize the full translation context.src/locales/<lang>/translation.jsonfile and importing it intosrc/i18n.ts.Screenshots or Video
2026-03-03.20-16-29.mp4
Related Issues
Author Checklist
--signoffoption of git commit.mainfromfork:branchname