feat(portal-shell): collapse accessibility routes into one i18n-marked route (#94)
CI / check (push) Successful in 4m0s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m56s
CI / a11y (push) Successful in 2m6s
CI / perf (push) Successful in 3m50s

## Summary

Continue ADR-0019: replace the `/accessibility` + `/accessibilite` twin routes with a single canonical route whose content is i18n-marked in the template. The per-locale build (en/fr) already inlines the right copy — the route-data + `copy()` service indirection is no longer carrying its weight.

## What changes

- **`AccessibilityStatement`** loses its `ActivatedRoute` injection, the `Lang` discriminator, and the `COPY` lookup table. The component is now a plain shell over the template.
- **`accessibility.html`** carries the title + intro + status panel as `i18n="@@page.accessibility.*"` markers. Six new trans-units in [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf) provide the French copy — verbatim from the old `COPY.fr` block, so the page reads the same in FR as before.
- **`app.routes.ts`** declares the single canonical route at `path: 'accessibility'` and keeps `/accessibilite` alive as a `redirectTo: 'accessibility'`. Drops `data: { lang: ... }` — no longer consumed.
- **`footer.html`** collapses the dual link into one i18n-marked link (`@@footer.accessibilityLink`). EN bundle reads "Accessibility statement"; FR bundle reads "Déclaration d'accessibilité".

## Decision worth flagging

The path stays in English across both locales for now: `/en/accessibility` and `/fr/accessibility`. Translating route *segments* (`/fr/declaration-d-accessibilite`) needs either a custom URL serializer or per-locale route trees — not worth the complexity at this scale. The page title and the link label already differ per locale via i18n, which is what's actually visible to users.

The historical `/accessibilite` path keeps working via the route-level redirect. Drops out of the codebase once analytics confirm no traffic reaches it.

## Test plan

- [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green. **35 / 35 specs** (was 36; the obsolete `lang`-fallback test is removed).
- [x] Production build emits both locales. FR bundle contains `Statut`, `Déclaration`, the FR intro / panel bodies. No leftover English on swept strings.
- [x] `extract-i18n` clean (49 unique units now: +5 for `page.accessibility.*` + `footer.accessibilityLink`, −0; the old route-data `lang` markers were not i18n).
- [ ] Manual: serve-static then `/en/accessibility` and `/fr/accessibility` render their respective content; `/fr/accessibilite` 301-redirects to `/fr/accessibility`.
- [ ] Manual: footer shows one link, locale-aware ("Accessibility statement" / "Déclaration d'accessibilité").
- [ ] Manual: browser tab title flips between bundles ("Accessibility statement · APF Portal" / "Déclaration d'accessibilité · Portail APF").

## What this PR explicitly does NOT do

- Translate the URL path segment (next ADR-only refinement if needed).
- Add the locale switcher in the footer — that's the next PR on the i18n track.
- Wire a CI gate on missing translations.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #94
This commit was merged in pull request #94.
This commit is contained in:
2026-05-11 20:18:28 +02:00
parent 65fae7f963
commit 8f84cc6389
7 changed files with 105 additions and 134 deletions
@@ -6,24 +6,11 @@
<p i18n="@@footer.copyright">&copy; {{ year }} APF France handicap</p>
<nav i18n-aria-label="@@footer.aria.legal" aria-label="Legal">
<ul class="flex items-center gap-4">
<li>
<a
routerLink="/accessibility"
lang="en"
class="rounded hover:text-brand-primary-500 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-primary-500 dark:hover:text-brand-primary-300"
>Accessibility</a
>
</li>
<li aria-hidden="true" class="text-gray-300 dark:text-gray-700">|</li>
<li>
<a
routerLink="/accessibilite"
lang="fr"
class="rounded hover:text-brand-primary-500 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-primary-500 dark:hover:text-brand-primary-300"
>Accessibilité</a
>
</li>
</ul>
<a
routerLink="/accessibility"
class="rounded hover:text-brand-primary-500 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-primary-500 dark:hover:text-brand-primary-300"
i18n="@@footer.accessibilityLink"
>Accessibility statement</a
>
</nav>
</footer>
@@ -25,11 +25,15 @@ describe('Footer', () => {
expect(text).toContain(String(new Date().getFullYear()));
});
it('renders both accessibility statement links (EN + FR)', async () => {
it('renders the accessibility statement link (single, locale-aware via i18n)', async () => {
const fixture = TestBed.createComponent(Footer);
await fixture.whenStable();
const root = fixture.nativeElement as HTMLElement;
expect(root.querySelector('a[href="/accessibility"]')).not.toBeNull();
expect(root.querySelector('a[href="/accessibilite"]')).not.toBeNull();
const link = root.querySelector('a[href="/accessibility"]') as HTMLAnchorElement | null;
expect(link).not.toBeNull();
expect(link?.textContent?.trim()).toBe('Accessibility statement');
// The historical `/accessibilite` link is gone — that path now
// redirects at the route level rather than being a separate link.
expect(root.querySelector('a[href="/accessibilite"]')).toBeNull();
});
});