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:
2026-06-27 14:00:10 +02:00
parent 1c65025fef
commit 474c040410
36 changed files with 193 additions and 181 deletions

View File

@@ -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 { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
@@ -31,8 +31,10 @@ import { submitRegistratie } from '@registratie/application/submit-registratie';
import { ApiClient } from '@shared/infrastructure/api-client';
const STORAGE_KEY = 'registratie-v1'; // ponytail: bump the suffix if the persisted shape changes; no migration.
const KANALEN = [{ value: 'email', label: 'E-mail' }, { value: 'post', label: 'Post' }];
const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
const KANALEN = [
{ value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },
{ value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },
];
const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
@@ -58,7 +60,7 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
[canGoBack]="cursor() > 0"
[errors]="errorList()"
[errorMessage]="errorMessage()"
submittingLabel="Uw registratie wordt verwerkt…"
i18n-submittingLabel="@@regWizard.submitting" submittingLabel="Uw registratie wordt verwerkt…"
(primary)="onPrimary()"
(back)="dispatch({ tag: 'Back' })"
(cancel)="restart()"
@@ -71,26 +73,26 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
} @else {
@switch (adresStatus()) {
@case ('gevonden') {
<app-alert type="info">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
<app-alert type="info" i18n="@@regWizard.brpGevonden">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
}
@case ('geen') {
<app-alert type="warning">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
<app-alert type="warning" i18n="@@regWizard.brpGeen">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
}
@case ('fout') {
<app-alert type="warning">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
<app-alert type="warning" i18n="@@regWizard.brpFout">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
}
}
<app-address-fields
[value]="{ straat: draft().straat ?? '', postcode: draft().postcode ?? '', woonplaats: draft().woonplaats ?? '' }"
[errors]="{ straat: err('straat'), postcode: err('postcode'), woonplaats: err('woonplaats') }"
(fieldChange)="set($event.key, $event.value)" />
<app-form-field label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" required [error]="err('correspondentie')">
<app-form-field i18n-label="@@regWizard.correspondentieLabel" label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" required [error]="err('correspondentie')">
<app-radio-group name="correspondentie" [options]="kanalen" [invalid]="!!err('correspondentie')"
[ngModel]="draft().correspondentie ?? ''" (ngModelChange)="setKanaal($event)" />
</app-form-field>
@if (draft().correspondentie === 'email') {
<app-form-field label="E-mailadres" fieldId="email" required [error]="err('email')">
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" placeholder="naam@voorbeeld.nl" />
<app-form-field i18n-label="@@regWizard.emailLabel" label="E-mailadres" fieldId="email" required [error]="err('email')">
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" i18n-placeholder="@@regWizard.emailPlaceholder" placeholder="naam@voorbeeld.nl" />
</app-form-field>
}
}
@@ -98,20 +100,20 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
@case ('beroep') {
<app-async [data]="lookupRd()">
<ng-template appAsyncLoaded let-data>
<app-form-field label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" required [error]="err('diploma')">
<app-form-field i18n-label="@@regWizard.diplomaLabel" label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" required [error]="err('diploma')">
<app-radio-group name="diploma" [options]="diplomaOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="diplomaKeuze()" (ngModelChange)="onDiplomaKeuze($any(data), $event)" />
</app-form-field>
@if (handmatigActief()) {
<app-alert type="warning">Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.</app-alert>
<app-form-field label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')">
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing">Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.</app-alert>
<app-form-field i18n-label="@@regWizard.beroepLabel" label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')">
<app-radio-group name="hm-beroep" [options]="beroepOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" />
</app-form-field>
} @else if (draft().beroep) {
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''" />
<app-data-row i18n-key="@@regWizard.beroepAfgeleid" key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''" />
</dl>
}
@@ -133,31 +135,31 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
</app-async>
}
@case ('controle') {
<app-alert type="info">Controleer uw gegevens en dien de registratie in.</app-alert>
<app-alert type="info" i18n="@@regWizard.controleer">Controleer uw gegevens en dien de registratie in.</app-alert>
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Adres" [value]="adresSamenvatting()" />
<app-data-row key="Herkomst adres" [value]="adresHerkomstLabel()" />
<app-data-row key="Correspondentie" [value]="correspondentieLabel()" />
<app-data-row i18n-key="@@regWizard.summary.adres" key="Adres" [value]="adresSamenvatting()" />
<app-data-row i18n-key="@@regWizard.summary.herkomstAdres" key="Herkomst adres" [value]="adresHerkomstLabel()" />
<app-data-row i18n-key="@@regWizard.summary.correspondentie" key="Correspondentie" [value]="correspondentieLabel()" />
@if (draft().correspondentie === 'email') {
<app-data-row key="E-mailadres" [value]="draft().email ?? ''" />
<app-data-row i18n-key="@@regWizard.summary.email" key="E-mailadres" [value]="draft().email ?? ''" />
}
<app-data-row key="Beroep" [value]="draft().beroep ?? ''" />
<app-data-row key="Herkomst diploma" [value]="diplomaHerkomstLabel()" />
<app-data-row i18n-key="@@regWizard.summary.beroep" key="Beroep" [value]="draft().beroep ?? ''" />
<app-data-row i18n-key="@@regWizard.summary.herkomstDiploma" key="Herkomst diploma" [value]="diplomaHerkomstLabel()" />
@for (item of samenvattingVragen(); track item.vraag) {
<app-data-row [key]="item.vraag" [value]="item.antwoord" />
}
</dl>
<div class="app-button-row app-section">
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 0 })">Adres wijzigen</app-button>
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 1 })">Diploma wijzigen</app-button>
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 0 })" i18n="@@regWizard.adresWijzigen">Adres wijzigen</app-button>
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 1 })" i18n="@@regWizard.diplomaWijzigen">Diploma wijzigen</app-button>
</div>
}
}
<div wizardSuccess>
<app-alert type="ok">Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</app-alert>
<app-alert type="ok" i18n="@@regWizard.success">Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="restart()">Nieuwe registratie starten</app-button>
<app-button variant="secondary" (click)="restart()" i18n="@@regWizard.nieuweRegistratie">Nieuwe registratie starten</app-button>
</div>
</div>
</app-wizard-shell>
@@ -176,8 +178,8 @@ export class RegistratieWizardComponent {
seed = input<RegistratieState>(initial);
readonly kanalen = KANALEN;
readonly stepLabels = ['Adres', 'Beroep', 'Controle']; // short labels for the stepper
private stepTitles = ['Adres en correspondentievoorkeur', 'Beroep op basis van uw diploma', 'Controleren en indienen'];
readonly stepLabels = [$localize`:@@regWizard.step.adres:Adres`, $localize`:@@regWizard.step.beroep:Beroep`, $localize`:@@regWizard.step.controle:Controle`]; // short labels for the stepper
private stepTitles = [$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`, $localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`, $localize`:@@regWizard.title.controle:Controleren en indienen`];
readonly state = this.store.model;
readonly dispatch = this.store.dispatch;
@@ -190,8 +192,8 @@ export class RegistratieWizardComponent {
protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');
// --- Presentational wiring for the shared wizard shell ---------------------
protected primaryLabel = computed(() => (this.step() === 'controle' ? 'Registratie indienen' : 'Volgende'));
protected errorMessage = computed(() => `Het indienen is niet gelukt: ${this.failedError()}`);
protected primaryLabel = computed(() => (this.step() === 'controle' ? $localize`:@@regWizard.indienen:Registratie indienen` : $localize`:@@wizard.volgende:Volgende`));
protected errorMessage = computed(() => $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Invullen': return 'editing';
@@ -217,9 +219,9 @@ export class RegistratieWizardComponent {
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');
});
// Readable labels for the controle summary (instead of raw enum values).
protected adresHerkomstLabel = computed(() => ({ brp: 'Automatisch uit de BRP', handmatig: 'Handmatig ingevoerd' }[this.draft().adresHerkomst ?? 'handmatig']));
protected correspondentieLabel = computed(() => ({ email: 'Per e-mail', post: 'Per post' }[this.draft().correspondentie ?? 'post']));
protected diplomaHerkomstLabel = computed(() => ({ duo: 'Geverifieerd via DUO', handmatig: 'Handmatig ingevoerd (wordt beoordeeld)' }[this.draft().diplomaHerkomst ?? 'handmatig']));
protected adresHerkomstLabel = computed(() => ({ brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`, handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd` }[this.draft().adresHerkomst ?? 'handmatig']));
protected correspondentieLabel = computed(() => ({ email: $localize`:@@regWizard.corr.email:Per e-mail`, post: $localize`:@@regWizard.corr.post:Per post` }[this.draft().correspondentie ?? 'post']));
protected diplomaHerkomstLabel = computed(() => ({ duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`, handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)` }[this.draft().diplomaHerkomst ?? 'handmatig']));
/** BRP lookup outcome. A failure or "geen adres" never blocks the wizard — both
fall back to manual entry (PRD §7). 'geen' covers both no-address-found and a
@@ -262,7 +264,7 @@ export class RegistratieWizardComponent {
protected diplomaOptions = (data: DuoLookupDto) => [
...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam}${d.instelling} (${d.jaar})` })),
{ value: HANDMATIG, label: 'Mijn diploma staat er niet bij' },
{ value: HANDMATIG, label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij` },
];
protected beroepOptions = (data: DuoLookupDto) => data.handmatig.beroepen.map((b) => ({ value: b, label: b }));