Skip to content

feat(admin): localize the operator app with react-i18next (3/4) - #1383

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

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

Conversation

@marcelo-maciel

Copy link
Copy Markdown
Contributor

Reopened from #1362. That PR was closed automatically on 2026-09-14, when the head fork
was deleted. The branch and the commits are unchanged — the head commit is still
a10d397626857cc47df8ba917b94df08c1a1d672. The earlier review history stays on #1362.


clients/admin slice of the i18n work, split out of #1344. 111 files, of which 30 are JSON catalogs. Mostly mechanical, as you predicted.

Independent of the backend slices. Nothing here needs the API change, and the API change needs nothing here. It can be reviewed and merged in any order relative to the other three.

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

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, split per feature namespace, held at strict key and placeholder parity in both directions by tests/i18n/parity.spec.ts — a missing or mis-arged translation fails the build instead of shipping English.
  • Language switcher in the topbar, and html[lang] follows the active language through a languageChanged listener. The app previously shipped a static lang="en" that nothing updated, so a Portuguese UI announced itself as English to screen readers and browser translation.
  • Formatting stays in the presentation layer (src/lib/format.ts). That is the other half of the framework slice's UI-culture-only decision: the API pins CultureInfo.CurrentCulture to invariant and the app formats numbers, dates and currency itself.
  • Impersonation handoff carries locale in the URL. StartImpersonation strips the target's locale claim on purpose, and the two apps normally sit on different origins, so there was no other way to convey it and the API fell through to the dashboard's own browser detection. Harmless on its own: on main nothing reads the parameter until the dashboard slice lands, and this PR's handoff-locale.spec.ts passes with only this half present.

Testing

Everything below is this slice on its own, at main plus these 111 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: 135 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. Full rationale in the framework PR.

Notes

  • The topbar's language hydration stops once the user chooses a language in-session, guarding against a stale profile save echoing the old locale back. That 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 with retries or a longer timeout. The underlying lost update on PUT /identity/profile is tracked in #1359, where the ponytail: comments in the topbar point.
  • 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. Applies to every session, not just impersonation. 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 dashboard 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.

Admin 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.
- Language switcher in the topbar; the chosen language is persisted to the user
  profile so it survives a reload, and `html[lang]` follows it through a
  `languageChanged` listener rather than staying pinned to `en`.
- Number, date and currency formatting moved to `src/lib/format.ts` so the
  presentation layer owns formatting. The API stays UI-culture-only.
- Impersonation handoff carries `locale` in the URL so an operator keeps their
  language when landing in the tenant app. Harmless on its own: nothing reads
  the parameter until the dashboard slice ships.
- Playwright specs pin catalog parity (keys and placeholders, both directions),
  the switcher, formatting 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 chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a10d397626

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

firstName: input.firstName ?? profile.firstName ?? null,
lastName: input.lastName ?? profile.lastName ?? null,
phoneNumber: input.phoneNumber ?? profile.phoneNumber ?? null,
locale: input.locale ?? profile.locale ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the locale field to the identity profile contract

PUT /identity/profile currently has no way to persist this field: UpdateUserCommand exposes only the existing name/phone/email/image properties, while the server-side UserDto and FshUser also have no Locale. ASP.NET simply ignores this JSON property, so selecting a language returns 200 but never survives a server profile read, cannot follow the user to another device, and cannot be minted into the refreshed JWT as the switcher expects.

Useful? React with 👍 / 👎.

Comment thread clients/admin/src/env.ts
dashboardUrl: (cfg.dashboardUrl ?? "http://localhost:5174").replace(/\/$/, ""),
inactivityIdleMs: positiveOr(cfg.inactivityIdleMs, DEFAULT_INACTIVITY_IDLE_MS),
inactivityWarningMs: positiveOr(cfg.inactivityWarningMs, DEFAULT_INACTIVITY_WARNING_MS),
defaultLanguage: cfg.defaultLanguage ?? "en-US",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate defaultLanguage to production runtime config

For Docker and Terraform deployments, the generated admin config.json still contains only apiBase, defaultTenant, and dashboardUrl; neither clients/admin/docker/config.json.template nor the Terraform runtime_config map emits defaultLanguage. Consequently every production deployment takes this fallback as en-US, making the advertised per-deployment default language impossible to configure outside the Vite development file.

AGENTS.md reference: AGENTS.md:L47-L49

Useful? React with 👍 / 👎.

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