From 44f00dced35d503f29cfe1f7220809de50a33294 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 11 May 2026 20:12:12 +0200 Subject: [PATCH] feat(portal-shell): collapse accessibility routes into one i18n-marked route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continue ADR-0019: replace the `/accessibility` + `/accessibilite` twin routes with a single canonical `/accessibility` path whose content is i18n-marked in templates. The per-locale build (en/fr) already inlines the right copy — the route data + `copy()` service indirection is no longer needed. What changes: - `AccessibilityStatement` loses its `ActivatedRoute` injection, its `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 strings as `i18n="@@page.accessibility.*"` markers; six new trans-units in `messages.fr.xlf` provide the French copy (verbatim from the old `COPY.fr` block, kept identical so the page reads the same in FR). - `app.routes.ts` declares the single canonical route and keeps `/accessibilite` as a `redirectTo: 'accessibility'` for backward compatibility with existing bookmarks. Drops the `data.lang` payload — no longer consumed. - `footer.html` collapses the dual link (`Accessibility` / `Accessibilité`) into one i18n-marked link (`@@footer.accessibilityLink`). EN bundle reads "Accessibility statement"; FR bundle reads "Déclaration d'accessibilité". The path stays in English across both locales for now (`/{en,fr}/accessibility`). Translating route segments (`/fr/declaration-d-accessibilite`) is a future refinement that needs either a custom URL serializer or per-locale route trees — not worth the complexity at this scale. Specs: - `accessibility.spec.ts` loses the `lang`-based tests and asserts on the rendered EN source content + the amber-panel structure (specs run in the source locale). - `footer.spec.ts` asserts on the single accessibility link and the absence of the historical `/accessibilite` href. Test plan / verification: - 35/35 specs pass (was 36; one obsolete `lang` fallback test removed). - Production build emits both locales; FR chunk contains "Statut", "Déclaration", FR intro/panel bodies, and no leftover English from the swept strings. --- apps/portal-shell/src/app/app.routes.ts | 22 +++--- .../src/app/components/footer/footer.html | 25 ++----- .../src/app/components/footer/footer.spec.ts | 10 ++- .../pages/accessibility/accessibility.html | 43 ++++++++--- .../pages/accessibility/accessibility.spec.ts | 38 ++++------ .../app/pages/accessibility/accessibility.ts | 75 +++---------------- apps/portal-shell/src/locale/messages.fr.xlf | 26 +++++++ 7 files changed, 105 insertions(+), 134 deletions(-) diff --git a/apps/portal-shell/src/app/app.routes.ts b/apps/portal-shell/src/app/app.routes.ts index 03db77f..ef48a31 100644 --- a/apps/portal-shell/src/app/app.routes.ts +++ b/apps/portal-shell/src/app/app.routes.ts @@ -1,9 +1,9 @@ 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. +// bundle ships its own. The accessibility statement now lives at the +// single canonical path `/accessibility` (per ADR-0019, replacing the +// previous `/accessibility` + `/accessibilite` pair). const homeTitle = $localize`:@@route.home.title:APF Portal`; const accessibilityTitle = $localize`:@@route.accessibility.title:Accessibility statement · APF Portal`; @@ -14,21 +14,21 @@ export const appRoutes: Route[] = [ loadComponent: () => import('./pages/home/home').then((m) => m.Home), title: homeTitle, }, - // RGAA + ADR-0016: the accessibility statement must be reachable - // from both locales' canonical paths. Same component, content - // driven by the `lang` route data property. + // RGAA + ADR-0016: the accessibility statement is reachable from + // `/accessibility` in every locale. Content is i18n-marked so the + // per-locale bundle ships the right copy automatically. { path: 'accessibility', loadComponent: () => import('./pages/accessibility/accessibility').then((m) => m.AccessibilityStatement), - data: { lang: 'en' }, title: accessibilityTitle, }, + // Backward-compat: the historical French path redirects to the + // canonical one. Bookmarks and existing inbound links keep working. + // Drops out of the codebase once analytics confirm no traffic. { path: 'accessibilite', - loadComponent: () => - import('./pages/accessibility/accessibility').then((m) => m.AccessibilityStatement), - data: { lang: 'fr' }, - title: accessibilityTitle, + redirectTo: 'accessibility', + pathMatch: 'full', }, ]; diff --git a/apps/portal-shell/src/app/components/footer/footer.html b/apps/portal-shell/src/app/components/footer/footer.html index b7f8081..22674c2 100644 --- a/apps/portal-shell/src/app/components/footer/footer.html +++ b/apps/portal-shell/src/app/components/footer/footer.html @@ -6,24 +6,11 @@

© {{ year }} APF France handicap

diff --git a/apps/portal-shell/src/app/components/footer/footer.spec.ts b/apps/portal-shell/src/app/components/footer/footer.spec.ts index 1080fe5..6f13e09 100644 --- a/apps/portal-shell/src/app/components/footer/footer.spec.ts +++ b/apps/portal-shell/src/app/components/footer/footer.spec.ts @@ -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(); }); }); diff --git a/apps/portal-shell/src/app/pages/accessibility/accessibility.html b/apps/portal-shell/src/app/pages/accessibility/accessibility.html index 765c498..d48cc0b 100644 --- a/apps/portal-shell/src/app/pages/accessibility/accessibility.html +++ b/apps/portal-shell/src/app/pages/accessibility/accessibility.html @@ -1,14 +1,26 @@ -
-

- {{ copy().pageTitle }} +
+

+ Accessibility statement

-

- {{ copy().introTitle }} +

+ About this statement

-

- {{ copy().introBody }} +

+ The APF Portal is committed to making its digital services accessible, in line with WCAG 2.2 + level AA and the French RGAA 4.1 reference, with targeted AAA criteria on the items most + impactful to the association's user base (see ADR-0016).

@@ -16,11 +28,20 @@ class="mt-8 rounded-md border-l-4 border-amber-400 bg-amber-50 p-4 dark:border-amber-500 dark:bg-amber-950/40" aria-labelledby="status-heading" > -

- {{ copy().panelNoticeTitle }} +

+ Status of this document

-

- {{ copy().panelNoticeBody }} +

+ This statement is awaiting review and sign-off by the APF France handicap user panel. The + final version will integrate the RGAA audit grid, the list of any identified gaps, the + remediation plan, and the contact details for reporting accessibility issues.

diff --git a/apps/portal-shell/src/app/pages/accessibility/accessibility.spec.ts b/apps/portal-shell/src/app/pages/accessibility/accessibility.spec.ts index c958411..f2c4549 100644 --- a/apps/portal-shell/src/app/pages/accessibility/accessibility.spec.ts +++ b/apps/portal-shell/src/app/pages/accessibility/accessibility.spec.ts @@ -1,40 +1,28 @@ import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { of } from 'rxjs'; import { AccessibilityStatement } from './accessibility'; -function configureWithLang(lang: 'fr' | 'en' | undefined) { - return TestBed.configureTestingModule({ - imports: [AccessibilityStatement], - providers: [{ provide: ActivatedRoute, useValue: { data: of({ lang }) } }], - }).compileComponents(); -} - describe('AccessibilityStatement', () => { - it('renders the French copy on /accessibilite (lang: fr)', async () => { - await configureWithLang('fr'); + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [AccessibilityStatement] }).compileComponents(); + }); + + it('renders the heading + intro + status sections', async () => { const fixture = TestBed.createComponent(AccessibilityStatement); await fixture.whenStable(); const root = fixture.nativeElement as HTMLElement; - expect(root.querySelector('h1')?.textContent).toContain('Déclaration d’accessibilité'); - expect(root.querySelector('article')?.getAttribute('lang')).toBe('fr'); + expect(root.querySelector('h1')?.textContent?.trim()).toBe('Accessibility statement'); + expect(root.querySelector('section[aria-labelledby="intro-heading"]')).not.toBeNull(); + expect(root.querySelector('section[aria-labelledby="status-heading"]')).not.toBeNull(); }); - it('renders the English copy on /accessibility (lang: en)', async () => { - await configureWithLang('en'); + it('exposes the panel-review status with an amber callout structure', async () => { const fixture = TestBed.createComponent(AccessibilityStatement); await fixture.whenStable(); const root = fixture.nativeElement as HTMLElement; - expect(root.querySelector('h1')?.textContent).toContain('Accessibility statement'); - expect(root.querySelector('article')?.getAttribute('lang')).toBe('en'); - }); - - it('falls back to English when the route omits the lang', async () => { - await configureWithLang(undefined); - const fixture = TestBed.createComponent(AccessibilityStatement); - await fixture.whenStable(); - expect((fixture.nativeElement as HTMLElement).querySelector('h1')?.textContent).toContain( - 'Accessibility statement', + const status = root.querySelector('section[aria-labelledby="status-heading"]'); + expect(status?.querySelector('#status-heading')?.textContent).toContain( + 'Status of this document', ); + expect(status?.textContent).toContain('APF France handicap user panel'); }); }); diff --git a/apps/portal-shell/src/app/pages/accessibility/accessibility.ts b/apps/portal-shell/src/app/pages/accessibility/accessibility.ts index a7ba1fa..9d5895a 100644 --- a/apps/portal-shell/src/app/pages/accessibility/accessibility.ts +++ b/apps/portal-shell/src/app/pages/accessibility/accessibility.ts @@ -1,77 +1,22 @@ -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { map } from 'rxjs'; - -type Lang = 'fr' | 'en'; - -interface AccessibilityCopy { - htmlLang: Lang; - pageTitle: string; - introTitle: string; - introBody: string; - panelNoticeTitle: string; - panelNoticeBody: string; -} - -const COPY: Record = { - fr: { - htmlLang: 'fr', - pageTitle: 'Déclaration d’accessibilité', - introTitle: 'À propos de cette déclaration', - introBody: - 'Le portail APF s’engage à rendre ses services numériques accessibles, conformément ' + - 'au RGAA 4.1 et à la norme WCAG 2.2 niveau AA, avec des critères AAA ciblés sur les ' + - 'points à fort impact pour les usagers de l’association (voir ADR-0016).', - panelNoticeTitle: 'Statut du présent document', - panelNoticeBody: - 'Cette déclaration est en attente de revue et de validation par le panel d’usagers ' + - 'd’APF France handicap. La version définitive intégrera la grille d’audit RGAA, le ' + - 'recensement des écarts éventuels, le plan de mise en conformité, et les coordonnées ' + - 'de signalement.', - }, - en: { - htmlLang: 'en', - pageTitle: 'Accessibility statement', - introTitle: 'About this statement', - introBody: - 'The APF Portal is committed to making its digital services accessible, in line with ' + - 'WCAG 2.2 level AA and the French RGAA 4.1 reference, with targeted AAA criteria on ' + - 'the items most impactful to the association’s user base (see ADR-0016).', - panelNoticeTitle: 'Status of this document', - panelNoticeBody: - 'This statement is awaiting review and sign-off by the APF France handicap user ' + - 'panel. The final version will integrate the RGAA audit grid, the list of any ' + - 'identified gaps, the remediation plan, and the contact details for reporting ' + - 'accessibility issues.', - }, -}; +import { ChangeDetectionStrategy, Component } from '@angular/core'; /** - * Accessibility statement, mounted at both `/accessibility` (en) and - * `/accessibilite` (fr) per ADR-0016. Same component, content - * selected from a route data property. + * Accessibility statement, mounted at `/accessibility` per ADR-0019. + * + * Content is marked for i18n; the production build emits one bundle + * per locale with the right text inlined. The previous twin route + * `/accessibilite` survives as a redirect (in `app.routes.ts`) so + * existing bookmarks keep working. * * v1 ships placeholder copy explicitly framed as "awaiting APF panel * review" — content is required to exist (RGAA + ADR-0016) but the * substance of the statement (audit grid, gaps list, remediation - * plan) is owned by the accessibility review and lands separately. + * plan, reporting channel) is owned by the accessibility review and + * lands separately. */ @Component({ selector: 'app-accessibility-statement', templateUrl: './accessibility.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AccessibilityStatement { - private readonly route = inject(ActivatedRoute); - - protected readonly lang = toSignal( - this.route.data.pipe(map((data): Lang => (data['lang'] as Lang | undefined) ?? 'en')), - // `as Lang` rather than `as const` so the toSignal overload - // picks `Signal` instead of `Signal` and downstream - // consumers see `Signal` cleanly. - { initialValue: 'en' as Lang }, - ); - - protected readonly copy = computed(() => COPY[this.lang()]); -} +export class AccessibilityStatement {} diff --git a/apps/portal-shell/src/locale/messages.fr.xlf b/apps/portal-shell/src/locale/messages.fr.xlf index e3c3856..4bbccfd 100644 --- a/apps/portal-shell/src/locale/messages.fr.xlf +++ b/apps/portal-shell/src/locale/messages.fr.xlf @@ -26,6 +26,10 @@ Legal Mentions légales + + Accessibility statement + Déclaration d’accessibilité + © APF France handicap © APF France handicap @@ -163,6 +167,28 @@ Portail APF + + + The APF Portal is committed to making its digital services accessible, in line with WCAG 2.2 level AA and the French RGAA 4.1 reference, with targeted AAA criteria on the items most impactful to the association's user base (see ADR-0016). + Le portail APF s’engage à rendre ses services numériques accessibles, conformément au RGAA 4.1 et à la norme WCAG 2.2 niveau AA, avec des critères AAA ciblés sur les points à fort impact pour les usagers de l’association (voir ADR-0016). + + + About this statement + À propos de cette déclaration + + + This statement is awaiting review and sign-off by the APF France handicap user panel. The final version will integrate the RGAA audit grid, the list of any identified gaps, the remediation plan, and the contact details for reporting accessibility issues. + Cette déclaration est en attente de revue et de validation par le panel d’usagers d’APF France handicap. La version définitive intégrera la grille d’audit RGAA, le recensement des écarts éventuels, le plan de mise en conformité, et les coordonnées de signalement. + + + Status of this document + Statut du présent document + + + Accessibility statement + Déclaration d’accessibilité + + 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. -- 2.30.2