feat(portal-shell): i18n string sweep — mark UI strings + FR translations #93

Merged
julien merged 1 commits from feat/portal-shell/i18n-string-sweep into main 2026-05-11 19:54:44 +02:00
Owner

Summary

Continue ADR-0019 implementation. Mark every UI string surfaced by the shell with i18n="@@id" (templates) or $localize (TypeScript), and populate messages.fr.xlf with French translations. The production build now ships two genuinely different bundles under dist/.../{en,fr}/.

43 trans-units marked, grouped by feature:

Surface Strings
app.html skip-link
header.html wordmark, search label + placeholder, action button aria-labels, user-menu placeholder
sidebar.ts + .html menu groups + items, aside / nav aria-labels, role badge, toggle button (Expand / Collapse aria + Collapse text)
theme-switcher.ts + .html mode labels, menu aria, trigger aria (Theme: <mode> (open menu) with named placeholder)
footer.html aria-labels, copyright (interpolation preserved)
home.html welcome heading + intro + status widget labels
app.routes.ts browser tab titles

Tooling

  • Add "@angular/localize" to the types array in both tsconfig.app.json and tsconfig.spec.json so TypeScript resolves the $localize global at compile time. Specs need it too — they evaluate the same component code paths.
  • Extraction target (nx run portal-shell:extract-i18n) reports 44 messages (43 unique IDs).

Translation choices worth flagging

  • Wordmark: "APF Portal" → "Portail APF". Same key used for the / browser tab title. The PWA manifest (site.webmanifest) stays "APF Portal" — manifest values are not bundled, they sit in the static assets and are language-neutral in v1.
  • System theme mode → "Système".
  • "Anonymous" role → "Anonyme"; "Role:""Rôle :" (French uses a non-breaking space before the colon — typographic convention, preserved in the XLIFF target).
  • Accessibility links in the footer stay bilingual. Each carries its own lang attribute (lang="en" and lang="fr"). The dual-link pattern goes away in the upcoming route-fusion PR; until then it's the most honest stopgap.
  • Both accessibility routes share one title key (@@route.accessibility.title). In the EN bundle, both display "Accessibility statement · APF Portal"; in the FR bundle, both display "Déclaration d'accessibilité · Portail APF". After route fusion only one route remains.

Verification

  • Production build: 129 kB gzip initial per locale (vs 122 kB before). +7 kB absorbs the i18n marker metadata and the embedded translation data in the FR bundle. Well under the 300 KB budget.
  • Spot-checked the FR bundle: no leftover English source text on any swept string, route title, or home page intro. The Welcome to APF Portal in the lazy home chunk shows "Bienvenue sur Portail APF" in FR.
  • 36 / 36 specs unchanged. They run in the source locale (en), so the English assertions still match. No spec edits needed.
  • Lint clean.

Out of scope (each its own follow-up PR)

  • Locale switcher in the footer + __Host-portal_locale cookie + smart / redirect.
  • Collapse /accessibility + /accessibilite into one localised route with locale-translated path segments.
  • CI gate that fails the build on a missing translation. Once the sweep is reviewed, we add nx build --localize to ci:check and verify it rejects unsealed strings.
  • Accessibility page content localisation. Its content is driven by a copy() service rather than i18n-marked templates — restructured during the route fusion.

Test plan

  • pnpm exec nx run-many -t lint test build --projects=portal-shell — green (36 / 36 specs).
  • pnpm exec nx run portal-shell:extract-i18n — clean, 43 unique unit IDs.
  • Production build emits both locales; FR bundle contains "Tableau de bord", "Aller au contenu principal", etc.
  • Manual: pnpm exec nx serve portal-shell shows the EN source. pnpm exec nx build portal-shell --configuration=production && pnpm exec nx run portal-shell:serve-static → open http://localhost:4200/fr/ for FR, /en/ for EN.
  • Manual: every aria-label announced by a screen reader in FR build matches the French translation.
  • Manual: browser tab title flips between bundles ("APF Portal" vs "Portail APF").
## Summary Continue ADR-0019 implementation. Mark every UI string surfaced by the shell with `i18n="@@id"` (templates) or `$localize` (TypeScript), and populate [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf) with French translations. The production build now ships **two genuinely different bundles** under `dist/.../{en,fr}/`. **43 trans-units** marked, grouped by feature: | Surface | Strings | |---|---| | `app.html` | skip-link | | `header.html` | wordmark, search label + placeholder, action button aria-labels, user-menu placeholder | | `sidebar.ts` + `.html` | menu groups + items, aside / nav aria-labels, role badge, toggle button (Expand / Collapse aria + Collapse text) | | `theme-switcher.ts` + `.html` | mode labels, menu aria, trigger aria (`Theme: <mode> (open menu)` with named placeholder) | | `footer.html` | aria-labels, copyright (interpolation preserved) | | `home.html` | welcome heading + intro + status widget labels | | `app.routes.ts` | browser tab titles | ## Tooling - Add `"@angular/localize"` to the `types` array in both [`tsconfig.app.json`](apps/portal-shell/tsconfig.app.json) and [`tsconfig.spec.json`](apps/portal-shell/tsconfig.spec.json) so TypeScript resolves the `$localize` global at compile time. Specs need it too — they evaluate the same component code paths. - Extraction target (`nx run portal-shell:extract-i18n`) reports **44 messages** (43 unique IDs). ## Translation choices worth flagging - **Wordmark**: "APF Portal" → "Portail APF". Same key used for the `/` browser tab title. The PWA manifest (`site.webmanifest`) stays "APF Portal" — manifest values are not bundled, they sit in the static assets and are language-neutral in v1. - **System theme mode** → "Système". - **"Anonymous"** role → "Anonyme"; **"Role:"** → **"Rôle :"** (French uses a non-breaking space before the colon — typographic convention, preserved in the XLIFF target). - **Accessibility links in the footer stay bilingual.** Each carries its own `lang` attribute (`lang="en"` and `lang="fr"`). The dual-link pattern goes away in the upcoming route-fusion PR; until then it's the most honest stopgap. - **Both accessibility routes share one title key** (`@@route.accessibility.title`). In the EN bundle, both display "Accessibility statement · APF Portal"; in the FR bundle, both display "Déclaration d'accessibilité · Portail APF". After route fusion only one route remains. ## Verification - Production build: **129 kB gzip initial per locale** (vs 122 kB before). +7 kB absorbs the i18n marker metadata and the embedded translation data in the FR bundle. Well under the 300 KB budget. - Spot-checked the FR bundle: no leftover English source text on any swept string, route title, or home page intro. The `Welcome to APF Portal` in the lazy `home` chunk shows "Bienvenue sur Portail APF" in FR. - **36 / 36 specs unchanged.** They run in the source locale (`en`), so the English assertions still match. No spec edits needed. - Lint clean. ## Out of scope (each its own follow-up PR) - **Locale switcher in the footer** + `__Host-portal_locale` cookie + smart `/` redirect. - **Collapse `/accessibility` + `/accessibilite`** into one localised route with locale-translated path segments. - **CI gate** that fails the build on a missing translation. Once the sweep is reviewed, we add `nx build --localize` to `ci:check` and verify it rejects unsealed strings. - **Accessibility page content localisation.** Its content is driven by a `copy()` service rather than i18n-marked templates — restructured during the route fusion. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (36 / 36 specs). - [x] `pnpm exec nx run portal-shell:extract-i18n` — clean, 43 unique unit IDs. - [x] Production build emits both locales; FR bundle contains "Tableau de bord", "Aller au contenu principal", etc. - [ ] Manual: `pnpm exec nx serve portal-shell` shows the EN source. `pnpm exec nx build portal-shell --configuration=production && pnpm exec nx run portal-shell:serve-static` → open `http://localhost:4200/fr/` for FR, `/en/` for EN. - [ ] Manual: every aria-label announced by a screen reader in FR build matches the French translation. - [ ] Manual: browser tab title flips between bundles ("APF Portal" vs "Portail APF").
julien added 1 commit 2026-05-11 19:52:18 +02:00
feat(portal-shell): i18n string sweep — mark UI strings + FR translations
CI / commits (pull_request) Successful in 2m12s
CI / scan (pull_request) Successful in 2m13s
CI / check (pull_request) Successful in 2m22s
CI / a11y (pull_request) Successful in 2m13s
CI / perf (pull_request) Successful in 5m27s
47277114da
Continue ADR-0019 implementation. Mark every UI string surfaced by
the shell with `i18n="@@id"` (templates) or `$localize` (TypeScript)
and populate `messages.fr.xlf` with French translations. 43 trans-
units total: skip-link, header, sidebar, theme switcher, footer,
home page, route titles.

What's marked:

- `app.html` skip-link.
- `header.html` brand wordmark, search label + placeholder, action
  buttons (notifications / help / settings), user-menu placeholder.
- `sidebar.ts` MENU data (group + item labels) and a computed aria-
  label for the toggle button (Expand / Collapse).
- `sidebar.html` aside / sections / role / collapse strings.
- `theme-switcher.ts` MODE_OPTIONS labels + computed trigger aria
  ("Theme: <mode> (open menu)" with a named placeholder).
- `theme-switcher.html` menu aria-label.
- `footer.html` aria-labels + copyright. The two accessibility-page
  link labels stay bilingual on purpose (one per language, each
  carrying its `lang` attribute) — the dual-link pattern goes away
  in the upcoming route-fusion PR.
- `home.html` welcome heading + intro + status widget labels.
- `app.routes.ts` browser tab titles. The `/accessibility` and
  `/accessibilite` routes share one title key; the route-fusion PR
  will collapse them to a single localised route.

Tooling:

- Add `"@angular/localize"` to the `types` array in both
  `tsconfig.app.json` and `tsconfig.spec.json` so TypeScript
  resolves the `$localize` global.

Translation choices worth flagging:

- Wordmark: "APF Portal" → "Portail APF" (consistent with the route
  title key).
- "System" theme mode → "Système".
- "Anonymous" role → "Anonyme"; "Role:" → "Rôle :" (French uses a
  non-breaking-space before the colon — typographic convention,
  preserved in the XLIFF target).

Verification:

- Production build emits both locales (`dist/.../{en,fr}/`).
  Spot-checked the FR bundle: no leftover English source text on
  any of the swept strings, route titles, or the home page intro.
- Initial bundle: 129 kB gzip per locale (vs 122 kB before).
  +7 kB gzip absorbs the i18n marker metadata + the FR translation
  data embedded in the FR bundle. Well under the 300 KB budget.
- 36 / 36 specs unchanged (they run in the source locale, so the
  English assertions still match).

Out of scope (each its own follow-up PR):

- Locale switcher in the footer + `__Host-portal_locale` cookie +
  smart `/` redirect.
- Collapse `/accessibility` + `/accessibilite` into one localised
  route.
- CI gate that fails the build on a missing translation.
julien merged commit 65fae7f963 into main 2026-05-11 19:54:44 +02:00
julien deleted branch feat/portal-shell/i18n-string-sweep 2026-05-11 19:54:46 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#93