Skip to content

feat(dashboard): localize the tenant app with react-i18next (4/4) - #1363

Open
marcelo-maciel wants to merge 1 commit into
fullstackhero:mainfrom
marcelo-maciel:feat/i18n-dashboard
Open

feat(dashboard): localize the tenant app with react-i18next (4/4)#1363
marcelo-maciel wants to merge 1 commit into
fullstackhero:mainfrom
marcelo-maciel:feat/i18n-dashboard

Conversation

@marcelo-maciel

@marcelo-maciel marcelo-maciel commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

clients/dashboard slice of the i18n work, split out of #1344. 134 files, of which 32 are JSON catalogs. Same shape as the admin slice.

Independent of the backend slices. Nothing here needs the API change, and the API change needs nothing here.

Slice Files PR
Framework 56 #1360 — the hard review
Module catalogs and wiring 229 #1361 — depends on the framework slice
clients/admin 111 #1362 — independent
clients/dashboard 134 this one

What is in here

  • react-i18next wiring in src/i18n.ts. The chosen language is persisted to the user profile and sent to the API as Accept-Language through apiFetch; variants are canonicalised onto a supported tag before the call, so the API never sees a bare pt.
  • en-US and pt-BR catalogs per feature namespace — catalog, chat, tickets, files, billing, identity, settings — held at strict key and placeholder parity in both directions by tests/i18n/parity.spec.ts.
  • Language switcher in the topbar, and html[lang] follows the active language through a languageChanged listener, replacing the static lang="en" that nothing updated.
  • Formatting stays in the presentation layer, the other half of the framework slice's UI-culture-only decision.
  • Impersonation handoff adopts locale from the URL before createRoot, which fixes the shell language and Accept-Language in the same step. Harmless on its own: on main nothing sends the parameter until the admin slice lands, and this PR's handoff-locale.spec.ts passes with only this half present.
  • The topbar stops hydrating the language from the profile once the user chooses one in-session. updateMyProfile is a read-modify-write with no concurrency token, and Settings › Profile invalidates the same ["identity","me"] key the topbar reads, so a save whose read preceded the language PUT could echo the old locale back and win — and the hydration effect would then switch the UI to it. A locale set on another device still carries over on a fresh mount.

Testing

Everything below is this slice on its own, at main plus these 134 files — not a share of the unsplit branch's totals.

  • npm ci, npm run build (tsc -b + vite build), npx tsc -b tsconfig.tests.json and npm run lint: all exit 0.
  • Playwright, full suite: 183 passed, 0 failed, 0 flaky, 0 skipped — the same count as on the unsplit branch, so nothing was lost in the cut. That includes tests/impersonation/handoff-locale.spec.ts, which passes with only this half of the handoff present.

src/Directory.Packages.props

One backend file in a React PR, which needs explaining. template-smoke.yml triggers on paths: clients/** and runs dotnet build on the scaffolded solution, and .template.config/template.json does not exclude src/Tests/** — so a front-end-only PR still restores the full package graph and hits NU1903 / GHSA-q939-rpr3-3284 on SSH.NET 2025.1.0, pulled transitively by Testcontainers. That advisory fails restore on main too, re-verified today at 3f2959e6.

The pin is byte-identical to #1333, which owns the fix, so both stay mergeable in either order and this copy can simply be dropped once #1333 lands. All four slices carry it for the same reason.

Notes

  • The hydration guard has no regression test. Reproducing it needs an in-mount profile refetch driven through the Settings form, and the click races the language-change re-render (element detached from the DOM). Three distinct approaches, then stopped rather than paper over it. The underlying lost update on PUT /identity/profile is a contract change and is tracked in #1359, where the ponytail: comment in the topbar points.
  • SignalR does not carry the app locale: the hub client builds its own requests instead of going through apiFetch, so Accept-Language on the negotiate is the browser's. Named explicitly in handoff-locale.spec.ts so any other channel that stops carrying the locale fails the test.

Docs (Golden Rule #10)

fullstackhero/docs#238, kept as a single PR covering all four slices — internationalization.mdx is one page whose sections map across the split. The Frontend (admin and dashboard) section is this slice and the admin one: catalogs, language detection and normalization, Accept-Language, the switcher and locale-aware formatting. That PR should merge after the last of the four, so the page never describes code that is not on main yet.

Dashboard slice of the i18n work (split of fullstackhero#1344 as requested in review).
Self-contained: it needs nothing from the backend slices, and the backend needs
nothing from it.

- `react-i18next` wiring in `src/i18n.ts`, language detected from the stored
  preference and negotiated with the API through `Accept-Language`.
- English and Brazilian Portuguese catalogs, split per feature namespace,
  covering catalog, chat, tickets, files, billing, identity and settings.
- Language switcher in the topbar; the chosen language is persisted to the user
  profile, and `html[lang]` follows it through a `languageChanged` listener.
- The topbar stops hydrating the language from the profile once the user has
  chosen one in the session, so a concurrent profile save cannot silently
  switch the app back. The underlying lost update on
  `PUT /identity/profile` is tracked in fullstackhero#1359 and fixed separately.
- Impersonation handoff adopts `locale` from the URL before `createRoot`, so an
  operator arriving from the admin app keeps their language. Harmless on its
  own: on `main` nothing sends the parameter yet.
- Playwright specs pin catalog parity (keys and placeholders, both directions),
  the switcher, the shell, the hydration guard and the handoff parameter.

The `SSH.NET` pin (`2026.0.0`) rides along because `template-smoke.yml` runs on
`clients/**` and builds the scaffolded solution, which fails `restore` with
`NU1903` until fullstackhero#1333 merges. It is byte-identical to that PR.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant