Skip to content

feat: add language switcher - #2527

Open
finnar-bin wants to merge 1 commit into
stagefrom
feat/4148-product-localization
Open

finnar-bin wants to merge 1 commit into
stagefrom
feat/4148-product-localization

Conversation

@finnar-bin

Copy link
Copy Markdown
Contributor

Description

Adds a language switcher in the user preferences settings

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Screenshots / Screen recording

image

@finnar-bin finnar-bin self-assigned this Jul 8, 2026
@finnar-bin finnar-bin changed the title Add language switcher feat: add language switcher Jul 8, 2026
Comment thread src/views/accounts/profile/Preference.js
Comment thread src/views/accounts/profile/Preference.js
Comment thread src/views/accounts/profile/Preference.js
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review summary

Small, contained change — a language dropdown on the Preferences page that persists prefs.locale on the user record. Three concerns, left as inline comments:

  1. Labels are BCP‑47 codes, not native names. HI-IN / ZH-CN etc. are hard to recognize without prior knowledge of locale tags. Displaying each language in its own script is the standard pattern.
  2. No consumer for the preference. No i18n library is installed and no code path in src/ reads prefs.locale. If Manager‑UI reads it server‑side that's fine — otherwise the setting will appear broken to users. Worth clarifying in the PR description or gating behind a flag until wiring lands.
  3. No validation of legacy values. If a stored prefs.locale isn't in LOCALES, the <NativeSelect> renders with an unmatched value and React will warn.

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.

@finnar-bin
finnar-bin force-pushed the feat/4148-product-localization branch from 6d97b2e to b9b6ddc Compare July 8, 2026 01:35
Comment thread src/views/accounts/profile/Preference.js
Comment thread src/views/accounts/profile/Preference.js
Comment thread src/views/accounts/profile/Preference.js
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review summary

The change itself is small and safely persists a new locale field into userInfo.prefs without disturbing existing keys (...prefs spread is preserved). A few observations:

Main concern — no consumer for this preference yet. A codebase search turns up no i18n runtime (no useTranslation, no i18n/LanguageProvider, no locale-aware string catalog) — the only locale references in src/ are unrelated (FillerContent, instance-level locale settings for CMS content, etc.). So this switcher lets a user save a locale that nothing currently reads. That's fine if it's a deliberate scaffolding step ahead of the Manager-UI i18n rollout, but worth calling out because:

  • A user changing this today gets no visible effect, which will read as a bug in support tickets.
  • Until a consumer lands, the "App Language" row could reasonably be gated behind a feature flag or omitted.

If there's a paired Manager-UI PR that consumes prefs.locale, linking it in the description would make the intent clear.

Smaller items (left as inline comments):

  • LOCALES naming diverges from the sibling teamOption / instanceOptions (camelCase) constants.
  • Labels display raw upper-cased locale codes (EN-US, HI-IN) rather than language names — awkward for users who don't already read English.
  • 'en-US' default is repeated in the useState init and the useEffect fallback; extract to a shared constant.
  • prefs?.locale || 'en-US'?? would be safer if empty string is ever a legitimate value (unlikely, but conveys intent).

No security or performance concerns.

finnar-bin added a commit to zesty-io/manager-ui that referenced this pull request Sep 15, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants