Shell - Product Localization - #4194
Conversation
Install react-i18next and supporting plugins, configure i18next with chained localStorage/HTTP backend, lazy loading, and git-hash-based cache busting. Add locale file structure for all 6 supported languages and i18next-parser config for key extraction.
…e (Phase 2) Connect LocaleSwitcher to i18n.changeLanguage(), add updateUser RTK Query mutation to persist locale in user.prefs, and apply DB locale preference on boot via load-instance before first render.
…ration (Phase 3 partial) - Replace hardcoded common strings (Save, Cancel, Edit, etc.) with t() calls across ~73 files - Fix i18next-parser config defaultValue to use function form so locale files get actual English strings - Populate public/locales/*/common.json with correct English values as translation placeholders - Upgrade TypeScript 4.9 → 5.9 to support i18next v26 type definitions (uses const type params) - Add skipLibCheck to tsconfig to suppress remaining third-party type incompatibilities - Add "shell" to i18n ns array in preparation for Phase 3 shell namespace
…ate plan
- Strip temporary { defaultValue: "..." } from all common namespace t() calls
- Update LOCALIZATION_PLAN.md: mark common namespace complete, check off Phase 5 items already done, reflect shell.json placeholder files added
…space - Translate all 9 Tier 1 shell components (ResizeableContainer, InvalidUrl, UserFilter, GlobalDirtyCodeModal, ConfirmDeleteModal, DropdownMenu, Comment/index, Favicon, GlobalDomainsMenu) - Move comment, reply, prod keys from shell to common namespace - Fix module-level CHIP_TITLE constant in GlobalDomainsMenu — replaced with inline t() ternary since module scope can't call t() - Add namespace prefix to existing t() calls across shell and sub-app files - Update locale files for all 6 languages (common + shell namespaces) - Update implementation plan with conventions for functions, object maps, prop-passed strings, and cross-namespace deduplication rules
…ate plan Locale files are confirmed correct — temporary defaultValue scaffolding removed from all 9 Tier 1 shell components. Updated plan to reflect completed status.
…mit link
- Replace slug-to-name conversion in global-menu with a productLabels map
backed by t() — all 13 product names now translated across 6 locales
- Add navAppTooltip key with {{name}} interpolation for the product tooltips
- Translate GlobalSidebar "View source code commit" link title
- data-cy values preserved via separate dataCyName variable so tests are unaffected
- Update plan to reflect completed items
Resolve the UI locale authoritatively from the logged-in user and fall back to the default. Previously a user with no saved locale (or a different one) kept the prior session's language because localStorage and i18n still held it after logout and there was no fallback.
Localize AccessDenied, NoInstancePermission, ConfirmPublishModal, global-tabs Dropdown (pluralized results), InstancesList (decoupled header text from icon logic), GlobalDocsMenu (module-level doc arrays + width bump to 520), InviteMembersModal, DateFilter, and RoleAccessInfo (role names left untranslated). Adds and translates all new common/shell keys across the 6 locales and marks Tier 2 done in the plan.
Code Review — ✅ No blockers · 🟡 4 warning(s) — see inline comments |
- resolveDevLng() now runs the stored app_locale through toSupportedLocale() before use, matching the navigator.language branch -- a stale/unsupported cached value no longer crashes dev on the first t() call. - Type the Autocomplete getOptionLabel callbacks against TONE_OPTIONS' element type instead of any (AIDrawer.tsx, AIGenerator.tsx x2). - updateUser's onQueryStarted forwards failed PUT errors to Sentry instead of silently swallowing them.
Localization Reviewer — ✅ No blockers · 🟡 3 advisory note(s)🔴 BlockingNone 🟡 Advisory
|
Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments |
Localization Reviewer — ✅ No blockers · 🟡 2 advisory note(s)🔴 BlockingNone 🟡 Advisory
|
Code Review — ✅ No blockers |
|
@finnar-bin please run negative QA on this |
# Conflicts: # package-lock.json
Localization Reviewer — ✅ No blockers · 🟡 2 advisory note(s)🔴 BlockingNone 🟡 Advisory
|
Localization Reviewer — ✅ No blockers · 🟡 5 advisory note(s)🔴 BlockingNone 🟡 Advisory
|
Code Review — ✅ No blockers · 🟡 3 warning(s) — see inline comments |
Negative QA🟡 Browser tab title shows the app name twice — once in English, once translated — for any non-English localeType: regression from this PR
Expected: Tab title shows the sub-app name once (e.g. Also checked and working correctly
|
Localization Reviewer — ✅ No blockers · 🟡 4 advisory note(s)🔴 BlockingNone 🟡 Advisory
|
Localization Reviewer — ✅ No blockers · 🟡 4 advisory note(s)🔴 BlockingNone 🟡 Advisory
|
| } | ||
|
|
||
| i18n.use(initReactI18next).init({ | ||
| lng: "en-US", |
There was a problem hiding this comment.
🟡 activePreview translations are dead code — locale always en-US
active-preview/i18n.js hardcodes lng: "en-US" with no LanguageDetector, so the non-English strings in every public/locales/*/activePreview.json are never loaded or displayed. The active-preview runs on the same origin as the shell, so localStorage.getItem("app_locale") is accessible — add LanguageDetector with the same detection config the shell uses, or at minimum seed lng from localStorage.getItem("app_locale") ?? "en-US".
Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments |
Resolves #4148
Summary
Rolls out full i18next + react-i18next localization across manager-ui — the app shell and every sub-app — supporting 6 locales:
en-US(fallback),es-ES,hi-IN,zh-CN,ru-RU,nl-NL.Dependencies
@zesty-io/materialfor accounts-ui to consume.prefs.localeDB field.Out of scope for this PR: the language switcher UI and the DB write-back of the selected locale both live in the two dependencies above, not in manager-ui. This repo only consumes
prefs.locale(src/shell/components/load-instance/index.js) and caches the resolved value tolocalStorageclient-side — it never writes locale back to the database itself.Architecture
src/shell/i18n/index.ts, loaded before the React root renders; root is wrapped in<Suspense>.public/locales/<locale>/<namespace>.json.t("content.publishItem")) — the namespace is the first dot-segment, everything after is one flat key. No nested JSON, no second dot.<Suspense>and callsuseTranslation("<ns>")once, but that trigger is now effectively a no-op since the namespace is already resolved by init time — kept intentionally. True lazy-loading was reverted: it caused crashes from a race condition where Redux-driven code (thunks/middleware callingi18n.t()outside of React) could fire before a sub-app's namespace had loaded, and the perf gain was minimal anyway.en-USand report once per key to Sentry.localizeTheme/LocalizedThemeProvider, not throught().formatLocalized/formatDistanceToNowLocalized; machine formats (yyyy-MM-dd, API/CSV payloads, URL params) are intentionally left locale-independent.Tooling added
Workflow({ name: "localize" })— an AI-driven pipeline (Discovery → Extract & Wire → Composer → Verifier) for localizing new copy going forward. Extracts hardcoded strings, wirest()/i18n.t()calls, writesen-US+ English-placeholder locale JSON, and verifies (tsc, JSON validity, key parity, broken-key refs). See README's "Localizing new copy" section.npm run i18n:extract— a lightweighti18next-parsersafety net that statically findst()calls and flags/backfills any keys missing from locale JSON. Safe to run repeatedly..github/workflows/claude-localization-reviewer.yml("Claude Localization Reviewer") — a dedicated PR check that runs only when a PR touchessrc/**/*.{js,jsx,ts,tsx}orpublic/locales/**. Two layers:ci/scripts/check_localization_objective.js— deterministic checks: TypeScript errors (scoped to changed files), locale JSON validity, cross-locale key parity (CLDR-plural-aware per locale), and brokent()/i18n.t()key references.t()wiring for new hardcoded copy, value-formatting rule violations, and translation quality/grammar in the non-English locale files.Posts inline PR comments on each confirmed finding (
ci/scripts/post_inline_comments.js, generic/reusable — parses a report's Blocking bullets rather than relying on the model to call its own commenting tool) plus a summary comment, and fails the check on any confirmed finding. The summary comment is posted fresh on every run rather than updated in place, mirroringclaude-auto-reviewer.yml, so the PR timeline shows the review history ("FAIL" → fix commits → "PASS").ci/scripts/build_localization_diff.jskeeps the diff handed to Claude within a fixed byte budget on large PRs without starving source-file coverage in favor of locale-file coverage (or vice versa). Runs onclaude-sonnet-5.cypress/e2e/.../sub-app-translations.spec.js— Cypress coverage for locale switching across sub-apps.Screenshots / video
Screencast_20260708_094140.webm