refactor(i18n): migrate packages/i18n from MobX to react-i18next#8898
refactor(i18n): migrate packages/i18n from MobX to react-i18next#8898sriramveeraghanta wants to merge 1 commit intopreviewfrom
Conversation
… per-feature namespaces Replaces the internals of packages/i18n with react-i18next while preserving the identical public API. Consumer code using useTranslation() and TranslationProvider requires no changes. Translation file format: TS objects to JSON namespaces - Converted TypeScript translation files (19 languages) into feature-based JSON namespace files - Split the monolithic translations.ts into per-feature namespace files: workspace.json, project.json, work-item.json, cycle.json, inbox.json, etc. - 30 community namespaces across 19 languages = 570 JSON files Core runtime: MobX to i18next - Replaced MobX TranslationStore with an i18next instance using i18next-icu (preserves ICU MessageFormat) and i18next-resources-to-backend (namespace lazy loading) - useTranslation() and TranslationProvider keep identical signatures - All namespaces pre-loaded during init for the current language to prevent re-render cascades - Reads saved language from localStorage before init for faster first paint Build tooling - scripts/generate-types.ts: Reads English JSON files and outputs keys.generated.ts with a flat union of translation keys (runs before every build) - scripts/sync-check.ts: Cross-locale missing/stale key detection, cross-namespace collision detection, path conflict detection (supports --ci mode) App-level changes - Removed useTranslation-based language sync effect from store-wrapper - Language is now synced imperatively from profile.store (fetchUserProfile, updateUserProfile) and root.store (resetOnSignOut) via setLanguage() Community scope - Enterprise-only namespaces (customer, epic, initiative, pql, power-k, teamspace, release) excluded - Enterprise-only keys pruned from shared namespaces (empty-state, navigation, project-settings, workspace-settings, work-item, importer, page, work-item-type)
|
Important Review skippedToo many files! This PR contains 299 files, which is 149 over the limit of 150. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (299)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Migrates packages/i18n from a MobX-backed translation store to a react-i18next/i18next runtime with ICU formatting, while moving translations from TS modules to per-namespace JSON files and adding locale sync tooling.
Changes:
- Replaced MobX
TranslationStoreusage with ani18nextinstance +TranslationProviderwrapper and updateduseTranslation(). - Split translations into per-feature namespace JSON files across locales and removed legacy TS locale modules.
- Added scripts to generate translation key types and verify locale sync/collisions in CI.
Reviewed changes
Copilot reviewed 82 out of 673 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/i18n/src/locales/de/workflow.json | Adds German workflow namespace JSON translations |
| packages/i18n/src/locales/de/wiki.json | Adds German wiki namespace JSON translations |
| packages/i18n/src/locales/de/update.json | Adds German update namespace JSON translations |
| packages/i18n/src/locales/de/tour.json | Adds German tour namespace JSON translations |
| packages/i18n/src/locales/de/template.json | Adds German template namespace JSON translations |
| packages/i18n/src/locales/de/stickies.json | Adds German stickies namespace JSON translations |
| packages/i18n/src/locales/de/settings.json | Adds German settings namespace JSON translations |
| packages/i18n/src/locales/de/page.json | Adds German page namespace JSON translations |
| packages/i18n/src/locales/de/notification.json | Adds German notification namespace JSON translations |
| packages/i18n/src/locales/de/navigation.json | Adds German navigation namespace JSON translations |
| packages/i18n/src/locales/de/module.json | Adds German module namespace JSON translations |
| packages/i18n/src/locales/de/intake-form.json | Adds German intake-form namespace JSON translations |
| packages/i18n/src/locales/de/inbox.json | Adds German inbox namespace JSON translations |
| packages/i18n/src/locales/de/home.json | Adds German home namespace JSON translations |
| packages/i18n/src/locales/de/empty-state.ts | Removes legacy German TS translation module |
| packages/i18n/src/locales/de/empty-state.json | Adds German empty-state namespace JSON translations |
| packages/i18n/src/locales/de/editor.json | Adds German editor namespace JSON translations |
| packages/i18n/src/locales/de/dashboard-widget.json | Adds German dashboard-widget namespace JSON translations |
| packages/i18n/src/locales/de/cycle.json | Adds German cycle namespace JSON translations |
| packages/i18n/src/locales/de/automation.json | Adds German automation namespace JSON translations |
| packages/i18n/src/locales/de/auth.json | Adds German auth namespace JSON translations |
| packages/i18n/src/locales/de/accessibility.ts | Removes legacy German accessibility TS module |
| packages/i18n/src/locales/de/accessibility.json | Adds German accessibility namespace JSON translations |
| packages/i18n/src/locales/cs/workflow.json | Adds Czech workflow namespace JSON translations |
| packages/i18n/src/locales/cs/wiki.json | Adds Czech wiki namespace JSON translations |
| packages/i18n/src/locales/cs/update.json | Adds Czech update namespace JSON translations |
| packages/i18n/src/locales/cs/tour.json | Adds Czech tour namespace JSON translations |
| packages/i18n/src/locales/cs/template.json | Adds Czech template namespace JSON translations |
| packages/i18n/src/locales/cs/stickies.json | Adds Czech stickies namespace JSON translations |
| packages/i18n/src/locales/cs/settings.json | Adds Czech settings namespace JSON translations |
| packages/i18n/src/locales/cs/project.json | Adds Czech project namespace JSON translations |
| packages/i18n/src/locales/cs/page.json | Adds Czech page namespace JSON translations |
| packages/i18n/src/locales/cs/notification.json | Adds Czech notification namespace JSON translations |
| packages/i18n/src/locales/cs/navigation.json | Adds Czech navigation namespace JSON translations |
| packages/i18n/src/locales/cs/module.json | Adds Czech module namespace JSON translations |
| packages/i18n/src/locales/cs/intake-form.json | Adds Czech intake-form namespace JSON translations |
| packages/i18n/src/locales/cs/inbox.json | Adds Czech inbox namespace JSON translations |
| packages/i18n/src/locales/cs/home.json | Adds Czech home namespace JSON translations |
| packages/i18n/src/locales/cs/empty-state.ts | Removes legacy Czech TS translation module |
| packages/i18n/src/locales/cs/empty-state.json | Adds Czech empty-state namespace JSON translations |
| packages/i18n/src/locales/cs/editor.ts | Removes legacy Czech editor TS translation module |
| packages/i18n/src/locales/cs/editor.json | Adds Czech editor namespace JSON translations |
| packages/i18n/src/locales/cs/dashboard-widget.json | Adds Czech dashboard-widget namespace JSON translations |
| packages/i18n/src/locales/cs/cycle.json | Adds Czech cycle namespace JSON translations |
| packages/i18n/src/locales/cs/automation.json | Adds Czech automation namespace JSON translations |
| packages/i18n/src/locales/cs/accessibility.ts | Removes legacy Czech accessibility TS module |
| packages/i18n/src/locales/cs/accessibility.json | Adds Czech accessibility namespace JSON translations |
| packages/i18n/src/index.ts | Refactors i18n package exports to new provider/hook/core APIs |
| packages/i18n/src/hooks/use-translation.ts | Reimplements useTranslation() using react-i18next |
| packages/i18n/src/core/set-language.ts | Adds imperative setLanguage() utility |
| packages/i18n/src/core/instance.ts | Adds and initializes i18next instance with lazy JSON namespace loading |
| packages/i18n/src/core/index.ts | Exposes core i18n instance/init and setLanguage() |
| packages/i18n/src/context/index.tsx | Removes MobX TranslationContext/Provider implementation |
| packages/i18n/src/constants/namespaces.ts | Adds namespace list/constants for i18next configuration |
| packages/i18n/src/constants/language.ts | Removes legacy translation file enum; keeps language constants |
| packages/i18n/src/constants/index.ts | Re-exports namespaces alongside language constants |
| packages/i18n/scripts/tsconfig.json | Adds scripts TS config (Node16 ESM) |
| packages/i18n/scripts/sync-check.ts | Adds locale sync/collision/path-conflict validation script |
| packages/i18n/scripts/generate-types.ts | Adds translation key type generation from English JSON |
| packages/i18n/package.json | Updates deps/scripts for i18next migration and new tooling |
| apps/web/core/store/user/profile.store.ts | Syncs app language via setLanguage() after profile fetch/update |
| apps/web/core/store/root.store.ts | Resets language via setLanguage(FALLBACK_LANGUAGE) on sign-out |
| apps/web/core/lib/wrappers/store-wrapper.tsx | Removes language-sync effect that used useTranslation() |
| i18nInstance | ||
| .use(ICU) | ||
| .use(initReactI18next) | ||
| .use(resourcesToBackend((language: string, namespace: string) => import(`../locales/${language}/${namespace}.json`))); |
There was a problem hiding this comment.
The backend import path uses the full i18next language code (e.g. de-DE) as the locale directory name. In this PR, locale directories shown are src/locales/de/... and src/locales/cs/..., which will cause runtime load failures (at least an initial failed import for de-DE) and can break translations if i18next doesn’t successfully fall back to de. Consider normalizing language to a folder key (e.g. language.split("-")[0]) inside the backend resolver, or ensure the on-disk locale folders match the SUPPORTED_LANGUAGES values (and configure i18next load: "languageOnly" if that’s the intended behavior).
| i18nInstance | |
| .use(ICU) | |
| .use(initReactI18next) | |
| .use(resourcesToBackend((language: string, namespace: string) => import(`../locales/${language}/${namespace}.json`))); | |
| const getLocaleFolderKey = (language: string): string => language.split("-")[0]; | |
| i18nInstance | |
| .use(ICU) | |
| .use(initReactI18next) | |
| .use( | |
| resourcesToBackend((language: string, namespace: string) => | |
| import(`../locales/${getLocaleFolderKey(language)}/${namespace}.json`), | |
| ), | |
| ); |
| const { t, i18n } = useI18nextTranslation(); | ||
|
|
||
| const changeLanguage = useCallback( | ||
| (lng: TLanguage) => { | ||
| void (async () => { | ||
| try { | ||
| await i18n.changeLanguage(lng); | ||
| if (typeof window === "undefined") return; | ||
| localStorage.setItem(LANGUAGE_STORAGE_KEY, lng); | ||
| document.documentElement.lang = lng; | ||
| } catch (err) { | ||
| console.error("Failed to change language:", err); | ||
| } | ||
| })(); | ||
| }, | ||
| [i18n] | ||
| ); |
There was a problem hiding this comment.
changeLanguage() duplicates the side effects in core/set-language.ts (localStorage + <html lang>), which makes it easy for the two paths to drift (e.g., preloading namespaces after language change, waiting for initPromise, error handling). To keep behavior consistent, consider delegating changeLanguage() to the exported setLanguage() utility (or at minimum share a single internal helper).
| * @returns {ILanguageOption[]} languages - available languages | ||
| * @throws {Error} if the TranslationProvider is not used | ||
| */ | ||
| export function useTranslation(): TTranslationStore { |
There was a problem hiding this comment.
Previously this hook threw an explicit error when used outside TranslationProvider. The new implementation will no longer throw (and may silently use a different/default i18n instance if the provider isn’t mounted), which is a behavioral change for API consumers and can make misconfiguration harder to detect. If “must be used within TranslationProvider” is still intended, consider restoring an explicit invariant (or ensure the hook always binds to the package’s i18nInstance so it behaves deterministically).
| // No namespace arg — fallbackNS in the i18next config ensures all namespaces | ||
| // are searched for any key. Passing NAMESPACES here would trigger concurrent | ||
| // async loads per component, causing a re-render cascade. | ||
| const { t, i18n } = useI18nextTranslation(); |
There was a problem hiding this comment.
Previously this hook threw an explicit error when used outside TranslationProvider. The new implementation will no longer throw (and may silently use a different/default i18n instance if the provider isn’t mounted), which is a behavioral change for API consumers and can make misconfiguration harder to detect. If “must be used within TranslationProvider” is still intended, consider restoring an explicit invariant (or ensure the hook always binds to the package’s i18nInstance so it behaves deterministically).
| export { TranslationProvider } from "./provider"; | ||
|
|
||
| // Hooks | ||
| export { useTranslation } from "./hooks/use-translation"; |
There was a problem hiding this comment.
PR description claims “Public API is unchanged”, but src/index.ts no longer re-exports ./context, ./store, and ./locales (it previously exported export * from "./..."). If any downstream code imports those symbols from @plane/i18n, this becomes a breaking change even if useTranslation() / TranslationProvider signatures are stable. Consider either restoring the previous re-exports or clarifying in the PR description what is considered public API.
| export { setLanguage } from "./core/set-language"; | ||
|
|
||
| // Constants | ||
| export { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "./constants/language"; |
There was a problem hiding this comment.
PR description claims “Public API is unchanged”, but src/index.ts no longer re-exports ./context, ./store, and ./locales (it previously exported export * from "./..."). If any downstream code imports those symbols from @plane/i18n, this becomes a breaking change even if useTranslation() / TranslationProvider signatures are stable. Consider either restoring the previous re-exports or clarifying in the PR description what is considered public API.
| "chart_models": { | ||
| "basic": "Základní", | ||
| "stacked": "Skládaný", | ||
| "grouped": "Skupinový" | ||
| }, |
There was a problem hiding this comment.
This Czech dashboard-widget JSON changes the shape of chart_models.* compared to other locales (e.g., it’s a string here but appears to be an object with short_label/long_label in German). If the UI expects nested keys like ...chart_models.basic.short_label, those lookups will fail and fall back to English/missing keys. The locale files should keep the same key structure across languages; update this locale to match the expected schema.
| "wrong_name": "Der Notizname darf nicht länger als 100 Zeichen sein.", | ||
| "already_exists": "Es existiert bereits eine Notiz ohne Beschreibung" | ||
| }, | ||
| "created": { | ||
| "title": "Notiz erstellt", | ||
| "message": "Die Notiz wurde erfolgreich erstellt" | ||
| }, | ||
| "not_created": { | ||
| "title": "Notiz nicht erstellt", | ||
| "message": "Die Notiz konnte nicht erstellt werden" | ||
| }, | ||
| "updated": { | ||
| "title": "Notiz aktualisiert", | ||
| "message": "Die Notiz wurde erfolgreich aktualisiert" | ||
| }, | ||
| "not_updated": { | ||
| "title": "Notiz nicht aktualisiert", | ||
| "message": "Die Notiz konnte nicht aktualisiert werden" | ||
| }, | ||
| "removed": { | ||
| "title": "Notiz entfernt", | ||
| "message": "Die Notiz wurde erfolgreich entfernt" | ||
| }, | ||
| "not_removed": { | ||
| "title": "Notiz nicht entfernt", | ||
| "message": "Die Notiz konnte nicht entfernt werden" |
There was a problem hiding this comment.
In the page_navigation_pane.toasts section, multiple messages reference “Notiz” (note) and appear to be copied from stickies/notes, not page navigation. This will show incorrect user-facing error/success messaging in the page UI. Adjust these strings (and possibly the keys) to match the page/navigation context.
| "wrong_name": "Der Notizname darf nicht länger als 100 Zeichen sein.", | |
| "already_exists": "Es existiert bereits eine Notiz ohne Beschreibung" | |
| }, | |
| "created": { | |
| "title": "Notiz erstellt", | |
| "message": "Die Notiz wurde erfolgreich erstellt" | |
| }, | |
| "not_created": { | |
| "title": "Notiz nicht erstellt", | |
| "message": "Die Notiz konnte nicht erstellt werden" | |
| }, | |
| "updated": { | |
| "title": "Notiz aktualisiert", | |
| "message": "Die Notiz wurde erfolgreich aktualisiert" | |
| }, | |
| "not_updated": { | |
| "title": "Notiz nicht aktualisiert", | |
| "message": "Die Notiz konnte nicht aktualisiert werden" | |
| }, | |
| "removed": { | |
| "title": "Notiz entfernt", | |
| "message": "Die Notiz wurde erfolgreich entfernt" | |
| }, | |
| "not_removed": { | |
| "title": "Notiz nicht entfernt", | |
| "message": "Die Notiz konnte nicht entfernt werden" | |
| "wrong_name": "Der Name des Navigationspunkts darf nicht länger als 100 Zeichen sein.", | |
| "already_exists": "Es existiert bereits ein Navigationspunkt ohne Beschreibung" | |
| }, | |
| "created": { | |
| "title": "Navigationspunkt erstellt", | |
| "message": "Der Navigationspunkt wurde erfolgreich erstellt" | |
| }, | |
| "not_created": { | |
| "title": "Navigationspunkt nicht erstellt", | |
| "message": "Der Navigationspunkt konnte nicht erstellt werden" | |
| }, | |
| "updated": { | |
| "title": "Navigationspunkt aktualisiert", | |
| "message": "Der Navigationspunkt wurde erfolgreich aktualisiert" | |
| }, | |
| "not_updated": { | |
| "title": "Navigationspunkt nicht aktualisiert", | |
| "message": "Der Navigationspunkt konnte nicht aktualisiert werden" | |
| }, | |
| "removed": { | |
| "title": "Navigationspunkt entfernt", | |
| "message": "Der Navigationspunkt wurde erfolgreich entfernt" | |
| }, | |
| "not_removed": { | |
| "title": "Navigationspunkt nicht entfernt", | |
| "message": "Der Navigationspunkt konnte nicht entfernt werden" |
| "build": "pnpm run generate:types && tsdown", | ||
| "generate:types": "npx tsx@4.19.2 scripts/generate-types.ts", | ||
| "sync:check": "npx tsx@4.19.2 scripts/sync-check.ts", | ||
| "check:sync": "npx tsx@4.19.2 scripts/sync-check.ts --ci", |
There was a problem hiding this comment.
Running tsx via npx in package scripts can add network dependency and variability/latency in CI and local builds (especially in constrained environments). Consider adding tsx as a devDependency (pinned to the same version) and invoking it directly (e.g., tsx scripts/...) for more reliable and faster execution.
Summary
Migrates
packages/i18nfrom the MobX-basedTranslationStoretoreact-i18next, splits the monolithictranslations.tsfiles into per-feature JSON namespace files, and adds build tooling for cross-locale sync validation. Public API is unchanged — consumers ofuseTranslation()andTranslationProviderrequire no updates.Changes
Core runtime: MobX → i18next
TranslationStorewith ani18nextinstance usingi18next-icu(preserves ICU MessageFormat) andi18next-resources-to-backend(lazy namespace loading)packages/i18n/src/core/— i18next instance + imperativesetLanguage()packages/i18n/src/provider/—TranslationProvider(thin wrapper aroundI18nextProvider)useTranslation()andTranslationProviderkeep identical signatureslocalStoragesynchronously beforei18nInstance.init()so non-English users load their language directlyTranslation files: TS → JSON namespaces
translations.tsinto feature-based files:workspace.json,project.json,work-item.json,cycle.json,inbox.json,page.json,wiki.json, etc.translations.ts,accessibility.ts,editor.ts,empty-state.ts,tour.ts,core.ts(77 files deleted)Build tooling
scripts/generate-types.ts— Generateskeys.generated.ts(flat union of ~4,098 keys) from English JSON files; runs before every build. Also detects cross-namespace key collisions and path conflicts.scripts/sync-check.ts— Cross-locale missing/stale key detection with--cimode that exits non-zero on issuesApp-level changes
useTranslation-based language sync effect fromapps/web/core/lib/wrappers/store-wrapper.tsxsetLanguage():profile.store.fetchUserProfile()→ callssetLanguage(profile.language)after loadprofile.store.updateUserProfile()→ callssetLanguage()optimisticallyroot.store.resetOnSignOut()→ resets toFALLBACK_LANGUAGECommunity scope
customer,epic,initiative,pql,power-k,teamspace,releaseempty-state,navigation,project-settings,workspace-settings,work-item,importer,page,work-item-type(initiatives / teamspaces / customers / releases / release_picker references removed)Test plan
packages/i18nbuild succeeds (pnpm run build— generates types + bundles)packages/i18ntype check passes (pnpm run check:types)apps/webtype check passes (pnpm run check:types)apps/spacetype check passes (pnpm run check:types)Related