Skip to content

feat: Implement Internationalization (i18n) Support #771

Open
kartikrautan wants to merge 3 commits intoaccordproject:mainfrom
kartikrautan:differentLanguageSupport
Open

feat: Implement Internationalization (i18n) Support #771
kartikrautan wants to merge 3 commits intoaccordproject:mainfrom
kartikrautan:differentLanguageSupport

Conversation

@kartikrautan
Copy link
Contributor

@kartikrautan kartikrautan commented Mar 3, 2026

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

  • Added react-i18next, i18next, and i18next-browser-languagedetector dependencies to power translation routing.
  • Created an i18n.ts configuration file handling language detection and localStorage persistence with English as the fallback.
  • Extracted hardcoded English UI strings from all major components (Navbar, MainContainer, PlaygroundSidebar, ProblemPanel, Footer, SettingsModal, FullScreenModal) and replaced them with the useTranslation (t()) hook.
  • Created comprehensive translation.json resources for en, fr, and es under the new src/locales/ directory.
  • Implemented a Language Switcher dropdown menu featuring a globe icon inside the Navbar to seamlessly select between supported languages.
  • Refactored Vitest test suites (src/tests/**/*.test.tsx) to globally mock react-i18next, ensuring UI string matching and DOM snapshot tests continue to pass correctly without failing due to dynamic translation rendering.

Flags

  • Test Mocks: The unit tests have been intentionally adjusted to 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.
  • Adding New Languages: Future language packs can be easily added by creating a new src/locales/<lang>/translation.json file and importing it into src/i18n.ts.

Screenshots or Video

2026-03-03.20-16-29.mp4

Related Issues

Author Checklist

  • Ensure you provide a DCO sign-off for your commits using the --signoff option of git commit.
  • Vital features and changes captured in unit and/or integration tests
  • Commits messages follow AP format
  • Extend the documentation, if necessary
  • Merging to main from fork:branchname

@kartikrautan kartikrautan requested a review from a team as a code owner March 3, 2026 14:41
@netlify
Copy link

netlify bot commented Mar 3, 2026

Deploy Preview for ap-template-playground ready!

Name Link
🔨 Latest commit 3ce273b
🔍 Latest deploy log https://app.netlify.com/projects/ap-template-playground/deploys/69ac1b214942f80008536eab
😎 Deploy Preview https://deploy-preview-771--ap-template-playground.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@kartikrautan kartikrautan changed the title feat: Implement Internationalization (i18n) Support #666 feat: Implement Internationalization (i18n) Support Mar 3, 2026
@kartikrautan
Copy link
Contributor Author

Hello @mttrbrts can you review this PR, making the application accessible to non-English speakers? thanks

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 added en/fr/es translation resources.
  • Updated component tests/snapshots to accommodate translated strings via react-i18next mocks.

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 (letconst) 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.

Comment on lines +370 to +386
{/* 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>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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).

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +12 to +26
.init({
resources: {
en: { translation: enTranslation },
fr: { translation: frTranslation },
es: { translation: esTranslation },
},
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +23
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() }
})
}));
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +193
onClick={onClick}
className={`group playground-sidebar-nav-bottom-item tour-${title.toLowerCase().replace(' ', '-')}`}
>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 76 to 82
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'
});
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +13
"navbar.templatePlayground": "Template Playground",
"navbar.github": "Github", // the original test asserted for Github with a capital G
};
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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”.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +170
className={`group playground-sidebar-nav-item ${active ? 'playground-sidebar-nav-item-active' : 'playground-sidebar-nav-item-inactive'
} tour-${title.toLowerCase().replace(' ', '-')}`}
>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 213 to 214
const currentLang = LANGUAGES.find(l => l.code === i18n.language) || LANGUAGES[0];

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +232
key={lang.code}
onClick={() => changeLanguage(lang.code)}
className={i18n.language === lang.code ? 'bg-gray-100 dark:bg-gray-700 font-semibold' : ''}
>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +381 to +385
<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>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@kartikrautan kartikrautan force-pushed the differentLanguageSupport branch 4 times, most recently from c9c20d1 to b986f31 Compare March 7, 2026 12:25
Signed-off-by: kartik <kartikrautan0@gmail.com>
Signed-off-by: kartik <kartikrautan0@gmail.com>
Signed-off-by: kartik <kartikrautan0@gmail.com>
@kartikrautan kartikrautan force-pushed the differentLanguageSupport branch from b986f31 to 3ce273b Compare March 7, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Implement Internationalization (i18n) Support

2 participants