docs(decisions): add ADR-0019 i18n + ADR-0020 portal-admin #89

Merged
julien merged 1 commits from docs/adr-i18n-and-portal-admin into main 2026-05-11 12:29:55 +02:00
Owner

Summary

Pure documentation PR. Two ADRs that answer the two strategic questions raised after the footer chantier:

  • ADR-0019 — how the portal handles multiple languages.
  • ADR-0020 — where portal administration lives.

Implementation will land across follow-up feature PRs, each consumable on its own.

ADR-0019 — Internationalisation

Decision: @angular/localize in build-time mode, two locales (fr default served at /, en source). Path-based URLs always prefixed (/fr/..., /en/...); / smart-redirects via cookie → Accept-Languagefr. The locale switcher in the footer writes a __Host-portal_locale cookie and hard-refreshes to the matching bundle.

Considered and rejected:

  • @angular/localize runtime mode (single bundle, higher LCP / payload cost).
  • @ngx-translate / transloco (community libraries; tech-bar prefers Angular first-party for foundational primitives).
  • Query-param URL strategy (fragile, weaker SEO, <html lang> becomes harder).
  • Subdomain URL strategy (breaks __Host- cookie scoping from ADR-0010).

Scope boundary: UI strings owned by developers (templates + $localize in code). Editorial content (CMS-managed pages, news, etc.) is BFF-served already localised — that's the admin-app pipeline (ADR-0020), not @angular/localize.

First sweep consequence: the duplicate /accessibility + /accessibilite routes collapse to one Angular route with locale-translated paths.

ADR-0020 — portal-admin

Decision: new Angular SPA portal-admin alongside portal-shell, sharing the existing portal-bff via /api/admin/* routes guarded by an Entra admin role plus @RequireMfa({ freshness: 600 }) at the entry route. Distinct origin + cookie + session (__Host-portal_admin_session).

v1 modules (all four selected):

  1. Editorial pages CMS (multilingual content, fed back to portal-shell via the BFF).
  2. Sidebar menu management (activates the requiredPermissions field already on MenuItem).
  3. User list (read-only).
  4. Audit log viewer (consumes the audit.events table per ADR-0013, via the audit_reader role).

Out of v1: B2B invitations (stay in Entra Admin Center), feature flags (no substrate yet), CMS workflow / approval flows, theme customisation, live preview.

Considered and rejected:

  • /admin/* lazy-loaded inside portal-shell (admin code in the same origin → weaker defense in depth, admin URL not IP-restrictable independently).
  • Two SPAs and two BFFs (doubles infra at our scale — bricolage).
  • Off-the-shelf admin tooling (Retool, etc. — escapes our security baseline).

Performance budget for admin: ≤ 500 KB gzip initial (vs 300 KB for portal-shell, per ADR-0017). Lighthouse Performance ≥ 85 on critical admin routes (vs ≥ 90 on portal-shell). Same a11y baseline (ADR-0016), same dark-mode support.

Shared-libs graduation: Icon, LayoutStateService, brand tokens, dark-mode SCSS helpers move from portal-shell to libs/shared/{ui,state} when both apps need them. Mechanical refactor; tracked as the first implementation PR.

Implementation roadmap (out of scope of this PR)

ADR-0019:

  1. Install @angular/localize, wire build target.
  2. Mark every existing UI string in portal-shell with i18n + @@id; produce messages.fr.xlf.
  3. Locale switcher in footer + /api/preferences/locale BFF route + smart redirect at /.
  4. Collapse the duplicate accessibility routes into a localised single route, with 301s.
  5. CI gate: nx build portal-shell --localize is added to ci:check and fails on missing translation.

ADR-0020:

  1. nx g @nx/angular:app portal-admin skeleton.
  2. Shared-libs extraction (libs/shared/ui, libs/shared/state).
  3. BFF AdminModule + AdminRoleGuard + smoke GET /api/admin/me.
  4. Admin shell (header / sidebar / footer with an "Admin" badge).
  5. One PR per v1 module — suggested order: CMS pages → menu management → audit viewer → user list.

Test plan

  • Both ADRs follow MADR 4.0.0 (frontmatter, sections, tags from the canonical vocabulary).
  • docs/decisions/README.md index updated in the same commit.
  • CLAUDE.md architecture summary picks up entries for both decisions and bumps the ADR coverage line to 0020.
  • Read-through review — invite the project lead to push back on any decision before locking implementation.
## Summary Pure documentation PR. Two ADRs that answer the two strategic questions raised after the footer chantier: - **[ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md)** — how the portal handles multiple languages. - **[ADR-0020](docs/decisions/0020-portal-admin-app.md)** — where portal administration lives. Implementation will land across follow-up feature PRs, each consumable on its own. ## ADR-0019 — Internationalisation **Decision:** `@angular/localize` in **build-time** mode, two locales (`fr` default served at `/`, `en` source). Path-based URLs always prefixed (`/fr/...`, `/en/...`); `/` smart-redirects via cookie → `Accept-Language` → `fr`. The locale switcher in the footer writes a `__Host-portal_locale` cookie and hard-refreshes to the matching bundle. **Considered and rejected:** - `@angular/localize` runtime mode (single bundle, higher LCP / payload cost). - `@ngx-translate` / `transloco` (community libraries; tech-bar prefers Angular first-party for foundational primitives). - Query-param URL strategy (fragile, weaker SEO, `<html lang>` becomes harder). - Subdomain URL strategy (breaks `__Host-` cookie scoping from ADR-0010). **Scope boundary:** UI strings owned by developers (templates + `$localize` in code). Editorial content (CMS-managed pages, news, etc.) is BFF-served already localised — that's the admin-app pipeline (ADR-0020), not `@angular/localize`. **First sweep consequence:** the duplicate `/accessibility` + `/accessibilite` routes collapse to one Angular route with locale-translated paths. ## ADR-0020 — `portal-admin` **Decision:** new Angular SPA `portal-admin` alongside `portal-shell`, sharing the existing `portal-bff` via `/api/admin/*` routes guarded by an Entra `admin` role plus `@RequireMfa({ freshness: 600 })` at the entry route. Distinct origin + cookie + session (`__Host-portal_admin_session`). **v1 modules** (all four selected): 1. Editorial pages CMS (multilingual content, fed back to `portal-shell` via the BFF). 2. Sidebar menu management (activates the `requiredPermissions` field already on `MenuItem`). 3. User list (read-only). 4. Audit log viewer (consumes the `audit.events` table per ADR-0013, via the `audit_reader` role). **Out of v1:** B2B invitations (stay in Entra Admin Center), feature flags (no substrate yet), CMS workflow / approval flows, theme customisation, live preview. **Considered and rejected:** - `/admin/*` lazy-loaded inside `portal-shell` (admin code in the same origin → weaker defense in depth, admin URL not IP-restrictable independently). - Two SPAs **and** two BFFs (doubles infra at our scale — bricolage). - Off-the-shelf admin tooling (Retool, etc. — escapes our security baseline). **Performance budget for admin:** ≤ 500 KB gzip initial (vs 300 KB for `portal-shell`, per ADR-0017). Lighthouse Performance ≥ 85 on critical admin routes (vs ≥ 90 on `portal-shell`). Same a11y baseline (ADR-0016), same dark-mode support. **Shared-libs graduation:** `Icon`, `LayoutStateService`, brand tokens, dark-mode SCSS helpers move from `portal-shell` to `libs/shared/{ui,state}` when both apps need them. Mechanical refactor; tracked as the first implementation PR. ## Implementation roadmap (out of scope of this PR) ADR-0019: 1. Install `@angular/localize`, wire build target. 2. Mark every existing UI string in `portal-shell` with `i18n` + `@@id`; produce `messages.fr.xlf`. 3. Locale switcher in footer + `/api/preferences/locale` BFF route + smart redirect at `/`. 4. Collapse the duplicate accessibility routes into a localised single route, with 301s. 5. CI gate: `nx build portal-shell --localize` is added to `ci:check` and fails on missing translation. ADR-0020: 1. `nx g @nx/angular:app portal-admin` skeleton. 2. Shared-libs extraction (`libs/shared/ui`, `libs/shared/state`). 3. BFF `AdminModule` + `AdminRoleGuard` + smoke `GET /api/admin/me`. 4. Admin shell (header / sidebar / footer with an "Admin" badge). 5. One PR per v1 module — suggested order: CMS pages → menu management → audit viewer → user list. ## Test plan - [x] Both ADRs follow MADR 4.0.0 (frontmatter, sections, tags from the canonical vocabulary). - [x] `docs/decisions/README.md` index updated in the same commit. - [x] `CLAUDE.md` architecture summary picks up entries for both decisions and bumps the ADR coverage line to 0020. - [ ] Read-through review — invite the project lead to push back on any decision before locking implementation.
julien added 1 commit 2026-05-11 12:20:23 +02:00
docs(decisions): add ADR-0019 i18n + ADR-0020 portal-admin
CI / scan (pull_request) Successful in 2m26s
CI / check (pull_request) Successful in 2m38s
CI / commits (pull_request) Successful in 2m44s
CI / a11y (pull_request) Successful in 1m17s
CI / perf (pull_request) Successful in 2m17s
161e2ecb1c
ADR-0019 picks `@angular/localize` in build-time mode with two locales
(`fr` default, `en` source). URLs are always prefixed (`/fr/...`,
`/en/...`); `/` smart-redirects via cookie → Accept-Language → fr.
UI strings live in XLIFF; editorial content (CMS-served) is locked to
the admin-app pipeline. The locale switcher in the footer writes a
`__Host-portal_locale` cookie and hard-refreshes to the matching
bundle. The `@angular/localize` runtime mode, ngx-translate, and
transloco alternatives are recorded as considered-and-rejected.

ADR-0020 splits portal administration into a dedicated Angular SPA
(`portal-admin`) sharing the existing `portal-bff` via `/api/admin/*`
routes guarded by an Entra `admin` role plus fresh-MFA at entry. Same
identity / sessions / audit / observability primitives reused, no
infrastructure duplication. v1 ships four modules: CMS for static
multilingual pages, sidebar menu management (activating the
`requiredPermissions` field already on `MenuItem`), read-only user
list, and an audit log viewer. Bundle budget relaxed to 500 KB gzip
(vs 300 KB on `portal-shell`); same a11y + dark-mode baseline.

Together they answer the two questions raised after the footer
chantier: how the multilingual story works, and where the admin
surface lives. Implementation lands across follow-up feature PRs;
this commit is documentation only.

CLAUDE.md picks up summary entries for both decisions and bumps the
ADR coverage line to 0020.
julien merged commit c5e91f240b into main 2026-05-11 12:29:55 +02:00
julien deleted branch docs/adr-i18n-and-portal-admin 2026-05-11 12:29:55 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#89