feat(portal-shell): i18n string sweep — mark UI strings + FR translations (#93)
## 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").
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #93
This commit was merged in pull request #93.
This commit is contained in:
@@ -16,14 +16,16 @@
|
||||
class="h-9 w-9 shrink-0"
|
||||
/>
|
||||
@if (!collapsed()) {
|
||||
<span class="text-base font-semibold sm:text-lg">APF Portal</span>
|
||||
<span class="text-base font-semibold sm:text-lg" i18n="@@header.wordmark">APF Portal</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center gap-4 px-4 sm:px-6">
|
||||
<form role="search" class="mx-2 hidden flex-1 md:flex" (submit)="$event.preventDefault()">
|
||||
<label class="sr-only" for="global-search">Search the portal</label>
|
||||
<label class="sr-only" for="global-search" i18n="@@header.search.label"
|
||||
>Search the portal</label
|
||||
>
|
||||
<div
|
||||
class="flex w-full max-w-xl items-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-4 py-2 focus-within:border-brand-primary-500 focus-within:bg-white focus-within:ring-2 focus-within:ring-brand-primary-200 dark:border-gray-700 dark:bg-gray-800 dark:focus-within:border-brand-primary-300 dark:focus-within:bg-gray-900 dark:focus-within:ring-brand-primary-700"
|
||||
>
|
||||
@@ -31,6 +33,7 @@
|
||||
<input
|
||||
id="global-search"
|
||||
type="search"
|
||||
i18n-placeholder="@@header.search.placeholder"
|
||||
placeholder="Search…"
|
||||
class="w-full bg-transparent text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none dark:text-gray-100 dark:placeholder:text-gray-500"
|
||||
/>
|
||||
@@ -41,6 +44,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
|
||||
i18n-aria-label="@@header.action.notifications"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<app-icon name="bell" />
|
||||
@@ -52,6 +56,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
|
||||
i18n-aria-label="@@header.action.help"
|
||||
aria-label="Help"
|
||||
>
|
||||
<app-icon name="circle-help" />
|
||||
@@ -60,11 +65,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
|
||||
i18n-aria-label="@@header.action.settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<app-icon name="settings" />
|
||||
</button>
|
||||
<span
|
||||
i18n-aria-label="@@header.action.userMenu"
|
||||
aria-label="User menu (coming soon)"
|
||||
class="ml-1 inline-flex h-9 w-9 items-center justify-center rounded-full bg-brand-primary-500 text-sm font-semibold text-white"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user