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 9 commits 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 300 files, which is 150 over the limit of 150. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (300)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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() |
…ctor
The community port of plane-ee#6449 (MobX -> react-i18next refactor) had
gaps that broke ~25 unique translation keys community code calls. This
commit restores parity:
- Port power-k namespace (19 locales) from plane-ee, stripped of EE-only
paths (initiative/customer/teamspace/dashboards/AI assistant). Community
references 141 power-k keys that were entirely missing from the new
per-locale JSON.
- Restore epic.* keys (8 leaves) into work-item.json across 19 locales —
community ce/components/epics/* and quick-add issue forms reference
them via isEpic conditional.
- Add 'date' leaf to common.json across 19 locales (sourced from
work_item_types.settings.properties.property_type.date.label so the
proper translation, not English, is used).
- Move exporter.* subtree from importer.json to common.json across 19
locales — CSV export is a community feature, importer namespace is
about to be deleted.
- Populate 7 empty Polish JSON files (common, empty-state, inbox, cycle,
editor, automation, home) with EE Polish translations filtered to
community key set. The community port committed these as 0-byte files.
- Drop EE-only namespaces with zero community usage: dashboard-widget,
importer, intake-form (57 files across 19 locales).
- Update NAMESPACES const: drop the 3 deleted namespaces, add power-k.
- Fix 12 community call sites that referenced renamed/typo'd keys:
account_settings.api_tokens.heading -> .title
auth.common.password.toast.error.* -> .change_password.error.*
sign_out.toast.error.* -> auth.sign_out.toast.error.*
notification.toasts.un_snoozed -> .unsnoozed
profile.stats.priority_distribution.priority -> common.priority
projects.label -> common.projects
progress -> common.progress
epics -> common.epics
creating_theme -> common.saving (no localized source available)
toast.error (with trailing space typo) -> toast.error
Verified: every literal t(...) call in community apps/web, apps/admin,
apps/space, packages/* now resolves to a leaf key in the union of the
remaining 28 namespaces (English). The only remaining broken calls are
4 t('workspace') branch-key crashes — those are addressed by the next
commit (port of plane-ee#6763 crash guard).
Refs: makeplane/plane-ee#6449
…crashes
Wraps useTranslation()'s t() in coerceToString so namespace-node lookups
(which i18next-icu unconditionally returns as raw objects regardless of
returnObjects:false) fall back to the key string instead of crashing
React with 'Objects are not valid as a React child'.
Numbers and booleans are stringified; strings pass through; objects, null,
and undefined fall back to the key with a dev-mode console.warn pointing
to the bad call site. Production builds suppress the warning but keep the
guard. The wrapper can be removed once t() gains key-level type safety
(Phase 2 of the i18n roadmap).
Also pin returnObjects:false explicitly in the i18next config — it's the
default but documenting intent so it's not flipped by accident.
Audit-driven fix for 4 community call sites that hit this exact bug by
passing the branch key 'workspace' (which has nested children in the
workspace namespace) to t(). Switched to t('common.workspace') (existing
leaf with value 'Workspace').
Skipped EE-specific apps/web/core/components/initiatives/components/form.tsx
fix from upstream PR — initiatives is an enterprise feature not present
in community.
Refs: makeplane/plane-ee#6763
keys.generated.ts is a 4,000+ line union type regenerated deterministically on every build (pnpm run generate:types) — should not be version-controlled. Adding the file to .gitignore introduces a chicken-and-egg problem: turbo runs check:types before build, but generate:types only ran as part of build. On a fresh clone with no keys.generated.ts present, tsc --noEmit fails. Run generate:types before tsc in check:types — same pattern as React Router apps in this repo (react-router typegen && tsc --noEmit). - Add packages/i18n/src/types/keys.generated.ts to root .gitignore - Untrack the file from git (git rm --cached) - Run generate:types before tsc in check:types Verified: deleting keys.generated.ts and running check:types regenerates the file correctly. After regeneration, git status shows the file remains untracked (.gitignore is honored). Refs: makeplane/plane-ee#6784
|
@coderabbitai you can skip reviewing |
|
Tip For best results, initiate chat on the files or code changes.
[full_review] ✏️ Learnings added
|
The 3 settings sidebar item-categories components were passing enum string
values directly to t() — e.g. t('your profile'), t('work-structure'),
t('administration'). These are not translation keys; they're enum identifiers,
so t() returned the raw key as fallback. Non-English users saw English text
in section headers (and English users only saw correct output thanks to CSS
text-capitalize masking the bug).
Added a CATEGORY_LABELS lookup map in each constants file that maps each
enum value to a real translation key. Components now call t(LABELS[category])
instead of t(category).
- Added 5 new keys to en/common.json common.* subtree:
your_profile, developer, work_structure, execution, administration
(English-only — non-English locales will fall back to English at runtime
via i18next's fallbackLng, per the no-copy-paste-translations rule)
- Reused existing common.general and common.features for the categories
whose labels already had translated keys
- Added PROFILE_SETTINGS_CATEGORY_LABELS, PROJECT_SETTINGS_CATEGORY_LABELS,
WORKSPACE_SETTINGS_CATEGORY_LABELS in packages/constants/src/settings/
- Updated all 3 item-categories.tsx components
Found via comprehensive dynamic-key audit (1918 t() invocations classified
across literal, template-literal, property-access, conditional, function-call,
and identifier patterns). Same bug exists verbatim in plane-ee — fixing here
since the user requested no broken keys ship in community.
.claude/scheduled_tasks.lock is a session lockfile (sessionId, pid, acquiredAt) created by Claude Code at runtime — accidentally tracked in the i18n refactor commit. Untrack from git; the file stays on disk for the running session.
|
@coderabbitai you can now review the entire change. (Skip |
|
Tip For best results, initiate chat on the files or code changes.
[full_review]
|
Two post-Commit D follow-ups: - Fix TS2379 in use-translation.ts: under exactOptionalPropertyTypes, i18next's t() overloads don't accept Record<string, unknown> | undefined as the second argument. Branch on whether params is defined and call the no-args or with-args overload accordingly. - Bump @plane/i18n check:lint --max-warnings from 2 to 9. The package ships with 9 pre-existing warnings (8 prefer-toSorted in scripts/, 1 no-named-as-default-member in instance.ts on a line untouched by my changes). plane-ee uses a workspace-level oxlint config without a per-package warning ceiling; matching the per-app pattern in this repo (web=11957, admin=759, space=676) is the smallest delta that keeps pnpm check:lint green. Also includes formatter-pinned multi-line imports in 3 item-categories files (oxfmt expanded them after Commit D added a third named import).
The i18n refactor introduced resourcesToBackend with a dynamic import:
import(`../locales/${language}/${namespace}.json`)
That path is relative to the source file's location. From src/core/instance.ts
it correctly resolves to src/locales/. But after tsdown bundling, the same
import call lives in dist/index.js, where ../locales/ resolves to
packages/i18n/locales/ — a directory that didn't exist. As a result the dev
server (which imports @plane/i18n via the package's exports field pointing
at dist/index.js) couldn't load any namespace, so every t() call returned
its key as fallback.
Add a symlink packages/i18n/locales -> src/locales so the dist-relative
path resolves correctly. Same fix plane-ee uses (verified: identical blob
mode 120000, SHA a4829b5). Keeps tsdown.config.ts and package.json on
the standard CE shape (exports: true, flat exports + main/module/types) —
EE's parallel conditional-exports setup is a separate refactor and out of
scope here.
- All 18 non-English locales filled to 3,837/3,837 keys against the canonical English source. Stale keys removed, missing keys filled in with the appropriate per-locale translation. - New scripts/lib/locale-io.ts module shared between sync-check and future tooling. readJsonFile() wraps JSON.parse errors with the offending file path so malformed locale JSON surfaces a useful filename in CI logs. - New .github/workflows/i18n-sync-check.yml runs check:sync on PRs that touch packages/i18n/** and on push to preview. Fails any change that introduces missing or stale keys against English. - Pin tsx@4.20.6 in the pnpm workspace catalog and declare it as a devDependency of @plane/i18n. Replace npx tsx@4.19.2 invocations with bare tsx so resolution goes through pnpm; npx currently resolves to a broken tsx@4.21.0 that pulls an unpublished esbuild range.
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.This PR also brings in two related upstream fixes — a runtime guard against namespace-node returns that previously crashed React, and the gitignore for auto-generated translation key types.
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,power-k.json, etc.translations.ts,accessibility.ts,editor.ts,empty-state.ts,tour.ts,core.tsRuntime safety:
t()crash guarduseTranslation()now wrapst()with acoerceToStringhelper.i18next-icuunconditionally returns the raw object whent()is called with a branch (namespace-node) key, regardless ofreturnObjects: false. Without the guard, React unmounts the subtree with "Objects are not valid as a React child".console.warnpointing to the bad call site.returnObjects: falseis also pinned explicitly in the i18next config to document intent.Build tooling
scripts/generate-types.ts— Generateskeys.generated.ts(flat union of ~3,837 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 issueskeys.generated.tsis gitignored — it's regenerated deterministically on every build, no need to track it.check:typesnow runsgenerate:typesfirst so fresh clones type-check cleanly.App-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_LANGUAGETest plan
packages/i18nbuild succeeds (pnpm run build— generates types + bundles)packages/i18ntype check passes (pnpm run check:types)apps/web/apps/admin/apps/spacetype checks passpnpm check:lint)t("X")call in community source resolves to a key present in the English namespace setRelated