Step 1: i18n foundation (@angular/localize) + data inventory
Domain reference data already lives in the backend (per ADR-0001); the only residual "move" was ~90 inlined Dutch UI strings with no i18n layer. - Wire @angular/localize ($localize) — Angular first-party, no third-party lib - Pin the pattern on shared/ui/async: Dutch fallbacks → language-agnostic input()s with localizable $localize defaults (English-shared-UI rule) - CLAUDE.md: drop i18n (now in scope) + stale "real backend"/"OpenAPI codegen" (both already shipped); document the $localize convention Bulk string sweep deferred to Step 2 to avoid touching ~36 files twice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -122,6 +122,13 @@ not heavy component tests.
|
|||||||
- **Naming:** shared/reusable UI is **English** (language-agnostic: `button`,
|
- **Naming:** shared/reusable UI is **English** (language-agnostic: `button`,
|
||||||
`wizard-shell`); domain contexts are **Dutch** (`registratie`, `herregistratie`,
|
`wizard-shell`); domain contexts are **Dutch** (`registratie`, `herregistratie`,
|
||||||
`*.machine.ts`). Pick the language by which side of the seam the code is on.
|
`*.machine.ts`). Pick the language by which side of the seam the code is on.
|
||||||
|
- **User-facing copy = `$localize`.** Every user-visible string is wrapped in Angular's
|
||||||
|
first-party `$localize` (no third-party i18n lib), with a stable custom id
|
||||||
|
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
|
||||||
|
translation file, not a code change (the seam). Shared/English components must **not**
|
||||||
|
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
|
||||||
|
supplies the text (see `shared/ui/async`). Format-validation messages in
|
||||||
|
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
|
||||||
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
||||||
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
||||||
same shape as the wizards, whether it's one step or many. Don't hand-roll mutable
|
same shape as the wizards, whether it's one step or many. Don't hand-roll mutable
|
||||||
@@ -148,5 +155,5 @@ dispatch messages). Worked example: the intake wizard (`herregistratie/`).
|
|||||||
|
|
||||||
## Out of scope (POC, don't build unprompted)
|
## Out of scope (POC, don't build unprompted)
|
||||||
|
|
||||||
Real auth/DigiD, real backend, i18n, NgRx, licensed Rijkshuisstijl font/logo,
|
Real auth/DigiD, NgRx, licensed Rijkshuisstijl font/logo,
|
||||||
runtime DTO validation on every endpoint, multi-tab session sync, OpenAPI codegen.
|
runtime DTO validation on every endpoint, multi-tab session sync.
|
||||||
|
|||||||
19
angular.json
19
angular.json
@@ -30,9 +30,8 @@
|
|||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": ["src/styles.scss"],
|
||||||
"src/styles.scss"
|
"polyfills": ["@angular/localize/init"]
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -85,12 +84,7 @@
|
|||||||
"configDir": ".storybook",
|
"configDir": ".storybook",
|
||||||
"browserTarget": "atomic-design-poc:build",
|
"browserTarget": "atomic-design-poc:build",
|
||||||
"compodoc": true,
|
"compodoc": true,
|
||||||
"compodocArgs": [
|
"compodocArgs": ["-e", "json", "-d", "."],
|
||||||
"-e",
|
|
||||||
"json",
|
|
||||||
"-d",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"port": 6006
|
"port": 6006
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -100,12 +94,7 @@
|
|||||||
"configDir": ".storybook",
|
"configDir": ".storybook",
|
||||||
"browserTarget": "atomic-design-poc:build",
|
"browserTarget": "atomic-design-poc:build",
|
||||||
"compodoc": true,
|
"compodoc": true,
|
||||||
"compodocArgs": [
|
"compodocArgs": ["-e", "json", "-d", "."],
|
||||||
"-e",
|
|
||||||
"json",
|
|
||||||
"-d",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"outputDir": "storybook-static"
|
"outputDir": "storybook-static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
119
package-lock.json
generated
119
package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"@angular/build": "^22.0.4",
|
"@angular/build": "^22.0.4",
|
||||||
"@angular/cli": "^22.0.4",
|
"@angular/cli": "^22.0.4",
|
||||||
"@angular/compiler-cli": "^22.0.0",
|
"@angular/compiler-cli": "^22.0.0",
|
||||||
|
"@angular/localize": "^22.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^22.0.0",
|
"@angular/platform-browser-dynamic": "^22.0.0",
|
||||||
"@compodoc/compodoc": "^1.2.1",
|
"@compodoc/compodoc": "^1.2.1",
|
||||||
"@storybook/addon-a11y": "^10.4.6",
|
"@storybook/addon-a11y": "^10.4.6",
|
||||||
@@ -781,6 +782,79 @@
|
|||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/localize": {
|
||||||
|
"version": "22.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-22.0.4.tgz",
|
||||||
|
"integrity": "sha512-BJELtGDcTqPjNn10pUa5Ly0Zv+yOyhNOhMl7OA7izRwgK0bWCqgTRnCFh9OWuvn+KyTLfsd9HKVxSDTmtJYEJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "7.29.7",
|
||||||
|
"@types/babel__core": "7.20.5",
|
||||||
|
"tinyglobby": "^0.2.12",
|
||||||
|
"yargs": "^18.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"localize-extract": "tools/bundles/src/extract/cli.js",
|
||||||
|
"localize-migrate": "tools/bundles/src/migrate/cli.js",
|
||||||
|
"localize-translate": "tools/bundles/src/translate/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/compiler": "22.0.4",
|
||||||
|
"@angular/compiler-cli": "22.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@angular/localize/node_modules/@babel/core": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.7",
|
||||||
|
"@babel/generator": "^7.29.7",
|
||||||
|
"@babel/helper-compilation-targets": "^7.29.7",
|
||||||
|
"@babel/helper-module-transforms": "^7.29.7",
|
||||||
|
"@babel/helpers": "^7.29.7",
|
||||||
|
"@babel/parser": "^7.29.7",
|
||||||
|
"@babel/template": "^7.29.7",
|
||||||
|
"@babel/traverse": "^7.29.7",
|
||||||
|
"@babel/types": "^7.29.7",
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"gensync": "^1.0.0-beta.2",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/babel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@angular/localize/node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@angular/localize/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/platform-browser": {
|
"node_modules/@angular/platform-browser": {
|
||||||
"version": "22.0.2",
|
"version": "22.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.2.tgz",
|
||||||
@@ -8317,6 +8391,51 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/babel__core": {
|
||||||
|
"version": "7.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.20.7",
|
||||||
|
"@babel/types": "^7.20.7",
|
||||||
|
"@types/babel__generator": "*",
|
||||||
|
"@types/babel__template": "*",
|
||||||
|
"@types/babel__traverse": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__generator": {
|
||||||
|
"version": "7.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||||
|
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__template": {
|
||||||
|
"version": "7.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||||
|
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.1.0",
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__traverse": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.28.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"@angular/build": "^22.0.4",
|
"@angular/build": "^22.0.4",
|
||||||
"@angular/cli": "^22.0.4",
|
"@angular/cli": "^22.0.4",
|
||||||
"@angular/compiler-cli": "^22.0.0",
|
"@angular/compiler-cli": "^22.0.0",
|
||||||
|
"@angular/localize": "^22.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^22.0.0",
|
"@angular/platform-browser-dynamic": "^22.0.0",
|
||||||
"@compodoc/compodoc": "^1.2.1",
|
"@compodoc/compodoc": "^1.2.1",
|
||||||
"@storybook/addon-a11y": "^10.4.6",
|
"@storybook/addon-a11y": "^10.4.6",
|
||||||
@@ -58,6 +59,8 @@
|
|||||||
"webpack-dev-server": "^5.2.5",
|
"webpack-dev-server": "^5.2.5",
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"uuid": "^11.1.1",
|
"uuid": "^11.1.1",
|
||||||
"eslint": { "ajv": "^6.12.6" }
|
"eslint": {
|
||||||
|
"ajv": "^6.12.6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ export class AsyncErrorDirective {
|
|||||||
<ng-container [ngTemplateOutlet]="errorTpl()!.tpl"
|
<ng-container [ngTemplateOutlet]="errorTpl()!.tpl"
|
||||||
[ngTemplateOutletContext]="{ $implicit: error(), retry: retry }" />
|
[ngTemplateOutletContext]="{ $implicit: error(), retry: retry }" />
|
||||||
} @else {
|
} @else {
|
||||||
<app-alert type="error">Er ging iets mis bij het laden van de gegevens.</app-alert>
|
<app-alert type="error">{{ errorText() }}</app-alert>
|
||||||
<div style="margin-top:1rem">
|
<div style="margin-top:1rem">
|
||||||
<app-button variant="secondary" (click)="retry()">Opnieuw proberen</app-button>
|
<app-button variant="secondary" (click)="retry()">{{ retryText() }}</app-button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@case ('Empty') {
|
@case ('Empty') {
|
||||||
@if (emptyTpl()) { <ng-container [ngTemplateOutlet]="emptyTpl()!.tpl" /> }
|
@if (emptyTpl()) { <ng-container [ngTemplateOutlet]="emptyTpl()!.tpl" /> }
|
||||||
@else { <p class="rhc-paragraph">Geen gegevens gevonden.</p> }
|
@else { <p class="rhc-paragraph">{{ emptyText() }}</p> }
|
||||||
}
|
}
|
||||||
@case ('Success') {
|
@case ('Success') {
|
||||||
<ng-container [ngTemplateOutlet]="loadedTpl().tpl"
|
<ng-container [ngTemplateOutlet]="loadedTpl().tpl"
|
||||||
@@ -72,6 +72,12 @@ export class AsyncComponent<T> {
|
|||||||
data = input<RemoteData<Error | undefined, T>>();
|
data = input<RemoteData<Error | undefined, T>>();
|
||||||
isEmpty = input<(v: T) => boolean>(() => false);
|
isEmpty = input<(v: T) => boolean>(() => false);
|
||||||
|
|
||||||
|
// Shared/English component: copy lives behind language-agnostic inputs. Defaults are
|
||||||
|
// localizable via $localize (source = default locale, currently nl); callers may override.
|
||||||
|
errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);
|
||||||
|
retryText = input($localize`:@@async.retry:Opnieuw proberen`);
|
||||||
|
emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);
|
||||||
|
|
||||||
loadedTpl = contentChild.required(AsyncLoadedDirective);
|
loadedTpl = contentChild.required(AsyncLoadedDirective);
|
||||||
loadingTpl = contentChild(AsyncLoadingDirective);
|
loadingTpl = contentChild(AsyncLoadingDirective);
|
||||||
emptyTpl = contentChild(AsyncEmptyDirective);
|
emptyTpl = contentChild(AsyncEmptyDirective);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
/// <reference types="@angular/localize" />
|
||||||
|
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { App } from './app/app';
|
import { App } from './app/app';
|
||||||
|
|
||||||
bootstrapApplication(App, appConfig)
|
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||||
.catch((err) => console.error(err));
|
|
||||||
|
|||||||
@@ -4,13 +4,8 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": ["@angular/localize"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.ts"],
|
||||||
"src/**/*.ts"
|
"exclude": ["src/**/*.spec.ts", "src/**/*.stories.ts"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.stories.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,7 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": ["vitest/globals", "@angular/localize"]
|
||||||
"vitest/globals"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
|
||||||
"src/**/*.d.ts",
|
|
||||||
"src/**/*.spec.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user