feat: add language switcher - #2527
finnar-bin wants to merge 1 commit into
Conversation
Review summarySmall, contained change — a language dropdown on the Preferences page that persists
Nothing security-sensitive; nothing that touches auth or the CMS fetch path. No performance concerns — six-item static list, one extra piece of state. Approval blockers are (1) and (2); (3) is a nice-to-have hardening. |
6d97b2e to
b9b6ddc
Compare
Review summaryThe change itself is small and safely persists a new Main concern — no consumer for this preference yet. A codebase search turns up no i18n runtime (no
If there's a paired Manager-UI PR that consumes Smaller items (left as inline comments):
No security or performance concerns. |
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 - [x] zesty-io/material#123 — adds the locale switcher component to `@zesty-io/material` for accounts-ui to consume. - [ ] zesty-io/website#2527 — adds the switcher UI to accounts-ui/Preferences, plus the endpoint + call that writes the selected locale to the user's `prefs.locale` DB 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 to `localStorage` client-side — it never writes locale back to the database itself. ## Architecture - Config lives in `src/shell/i18n/index.ts`, loaded before the React root renders; root is wrapped in `<Suspense>`. - Locale data is served from `public/locales/<locale>/<namespace>.json`. - **DB preference wins eventually, not on first paint** — the app renders right away using the cached/browser locale, then switches to the DB preference once it loads, instead of blocking render on that fetch. We chose a possible brief locale flash over a blank white screen while waiting. - Keys are flat, qualified camelCase strings (`t("content.publishItem")`) — the namespace is the first dot-segment, everything after is one flat key. No nested JSON, no second dot. - All 15 namespaces load **eagerly at boot**, not lazy-loaded per sub-app. Each sub-app root still wraps a local `<Suspense>` and calls `useTranslation("<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 calling `i18n.t()` outside of React) could fire before a sub-app's namespace had loaded, and the perf gain was minimal anyway. - Dev throws on any missing key (with the en-US fallback disabled in dev so non-English gaps surface immediately); stage/prod fall back to `en-US` and report once per key to Sentry. - MUI component chrome (DataGrid/DatePicker/Autocomplete labels) localizes separately through `localizeTheme` / `LocalizedThemeProvider`, not through `t()`. - Dates go through `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, wires `t()`/`i18n.t()` calls, writes `en-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 lightweight `i18next-parser` safety net that statically finds `t()` 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 touches `src/**/*.{js,jsx,ts,tsx}` or `public/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 broken `t()`/`i18n.t()` key references. - A Claude review pass over the changed diff for what only language/intent can catch: missed `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, mirroring `claude-auto-reviewer.yml`, so the PR timeline shows the review history ("FAIL" → fix commits → "PASS"). `ci/scripts/build_localization_diff.js` keeps 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 on `claude-sonnet-5`. - `cypress/e2e/.../sub-app-translations.spec.js` — Cypress coverage for locale switching across sub-apps. ## Screenshots / video [Screencast_20260708_094140.webm](https://github.com/user-attachments/assets/93f36b18-5d00-4114-b928-1388d3180bd0) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Description
Adds a language switcher in the user preferences settings
Type of change
Please delete options that are not relevant.
Screenshots / Screen recording