From 65fae7f963d5a0e4f86b87868ba4875bf7f44c0e Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 11 May 2026 19:54:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(portal-shell):=20i18n=20string=20sweep=20?= =?UTF-8?q?=E2=80=94=20mark=20UI=20strings=20+=20FR=20translations=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: (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"). --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/93 --- apps/portal-shell/src/app/app.html | 2 +- apps/portal-shell/src/app/app.routes.ts | 13 +- .../src/app/components/footer/footer.html | 7 +- .../src/app/components/header/header.html | 11 +- .../src/app/components/sidebar/sidebar.html | 22 +- .../src/app/components/sidebar/sidebar.ts | 35 ++- .../theme-switcher/theme-switcher.html | 10 +- .../theme-switcher/theme-switcher.ts | 13 +- .../portal-shell/src/app/pages/home/home.html | 40 +++- apps/portal-shell/src/locale/messages.fr.xlf | 211 +++++++++++++++++- apps/portal-shell/tsconfig.app.json | 2 +- apps/portal-shell/tsconfig.spec.json | 2 +- 12 files changed, 312 insertions(+), 56 deletions(-) diff --git a/apps/portal-shell/src/app/app.html b/apps/portal-shell/src/app/app.html index 0f124a2..d5b6da7 100644 --- a/apps/portal-shell/src/app/app.html +++ b/apps/portal-shell/src/app/app.html @@ -1,4 +1,4 @@ - +
diff --git a/apps/portal-shell/src/app/app.routes.ts b/apps/portal-shell/src/app/app.routes.ts index 42fd526..03db77f 100644 --- a/apps/portal-shell/src/app/app.routes.ts +++ b/apps/portal-shell/src/app/app.routes.ts @@ -1,11 +1,18 @@ import { Route } from '@angular/router'; +// Tab titles per route — marked with `$localize` so each locale's +// bundle ships its own. The `/accessibility` and `/accessibilite` +// pair shares a title key; the upcoming route fusion (per ADR-0019) +// collapses them to a single localised route. +const homeTitle = $localize`:@@route.home.title:APF Portal`; +const accessibilityTitle = $localize`:@@route.accessibility.title:Accessibility statement · APF Portal`; + export const appRoutes: Route[] = [ { path: '', pathMatch: 'full', loadComponent: () => import('./pages/home/home').then((m) => m.Home), - title: 'APF Portal', + title: homeTitle, }, // RGAA + ADR-0016: the accessibility statement must be reachable // from both locales' canonical paths. Same component, content @@ -15,13 +22,13 @@ export const appRoutes: Route[] = [ loadComponent: () => import('./pages/accessibility/accessibility').then((m) => m.AccessibilityStatement), data: { lang: 'en' }, - title: 'Accessibility statement · APF Portal', + title: accessibilityTitle, }, { path: 'accessibilite', loadComponent: () => import('./pages/accessibility/accessibility').then((m) => m.AccessibilityStatement), data: { lang: 'fr' }, - title: 'Déclaration d’accessibilité · APF Portal', + title: accessibilityTitle, }, ]; diff --git a/apps/portal-shell/src/app/components/footer/footer.html b/apps/portal-shell/src/app/components/footer/footer.html index e845a8d..b7f8081 100644 --- a/apps/portal-shell/src/app/components/footer/footer.html +++ b/apps/portal-shell/src/app/components/footer/footer.html @@ -1,14 +1,16 @@
-

© {{ year }} APF France handicap

+

© {{ year }} APF France handicap

-