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:
@@ -3,20 +3,207 @@
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<!--
|
||||
French translation file for portal-shell.
|
||||
French translations for portal-shell, per ADR-0019.
|
||||
|
||||
Empty in this initial plumbing PR per ADR-0019. The first string
|
||||
sweep PR will mark every UI string in the templates with
|
||||
`i18n="@@<id>"` and add a `<trans-unit id="<id>">` entry below
|
||||
with the French translation. Until then, the production build's
|
||||
`--localize` emits both `dist/apps/portal-shell/browser/en/...`
|
||||
and `dist/apps/portal-shell/browser/fr/...`, but the FR bundle
|
||||
falls back to source text for every untranslated key.
|
||||
|
||||
Editor convention: keep the entries grouped by feature
|
||||
(header.*, sidebar.*, footer.*, page.<route>.*) and ordered
|
||||
alphabetically within each group, so diffs stay readable.
|
||||
Source IDs are stable @@-prefixed values authored in the
|
||||
templates / `$localize` calls. Trans-units are grouped by
|
||||
feature (app, footer, header, sidebar, theme, page.*) and
|
||||
alphabetical within each group so diffs stay readable.
|
||||
-->
|
||||
|
||||
<!-- app -->
|
||||
<trans-unit id="app.skipLink" datatype="html">
|
||||
<source>Skip to main content</source>
|
||||
<target>Aller au contenu principal</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- footer -->
|
||||
<trans-unit id="footer.aria.label" datatype="html">
|
||||
<source>Page footer</source>
|
||||
<target>Pied de page</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="footer.aria.legal" datatype="html">
|
||||
<source>Legal</source>
|
||||
<target>Mentions légales</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="footer.copyright" datatype="html">
|
||||
<source>© <x id="INTERPOLATION" equiv-text="{{ year }}"/> APF France handicap</source>
|
||||
<target>© <x id="INTERPOLATION" equiv-text="{{ year }}"/> APF France handicap</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- header -->
|
||||
<trans-unit id="header.action.help" datatype="html">
|
||||
<source>Help</source>
|
||||
<target>Aide</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.action.notifications" datatype="html">
|
||||
<source>Notifications</source>
|
||||
<target>Notifications</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.action.settings" datatype="html">
|
||||
<source>Settings</source>
|
||||
<target>Paramètres</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.action.userMenu" datatype="html">
|
||||
<source>User menu (coming soon)</source>
|
||||
<target>Menu utilisateur (bientôt disponible)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.search.label" datatype="html">
|
||||
<source>Search the portal</source>
|
||||
<target>Rechercher dans le portail</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.search.placeholder" datatype="html">
|
||||
<source>Search…</source>
|
||||
<target>Rechercher…</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.wordmark" datatype="html">
|
||||
<source>APF Portal</source>
|
||||
<target>Portail APF</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- sidebar -->
|
||||
<trans-unit id="sidebar.aria.label" datatype="html">
|
||||
<source>Sidebar navigation</source>
|
||||
<target>Navigation latérale</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.aria.sections" datatype="html">
|
||||
<source>Sections</source>
|
||||
<target>Sections</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.group.communication" datatype="html">
|
||||
<source>Communication</source>
|
||||
<target>Communication</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.group.establishments" datatype="html">
|
||||
<source>Establishments</source>
|
||||
<target>Établissements</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.group.general" datatype="html">
|
||||
<source>General</source>
|
||||
<target>Général</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.item.activities" datatype="html">
|
||||
<source>Activities</source>
|
||||
<target>Activités</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.item.calendar" datatype="html">
|
||||
<source>Calendar</source>
|
||||
<target>Calendrier</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.item.dashboard" datatype="html">
|
||||
<source>Dashboard</source>
|
||||
<target>Tableau de bord</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.item.directory" datatype="html">
|
||||
<source>Directory</source>
|
||||
<target>Annuaire</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.item.documents" datatype="html">
|
||||
<source>Documents</source>
|
||||
<target>Documents</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.item.messages" datatype="html">
|
||||
<source>Messages</source>
|
||||
<target>Messages</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.role.anonymous" datatype="html">
|
||||
<source>Anonymous</source>
|
||||
<target>Anonyme</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.role.aria" datatype="html">
|
||||
<source>Current role: anonymous (signed-out)</source>
|
||||
<target>Rôle actuel : anonyme (non connecté)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.role.label" datatype="html">
|
||||
<source>Role:</source>
|
||||
<target>Rôle :</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.toggle.aria.collapse" datatype="html">
|
||||
<source>Collapse sidebar</source>
|
||||
<target>Replier la barre latérale</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.toggle.aria.expand" datatype="html">
|
||||
<source>Expand sidebar</source>
|
||||
<target>Déplier la barre latérale</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sidebar.toggle.collapse" datatype="html">
|
||||
<source>Collapse</source>
|
||||
<target>Replier</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- theme -->
|
||||
<trans-unit id="theme.menu.aria" datatype="html">
|
||||
<source>Theme</source>
|
||||
<target>Thème</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="theme.mode.dark" datatype="html">
|
||||
<source>Dark</source>
|
||||
<target>Sombre</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="theme.mode.light" datatype="html">
|
||||
<source>Light</source>
|
||||
<target>Clair</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="theme.mode.system" datatype="html">
|
||||
<source>System</source>
|
||||
<target>Système</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="theme.trigger.aria" datatype="html">
|
||||
<source>Theme: <x id="mode" equiv-text="this.currentLabel()"/> (open menu)</source>
|
||||
<target>Thème : <x id="mode" equiv-text="this.currentLabel()"/> (ouvrir le menu)</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- route -->
|
||||
<trans-unit id="route.accessibility.title" datatype="html">
|
||||
<source>Accessibility statement · APF Portal</source>
|
||||
<target>Déclaration d’accessibilité · Portail APF</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="route.home.title" datatype="html">
|
||||
<source>APF Portal</source>
|
||||
<target>Portail APF</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- page.home -->
|
||||
<trans-unit id="page.home.intro" datatype="html">
|
||||
<source> This is the placeholder home page for the APF Portal. Real content lands once the comms team and the accessibility review panel sign off on the copy. </source>
|
||||
<target> Ceci est la page d'accueil provisoire du portail APF. Le contenu définitif arrivera après validation par l'équipe communication et le panel d'audit d'accessibilité. </target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.error" datatype="html">
|
||||
<source> Backend unreachable. The BFF dev server may not be running, or CORS is misconfigured. </source>
|
||||
<target> Backend inaccessible. Le serveur BFF de développement est peut-être arrêté, ou la configuration CORS est incorrecte. </target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.loading" datatype="html">
|
||||
<source> Checking the backend… </source>
|
||||
<target> Vérification du backend… </target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.service" datatype="html">
|
||||
<source>Service</source>
|
||||
<target>Service</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.status" datatype="html">
|
||||
<source>Status</source>
|
||||
<target>Statut</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.title" datatype="html">
|
||||
<source> System status </source>
|
||||
<target> État du système </target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.uptime" datatype="html">
|
||||
<source>Uptime</source>
|
||||
<target>Disponibilité</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.uptimeValue" datatype="html">
|
||||
<source><x id="INTERPOLATION" equiv-text="{{ h.uptimeSeconds }}"/> s</source>
|
||||
<target><x id="INTERPOLATION" equiv-text="{{ h.uptimeSeconds }}"/> s</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.status.version" datatype="html">
|
||||
<source>Version</source>
|
||||
<target>Version</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="page.home.title" datatype="html">
|
||||
<source> Welcome to APF Portal </source>
|
||||
<target> Bienvenue sur Portail APF </target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user