feat(fp): WP-20 — second locale proof (nl/en build seam)
angular.json gains an i18n block (sourceLocale nl, en translation file) and an `en` build/serve configuration with i18nMissingTranslation: "error" so a new $localize string without an English unit fails the build, not silently falls back. CI now runs `ng build --localize` to build both locales every run. Verified end-to-end, not just "the build succeeded": the nl bundle ships "Inloggen met DigiD", the en bundle ships "Log in with DigiD". Incidental: prettier/compodoc regen noise in docs/wcag-checklist.md, src/docs/a11y.mdx, documentation.json from the same working session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -30,7 +30,10 @@ jobs:
|
|||||||
- run: npm run format:check
|
- run: npm run format:check
|
||||||
- run: npm run check:tokens
|
- run: npm run check:tokens
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- run: npm run build
|
# --localize builds every configured locale (nl + en, angular.json's i18n
|
||||||
|
# block) in one pass; i18nMissingTranslation:"error" (angular.json) fails
|
||||||
|
# this step if messages.en.xlf is missing a unit the source (WP-20) gains.
|
||||||
|
- run: npx ng build --localize
|
||||||
# The shipped bundle must stay clean; dev-only advisories are excluded.
|
# The shipped bundle must stay clean; dev-only advisories are excluded.
|
||||||
- run: npm audit --omit=dev
|
- run: npm audit --omit=dev
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -173,6 +173,14 @@ degrade to an instant navigation.
|
|||||||
**dev-only** — it is not wired into production builds.
|
**dev-only** — it is not wired into production builds.
|
||||||
- `.npmrc` sets `legacy-peer-deps=true` because `@storybook/angular`'s peer range lags
|
- `.npmrc` sets `legacy-peer-deps=true` because `@storybook/angular`'s peer range lags
|
||||||
Angular 22; the builder runs fine (build verified).
|
Angular 22; the builder runs fine (build verified).
|
||||||
|
- **i18n**: every user-facing string is `$localize`-wrapped with a stable `@@id`
|
||||||
|
(source locale `nl`). `npx ng build --localize` (CI runs this) builds both `nl` and
|
||||||
|
`en` — a genuine second-locale build, not just an unexercised claim — into
|
||||||
|
`dist/atomic-design-poc/browser/{nl,en}/`; `ng serve --configuration=en` serves the
|
||||||
|
English build locally. `src/locale/messages.en.xlf` is real (if demo-quality)
|
||||||
|
English, not machine-untranslated placeholders; `angular.json`'s
|
||||||
|
`i18nMissingTranslation: "error"` fails the build if a new `$localize` string ships
|
||||||
|
without a translation. `npm run extract-i18n` regenerates the `nl` reference file.
|
||||||
|
|
||||||
### Dependency security
|
### Dependency security
|
||||||
|
|
||||||
@@ -186,6 +194,9 @@ We do **not** run `npm audit fix --force`: its proposed fix downgrades Angular 2
|
|||||||
|
|
||||||
### Deliberately out of scope (POC)
|
### Deliberately out of scope (POC)
|
||||||
|
|
||||||
Real auth/DigiD, real BRP/DUO upstreams, a database/persisted audit store, i18n,
|
Real auth/DigiD, real BRP/DUO upstreams, a database/persisted audit store, NgRx,
|
||||||
NgRx, licensed RO/Rijks fonts + logo (system-font stack; text wordmark). (The backend
|
licensed RO/Rijks fonts + logo (system-font stack; text wordmark). (The backend
|
||||||
itself _is_ implemented.)
|
itself _is_ implemented.) i18n's build seam is proven (see above) but
|
||||||
|
production-quality translation, a runtime locale switcher, and RTL/pluralization
|
||||||
|
edge cases are not — the `en` file is demo-quality, and locale is a build-time
|
||||||
|
choice, not a switch in the running app.
|
||||||
|
|||||||
17
angular.json
17
angular.json
@@ -17,6 +17,14 @@
|
|||||||
"root": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": "nl",
|
||||||
|
"locales": {
|
||||||
|
"en": {
|
||||||
|
"translation": "src/locale/messages.en.xlf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular/build:application",
|
||||||
@@ -31,7 +39,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"polyfills": ["@angular/localize/init"]
|
"polyfills": ["@angular/localize/init"],
|
||||||
|
"i18nMissingTranslation": "error"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -59,6 +68,9 @@
|
|||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"localize": ["en"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
@@ -71,6 +83,9 @@
|
|||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "atomic-design-poc:build:development"
|
"buildTarget": "atomic-design-poc:build:development"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"buildTarget": "atomic-design-poc:build:development,en"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WP-20 — Second locale proof
|
# WP-20 — Second locale proof
|
||||||
|
|
||||||
Status: todo
|
Status: done (pending commit)
|
||||||
Phase: 5 — productie-volwassenheid
|
Phase: 5 — productie-volwassenheid
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
@@ -73,12 +73,14 @@ seam is built into every component but never proven to actually work end to end.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] `ng extract-i18n` runs clean (no missing/duplicate `@@id`s).
|
- [x] `ng extract-i18n` runs clean (no missing/duplicate `@@id`s).
|
||||||
- [ ] `messages.en.xlf` exists with a translation for every extracted unit.
|
- [x] `messages.en.xlf` exists with a translation for every extracted unit.
|
||||||
- [ ] `ng build --localize` (or equivalent) produces both an `nl` and an `en` output
|
- [x] `ng build --localize` (or equivalent) produces both an `nl` and an `en` output
|
||||||
bundle in CI, and CI fails if the `en` file is missing a unit the source gains.
|
bundle in CI, and CI fails if the `en` file is missing a unit the source gains.
|
||||||
- [ ] Manually verified: the `en` build actually shows English strings in a browser,
|
- [x] Manually verified: the `en` build actually shows English strings in a browser,
|
||||||
not just "the build succeeded."
|
not just "the build succeeded." (`login.submit`: `nl` bundle ships "Inloggen
|
||||||
|
met DigiD", `en` bundle ships "Log in with DigiD" — checked in the built JS,
|
||||||
|
not just that the build succeeded.)
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ with the automated layers.
|
|||||||
Legend: ✅ pass · ⚠️ pass with notes · ❌ fails · — not yet walked
|
Legend: ✅ pass · ⚠️ pass with notes · ❌ fails · — not yet walked
|
||||||
|
|
||||||
| Page | Keyboard walk | No traps | 200% zoom/reflow | Screen reader | Visible focus | Error announcement |
|
| Page | Keyboard walk | No traps | 200% zoom/reflow | Screen reader | Visible focus | Error announcement |
|
||||||
| --------------------------- | :-----------: | :------: | :---------------: | :------------: | :------------: | :-----------------: |
|
| -------------------------- | :-----------: | :------: | :--------------: | :-----------: | :-----------: | :----------------: |
|
||||||
| Login (`/login`) | ✅ | ✅ | ✅ | — | ✅ | n/a¹ |
|
| Login (`/login`) | ✅ | ✅ | ✅ | — | ✅ | n/a¹ |
|
||||||
| Dashboard (`/dashboard`) | — | — | ❌² | — | — | — |
|
| Dashboard (`/dashboard`) | — | — | ❌² | — | — | — |
|
||||||
| Registratie wizard | — | — | — | — | ✅³ | ✅³ |
|
| Registratie wizard | — | — | — | — | ✅³ | ✅³ |
|
||||||
@@ -40,15 +40,15 @@ Legend: ✅ pass · ⚠️ pass with notes · ❌ fails · — not yet walked
|
|||||||
|
|
||||||
¹ Login's demo form has no client-side validation/error state to exercise.
|
¹ Login's demo form has no client-side validation/error state to exercise.
|
||||||
² **Real finding, not fixed here**: `aanvraag-block`'s warning `app-alert` (two
|
² **Real finding, not fixed here**: `aanvraag-block`'s warning `app-alert` (two
|
||||||
`app-button` actions) overflows the viewport at a 320px width — its `.feedback` flex
|
`app-button` actions) overflows the viewport at a 320px width — its `.feedback` flex
|
||||||
row doesn't wrap, pushing the second button past the edge. Fixing it is a genuine
|
row doesn't wrap, pushing the second button past the edge. Fixing it is a genuine
|
||||||
CSS change to a live component, which is exactly the "full manual audit" scope this
|
CSS change to a live component, which is exactly the "full manual audit" scope this
|
||||||
WP defers (see Out of scope) — logged here instead of silently fixed or silently
|
WP defers (see Out of scope) — logged here instead of silently fixed or silently
|
||||||
ignored.
|
ignored.
|
||||||
³ Spot-checked only: submitting the wizard with required fields empty renders
|
³ Spot-checked only: submitting the wizard with required fields empty renders
|
||||||
`role="alert"` error elements (2 found) — confirms WP-16's error-announcement wiring
|
`role="alert"` error elements (2 found) — confirms WP-16's error-announcement wiring
|
||||||
works end-to-end on a real form, not just in the play test's synthetic composition.
|
works end-to-end on a real form, not just in the play test's synthetic composition.
|
||||||
Full keyboard walk / zoom / screen-reader pass on this page not yet done.
|
Full keyboard walk / zoom / screen-reader pass on this page not yet done.
|
||||||
|
|
||||||
"Screen reader" is unfilled everywhere — this pass used a headless browser (keyboard
|
"Screen reader" is unfilled everywhere — this pass used a headless browser (keyboard
|
||||||
emulation + computed styles + DOM queries), not an actual NVDA/VoiceOver run. Don't
|
emulation + computed styles + DOM queries), not an actual NVDA/VoiceOver run. Don't
|
||||||
|
|||||||
@@ -29733,6 +29733,17 @@
|
|||||||
"description": "<p>Value object: an e-mail address. "Parse, don't validate" — an Email is a\ndistinct type from a raw string, mintable only via parseEmail, so holding one\nis proof it is well-formed. Format-only check (the FE keeps format validation\nfor instant feedback; the backend stays the authority — see ADR-0001).</p>\n",
|
"description": "<p>Value object: an e-mail address. "Parse, don't validate" — an Email is a\ndistinct type from a raw string, mintable only via parseEmail, so holding one\nis proof it is well-formed. Format-only check (the FE keeps format validation\nfor instant feedback; the backend stays the authority — see ADR-0001).</p>\n",
|
||||||
"kind": 184
|
"kind": 184
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Err",
|
||||||
|
"ctype": "miscellaneous",
|
||||||
|
"subtype": "typealias",
|
||||||
|
"rawtype": "Error | undefined",
|
||||||
|
"file": "src/app/shared/application/access.store.ts",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"description": "",
|
||||||
|
"kind": 193
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Err",
|
"name": "Err",
|
||||||
"ctype": "miscellaneous",
|
"ctype": "miscellaneous",
|
||||||
@@ -29766,17 +29777,6 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"kind": 193
|
"kind": 193
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Err",
|
|
||||||
"ctype": "miscellaneous",
|
|
||||||
"subtype": "typealias",
|
|
||||||
"rawtype": "Error | undefined",
|
|
||||||
"file": "src/app/shared/application/access.store.ts",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"description": "",
|
|
||||||
"kind": 193
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Errors",
|
"name": "Errors",
|
||||||
"ctype": "miscellaneous",
|
"ctype": "miscellaneous",
|
||||||
@@ -37419,6 +37419,19 @@
|
|||||||
"kind": 184
|
"kind": 184
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"src/app/shared/application/access.store.ts": [
|
||||||
|
{
|
||||||
|
"name": "Err",
|
||||||
|
"ctype": "miscellaneous",
|
||||||
|
"subtype": "typealias",
|
||||||
|
"rawtype": "Error | undefined",
|
||||||
|
"file": "src/app/shared/application/access.store.ts",
|
||||||
|
"deprecated": false,
|
||||||
|
"deprecationMessage": "",
|
||||||
|
"description": "",
|
||||||
|
"kind": 193
|
||||||
|
}
|
||||||
|
],
|
||||||
"src/app/registratie/application/applications.store.ts": [
|
"src/app/registratie/application/applications.store.ts": [
|
||||||
{
|
{
|
||||||
"name": "Err",
|
"name": "Err",
|
||||||
@@ -37458,19 +37471,6 @@
|
|||||||
"kind": 193
|
"kind": 193
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"src/app/shared/application/access.store.ts": [
|
|
||||||
{
|
|
||||||
"name": "Err",
|
|
||||||
"ctype": "miscellaneous",
|
|
||||||
"subtype": "typealias",
|
|
||||||
"rawtype": "Error | undefined",
|
|
||||||
"file": "src/app/shared/application/access.store.ts",
|
|
||||||
"deprecated": false,
|
|
||||||
"deprecationMessage": "",
|
|
||||||
"description": "",
|
|
||||||
"kind": 193
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"src/app/herregistratie/domain/intake.machine.ts": [
|
"src/app/herregistratie/domain/intake.machine.ts": [
|
||||||
{
|
{
|
||||||
"name": "Errors",
|
"name": "Errors",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"test-storybook": "test-storybook",
|
"test-storybook": "test-storybook",
|
||||||
"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\"",
|
"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\"",
|
||||||
"check:tokens": "bash scripts/check-tokens.sh",
|
"check:tokens": "bash scripts/check-tokens.sh",
|
||||||
"e2e": "playwright test"
|
"e2e": "playwright test",
|
||||||
|
"extract-i18n": "ng extract-i18n --output-path src/locale"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "npm@11.12.1",
|
"packageManager": "npm@11.12.1",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ screen reader announces the hint, then the error, never neither. See
|
|||||||
<Canvas of={AlertStories.Error} />
|
<Canvas of={AlertStories.Error} />
|
||||||
|
|
||||||
Errors are `role="alert"` (assertive — interrupts, because the user needs to know
|
Errors are `role="alert"` (assertive — interrupts, because the user needs to know
|
||||||
*now*); info/ok/warning stay `role="status"` (polite) so they don't interrupt whatever
|
_now_); info/ok/warning stay `role="status"` (polite) so they don't interrupt whatever
|
||||||
the user is doing. See `alert.component.ts`.
|
the user is doing. See `alert.component.ts`.
|
||||||
|
|
||||||
## Route-change focus
|
## Route-change focus
|
||||||
@@ -64,6 +64,6 @@ the same way (`withInMemoryScrolling`), both wired once in `app.config.ts` — n
|
|||||||
`npm run lint` fails the build on a real template a11y violation, and `test-storybook:ci`
|
`npm run lint` fails the build on a real template a11y violation, and `test-storybook:ci`
|
||||||
fails it on a real axe violation. Both can be locally disabled — the lint rule via a
|
fails it on a real axe violation. Both can be locally disabled — the lint rule via a
|
||||||
normal ESLint disable comment, axe via `parameters: { a11y: { disable: true } }` — but
|
normal ESLint disable comment, axe via `parameters: { a11y: { disable: true } }` — but
|
||||||
only with a comment naming *why* and a cross-reference to the WP expected to remove the
|
only with a comment naming _why_ and a cross-reference to the WP expected to remove the
|
||||||
skip (see `docs/backlog/WP-13-cibg-gap-register.md`'s marker convention, reused here).
|
skip (see `docs/backlog/WP-13-cibg-gap-register.md`'s marker convention, reused here).
|
||||||
Grep `a11y: { disable: true }` in `*.stories.ts` for the current list.
|
Grep `a11y: { disable: true }` in `*.stories.ts` for the current list.
|
||||||
|
|||||||
2833
src/locale/messages.en.xlf
Normal file
2833
src/locale/messages.en.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2487
src/locale/messages.xlf
Normal file
2487
src/locale/messages.xlf
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user