Step 2 (i18n): $localize sweep + JA_NEE dedup (M3, M4)

Wrap every user-facing Dutch string in Angular's first-party i18n — `i18n`/
`i18n-<attr>` in templates, `$localize` in TS (value-objects, machines, commands,
label constants, shared-component defaults). Source locale stays nl; a second
locale is now a translation file, not a code change.

- M3: ~145 strings localized with stable @@ ids across registratie,
  herregistratie, auth, shared/ui, shared/layout. Skipped: showcase, debug-state,
  scenario interceptor, generated client, specs/stories, raw status enum tags,
  internal parse* diagnostics.
- M4: single shared JA_NEE (localized labels) in radio-group; both wizard copies
  removed.

Gate green: lint, check:tokens, build, test 77/77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-06-27 14:00:10 +02:00
co-authored by Claude Opus 4.8
parent 1c65025fef
commit 474c040410
36 changed files with 193 additions and 181 deletions
@@ -73,9 +73,9 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
const errors: Errors = {};
switch (step) {
case 'buitenland':
if (!a.buitenlandGewerkt) errors.buitenlandGewerkt = 'Maak een keuze.';
if (!a.buitenlandGewerkt) errors.buitenlandGewerkt = $localize`:@@validation.maakKeuze:Maak een keuze.`;
else if (a.buitenlandGewerkt === 'ja') {
if (!a.land || a.land.trim() === '') errors.land = 'Vul een land in.';
if (!a.land || a.land.trim() === '') errors.land = $localize`:@@validation.land:Vul een land in.`;
const u = parseUren(a.buitenlandseUren ?? '');
if (!u.ok) errors.buitenlandseUren = u.error;
}
@@ -83,7 +83,7 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
case 'werk': {
const u = parseUren(a.uren ?? '');
if (!u.ok) errors.uren = u.error;
if (lageUren(a, scholingThreshold) && !a.scholingGevolgd) errors.scholingGevolgd = 'Maak een keuze.';
if (lageUren(a, scholingThreshold) && !a.scholingGevolgd) errors.scholingGevolgd = $localize`:@@validation.maakKeuze:Maak een keuze.`;
// Nascholingspunten are only asked (and required) when scholing was followed.
if (a.scholingGevolgd === 'ja') {
const p = parseUren(a.punten ?? '');
@@ -36,23 +36,23 @@ import { ApiClient } from '@shared/infrastructure/api-client';
(retry)="onRetry()">
@if (step() === 1) {
<app-form-field label="Gewerkte uren (afgelopen 5 jaar)" fieldId="uren" required [error]="errUren()">
<app-form-field i18n-label="@@herregWizard.urenLabel" label="Gewerkte uren (afgelopen 5 jaar)" fieldId="uren" required [error]="errUren()">
<app-text-input inputId="uren" [ngModel]="draft().uren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'uren', value: $event })"
name="uren" [invalid]="!!errUren()" placeholder="bijv. 4160" />
name="uren" [invalid]="!!errUren()" i18n-placeholder="@@herregWizard.urenPlaceholder" placeholder="bijv. 4160" />
</app-form-field>
<app-form-field label="Aantal jaren werkzaam" fieldId="jaren" required [error]="errJaren()">
<app-form-field i18n-label="@@herregWizard.jarenLabel" label="Aantal jaren werkzaam" fieldId="jaren" required [error]="errJaren()">
<app-text-input inputId="jaren" [ngModel]="draft().jaren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'jaren', value: $event })"
name="jaren" [invalid]="!!errJaren()" placeholder="bijv. 5" />
name="jaren" [invalid]="!!errJaren()" i18n-placeholder="@@herregWizard.jarenPlaceholder" placeholder="bijv. 5" />
</app-form-field>
} @else {
<app-form-field label="Behaalde nascholingspunten" fieldId="punten" required [error]="errPunten()">
<app-form-field i18n-label="@@herregWizard.puntenLabel" label="Behaalde nascholingspunten" fieldId="punten" required [error]="errPunten()">
<app-text-input inputId="punten" [ngModel]="draft().punten" (ngModelChange)="dispatch({ tag: 'SetField', key: 'punten', value: $event })"
name="punten" [invalid]="!!errPunten()" placeholder="bijv. 200" />
name="punten" [invalid]="!!errPunten()" i18n-placeholder="@@herregWizard.puntenPlaceholder" placeholder="bijv. 200" />
</app-form-field>
}
<div wizardSuccess>
<app-alert type="ok">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
<app-alert type="ok" i18n="@@herregWizard.success">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
</div>
</app-wizard-shell>
`,
@@ -69,8 +69,8 @@ export class HerregistratieWizardComponent {
protected dispatch = this.store.dispatch;
// Stepper labels + per-step heading titles (presentational only).
readonly stepLabels = ['Werkervaring', 'Nascholing'];
private stepTitles = ['Werkervaring (afgelopen 5 jaar)', 'Nascholing'];
readonly stepLabels = [$localize`:@@herregWizard.step.werkervaring:Werkervaring`, $localize`:@@herregWizard.step.nascholing:Nascholing`];
private stepTitles = [$localize`:@@herregWizard.title.werkervaring:Werkervaring (afgelopen 5 jaar)`, $localize`:@@herregWizard.title.nascholing:Nascholing`];
private editing = computed(() => whenTag(this.state(), 'Editing'));
protected step = computed(() => this.editing()?.step ?? 1);
@@ -82,8 +82,8 @@ export class HerregistratieWizardComponent {
// --- Presentational wiring for the shared wizard shell ---------------------
protected stepTitle = computed(() => this.stepTitles[this.step() - 1]);
protected primaryLabel = computed(() => (this.step() === 1 ? 'Volgende' : 'Herregistratie aanvragen'));
protected errorMessage = computed(() => `Indienen mislukt: ${this.failedError()}`);
protected primaryLabel = computed(() => (this.step() === 1 ? $localize`:@@wizard.volgende:Volgende` : $localize`:@@herregWizard.indienen:Herregistratie aanvragen`));
protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Editing': return 'editing';
@@ -13,18 +13,18 @@ import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie
selector: 'app-herregistratie-page',
imports: [PageShellComponent, AlertComponent, ...ASYNC, HerregistratieWizardComponent],
template: `
<app-page-shell heading="Herregistratie aanvragen" backLink="/dashboard">
<app-page-shell i18n-heading="@@herregistratie.heading" heading="Herregistratie aanvragen" backLink="/dashboard">
<app-async [data]="eligibility()">
<ng-template appAsyncLoaded let-eligible>
@if (eligible) {
<app-alert type="info">
<app-alert type="info" i18n="@@herregistratie.eligible">
Uw huidige registratie verloopt binnenkort. Vraag tijdig herregistratie aan.
</app-alert>
<div class="app-section">
<app-herregistratie-wizard />
</div>
} @else {
<app-alert type="warning">
<app-alert type="warning" i18n="@@herregistratie.notEligible">
Voor uw huidige registratiestatus is herregistratie niet mogelijk.
</app-alert>
}
@@ -2,7 +2,7 @@ import { Component, computed, effect, inject, input, untracked } from '@angular/
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
@@ -26,7 +26,6 @@ import { ApiClient } from '@shared/infrastructure/api-client';
import { IntakePolicyAdapter } from '@herregistratie/infrastructure/intake-policy.adapter';
const STORAGE_KEY = 'intake-v3'; // ponytail: bump the suffix if the persisted state shape changes; no migration.
const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
/** Organism: a BRANCHING intake questionnaire. All state lives in one signal
driven by the pure `reduce` (intake.machine.ts). Which step renders is derived
@@ -53,58 +52,58 @@ const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
@switch (step()) {
@case ('buitenland') {
<app-form-field label="Heeft u de afgelopen 5 jaar buiten Nederland gewerkt?" fieldId="buitenlandGewerkt" required [error]="err('buitenlandGewerkt')">
<app-form-field i18n-label="@@intake.q.buitenland" label="Heeft u de afgelopen 5 jaar buiten Nederland gewerkt?" fieldId="buitenlandGewerkt" required [error]="err('buitenlandGewerkt')">
<app-radio-group name="buitenlandGewerkt" [options]="jaNee"
[ngModel]="answers().buitenlandGewerkt ?? ''" (ngModelChange)="set('buitenlandGewerkt', $event)" />
</app-form-field>
@if (answers().buitenlandGewerkt === 'ja') {
<app-form-field label="In welk land?" fieldId="land" required [error]="err('land')">
<app-text-input inputId="land" [ngModel]="answers().land ?? ''" (ngModelChange)="set('land', $event)" name="land" placeholder="bijv. België" />
<app-form-field i18n-label="@@intake.q.land" label="In welk land?" fieldId="land" required [error]="err('land')">
<app-text-input inputId="land" [ngModel]="answers().land ?? ''" (ngModelChange)="set('land', $event)" name="land" i18n-placeholder="@@intake.q.landPlaceholder" placeholder="bijv. België" />
</app-form-field>
<app-form-field label="Hoeveel uur heeft u daar gewerkt?" fieldId="buitenlandseUren" required [error]="err('buitenlandseUren')">
<app-text-input inputId="buitenlandseUren" [ngModel]="answers().buitenlandseUren ?? ''" (ngModelChange)="set('buitenlandseUren', $event)" name="buitenlandseUren" placeholder="bijv. 800" />
<app-form-field i18n-label="@@intake.q.buitenlandseUren" label="Hoeveel uur heeft u daar gewerkt?" fieldId="buitenlandseUren" required [error]="err('buitenlandseUren')">
<app-text-input inputId="buitenlandseUren" [ngModel]="answers().buitenlandseUren ?? ''" (ngModelChange)="set('buitenlandseUren', $event)" name="buitenlandseUren" i18n-placeholder="@@intake.q.buitenlandseUrenPlaceholder" placeholder="bijv. 800" />
</app-form-field>
}
}
@case ('werk') {
<app-form-field label="Gewerkte uren in Nederland (afgelopen 5 jaar)" fieldId="uren" required [error]="err('uren')">
<app-text-input inputId="uren" [ngModel]="answers().uren ?? ''" (ngModelChange)="set('uren', $event)" name="uren" placeholder="bijv. 4160" />
<app-form-field i18n-label="@@intake.q.urenNl" label="Gewerkte uren in Nederland (afgelopen 5 jaar)" fieldId="uren" required [error]="err('uren')">
<app-text-input inputId="uren" [ngModel]="answers().uren ?? ''" (ngModelChange)="set('uren', $event)" name="uren" i18n-placeholder="@@intake.q.urenNlPlaceholder" placeholder="bijv. 4160" />
</app-form-field>
@if (scholingZichtbaar()) {
<app-form-field label="U werkte relatief weinig uren. Heeft u aanvullende scholing gevolgd?" fieldId="scholingGevolgd" required [error]="err('scholingGevolgd')">
<app-form-field i18n-label="@@intake.q.scholing" label="U werkte relatief weinig uren. Heeft u aanvullende scholing gevolgd?" fieldId="scholingGevolgd" required [error]="err('scholingGevolgd')">
<app-radio-group name="scholingGevolgd" [options]="jaNee"
[ngModel]="answers().scholingGevolgd ?? ''" (ngModelChange)="set('scholingGevolgd', $event)" />
</app-form-field>
}
@if (answers().scholingGevolgd === 'ja') {
<app-form-field label="Behaalde nascholingspunten" fieldId="punten" required [error]="err('punten')">
<app-text-input inputId="punten" [ngModel]="answers().punten ?? ''" (ngModelChange)="set('punten', $event)" name="punten" placeholder="bijv. 200" />
<app-form-field i18n-label="@@intake.q.punten" label="Behaalde nascholingspunten" fieldId="punten" required [error]="err('punten')">
<app-text-input inputId="punten" [ngModel]="answers().punten ?? ''" (ngModelChange)="set('punten', $event)" name="punten" i18n-placeholder="@@intake.q.puntenPlaceholder" placeholder="bijv. 200" />
</app-form-field>
}
}
@case ('review') {
<app-alert type="info">Controleer uw antwoorden en dien de aanvraag in.</app-alert>
<app-alert type="info" i18n="@@intake.review.controleer">Controleer uw antwoorden en dien de aanvraag in.</app-alert>
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Buiten NL gewerkt" [value]="answers().buitenlandGewerkt ?? '—'" />
<app-data-row i18n-key="@@intake.review.buitenNl" key="Buiten NL gewerkt" [value]="answers().buitenlandGewerkt ?? '—'" />
@if (answers().buitenlandGewerkt === 'ja') {
<app-data-row key="Land" [value]="answers().land ?? ''" />
<app-data-row key="Buitenlandse uren" [value]="answers().buitenlandseUren ?? ''" />
<app-data-row i18n-key="@@intake.review.land" key="Land" [value]="answers().land ?? ''" />
<app-data-row i18n-key="@@intake.review.buitenlandseUren" key="Buitenlandse uren" [value]="answers().buitenlandseUren ?? ''" />
}
<app-data-row key="Uren NL" [value]="answers().uren ?? ''" />
<app-data-row i18n-key="@@intake.review.urenNl" key="Uren NL" [value]="answers().uren ?? ''" />
@if (scholingZichtbaar()) {
<app-data-row key="Aanvullende scholing" [value]="answers().scholingGevolgd ?? ''" />
<app-data-row i18n-key="@@intake.review.scholing" key="Aanvullende scholing" [value]="answers().scholingGevolgd ?? ''" />
}
@if (answers().scholingGevolgd === 'ja') {
<app-data-row key="Nascholingspunten" [value]="answers().punten ?? ''" />
<app-data-row i18n-key="@@intake.review.punten" key="Nascholingspunten" [value]="answers().punten ?? ''" />
}
</dl>
}
}
<div wizardSuccess>
<app-alert type="ok">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
<app-alert type="ok" i18n="@@intake.success">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="restart()">Opnieuw beginnen</app-button>
<app-button variant="secondary" (click)="restart()" i18n="@@intake.opnieuw">Opnieuw beginnen</app-button>
</div>
</div>
</app-wizard-shell>
@@ -140,15 +139,15 @@ export class IntakeWizardComponent {
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
// --- Presentational wiring for the shared wizard shell ---------------------
readonly stepLabels = ['Buitenland', 'Werk', 'Controle'];
readonly stepLabels = [$localize`:@@intake.step.buitenland:Buitenland`, $localize`:@@intake.step.werk:Werk`, $localize`:@@intake.step.controle:Controle`];
private stepTitles: Record<StepId, string> = {
buitenland: 'Werken in het buitenland',
werk: 'Werkervaring in Nederland',
review: 'Controleren en indienen',
buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
review: $localize`:@@intake.title.review:Controleren en indienen`,
};
protected stepTitle = computed(() => this.stepTitles[this.step()]);
protected primaryLabel = computed(() => (this.step() === 'review' ? 'Aanvraag indienen' : 'Volgende'));
protected errorMessage = computed(() => `Indienen mislukt: ${this.failedError()}`);
protected primaryLabel = computed(() => (this.step() === 'review' ? $localize`:@@intake.indienen:Aanvraag indienen` : $localize`:@@wizard.volgende:Volgende`));
protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Answering': return 'editing';
+2 -2
View File
@@ -9,8 +9,8 @@ import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-w
selector: 'app-intake-page',
imports: [PageShellComponent, AlertComponent, IntakeWizardComponent],
template: `
<app-page-shell heading="Herregistratie — intake" backLink="/dashboard">
<app-alert type="info">
<app-page-shell i18n-heading="@@intake.heading" heading="Herregistratie — intake" backLink="/dashboard">
<app-alert type="info" i18n="@@intake.intro">
Een paar vragen bepalen welke gegevens we nodig hebben. Afhankelijk van uw
antwoorden verschijnen er extra vragen. Uw antwoorden blijven bewaard als u
de pagina herlaadt.