feat(reports): wizard settings step with report language selection - #1903
Conversation
5-step wizard adds a new Settings step (report language en/de + relocated toggles); the exported PDF now renders fully in the selected language via getFixedT + createFormatters, while the app UI language stays untouched. Also fixes tablet stepper density. Fixes #1899 Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com> Co-Authored-By: Claude translator (Sonnet 4.5) <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com> Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
|
[ux-designer] Design review against my visual spec on #1899 (issuecomment-5141421842). Verdict: APPROVED (posted as a comment — self-review approval is blocked by GitHub for this account: "Can not approve your own pull request") Checked against every section of the spec:
One informational nit, non-blocking: No accessibility or dark-mode issues found. |
… displayed author PR #1903 showed a human GitHub username as author but gh pr review --approve still failed with "Can not approve your own pull request" — record that the check isn't reliably predictable from author.login. Co-Authored-By: Claude ux-designer (Sonnet 4.6) <noreply@anthropic.com>
|
[product-architect] Architecture, code-quality and test-coverage review of the 5-step wizard restructure + report-language plumbing. Verdict: APPROVEThe core architectural claim of this PR holds up under inspection, and the test suite is genuinely strong. Two Medium findings and a few Low/informational items below — none of them break a contract or introduce architectural debt that must be paid before merge, so they're noted for refinement rather than blocking. What I verified as sound1. The
2. The 3. Step renumbering is complete. 4. Component split is clean. 5. Test quality is high, and the Also verified: keys added to both FindingsM1 (Medium) —
|
steilerDev
left a comment
There was a problem hiding this comment.
[product-owner] Requirements review of PR #1903 against Story #1899 acceptance criteria.
Verdict: APPROVED WITH ONE MUST FIX
Posted as a comment rather than --approve because GitHub blocks self-approval on this account ("Can not approve your own pull request") — same constraint the ux-designer hit above. Treat this as a product-owner approval, conditional on the single MUST FIX below. The gap is a default-value seeding issue, not a functional break, so per the verdict matrix it is flagged --comment / MUST FIX rather than --request-changes — it does not need to re-enter the review loop, but it must be fixed before merge.
AC Coverage
1. Wizard gains a fifth step — PASS (4/4)
| AC | Verdict | Evidence |
|---|---|---|
| 1.1 Five steps in order | PASS | steps = use-case, source, invoices, settings, actions → Report Type → Budget Source → Select Invoices → Settings → Preview & Export. ReportWizardPage.test.tsx asserts exactly 5 listitems in the stepper nav; E2E Scenario 8 asserts stepItems count 5; Scenario 12 asserts nth(3)="Settings", nth(4)="Preview & Export". |
| 1.2 Nav/gating/focus consistent across 5 | PASS | Step 4 Back→3, Next→setMaxReachedStep(max, 5) + step 5; step 5 Back→4. stepHeadingsRef indices 0–4 all assigned, focus effect reads currentStep - 1 — consistent. E2E Scenario 9 extended to prove step 5 is a non-interactive element (no <button>) while maxReachedStep is 4. |
| 1.3 Mobile "step X of Y" reports 5 | PASS | Unit tests assert "Step 1 of 5", "Step 4 of 5", "Step 5 of 5"; E2E Scenario 10 updated to 5 dots + toContainText('5'). |
| 1.4 Deep link no regression | PASS | E2E Scenario 7 (?sourceId=) walks through the new Settings step to a ready preview + enabled Download, so the #1879 fix is still exercised end-to-end. |
2. Settings step contents — PASS (6/7), 1 MUST FIX
| AC | Verdict | Evidence |
|---|---|---|
| 2.1 Language picker offering en/de | PASS | Step4Settings.tsx radio group, English/Deutsch. Autonyms deliberately untranslated per the ux-designer spec and the ProfilePage precedent — reviewed, not a missed-translation bug. |
| 2.2 Defaults to current UI locale | MUST FIX | See finding below. |
| 2.3 Two toggles moved, defaults/behaviour preserved | PASS | Moved verbatim — optionRow/optionCheckbox/optionLabel/optionHelper, htmlFor/id pairing, disabled={coverLetterDisabled} and the title disabled-reason hint on both input and label all survive the move. |
| 2.4 Toggles no longer on Step 5 | PASS | Step5Actions.tsx drops 53 lines; unit test asserts step 5 has no radio "English" and no "Include cover letter" label, and step 4 has no Download/Upload/Mark-claimed/iframe. Exactly one place each. |
| 2.5 Selection retained across back/forward | PASS | State lifted to ReportWizardPage (same pattern as attachDocuments). Unit test picks Deutsch → Back to step 3 → Next → still checked. |
| 2.6 App UI locale is NOT changed | PASS | Implemented via i18n.getFixedT(reportLanguage, 'budget') — changeLanguage() is never called. realRender.test.ts asserts the production singleton's i18n.language stays 'en' before and after getFixedT('de', …). E2E Scenario 12 asserts the heading/stepper/buttons stay English after selecting Deutsch. |
| 2.7 Keyboard + screen reader | PASS | role="group" + aria-labelledby="report-language-heading"; implicit <label> wrapping gives each radio an accessible name (tests select by getByRole('radio', { name: 'English' })); native radio arrow-key traversal; native :focus-visible retained with no outline: none. Matches the ux-designer spec §2/§8. |
3. Report renders in the selected language — PASS (8/8)
| AC | Verdict | Evidence |
|---|---|---|
| 3.1 All PDF text in selected language | PASS | reportT threaded as the 6th arg to generateReportPdf. Confirmed overviewPdf.ts / coverLetterPdf.ts / merge.ts import TFunction type-only from i18next and never touch the singleton — every string is injection-resolved. realRender.test.ts's it.each([en, de]) covers the full unmocked pipeline including the #1898 Usage column, attachment notes (incl. _one/_other plurals) and both deposit-footnote wordings. |
| 3.2 Cover letter in selected language | PASS | Same t flows merge.ts → buildCoverLetterContent; covered by the same en/de it.each render. |
| 3.3 Dates via shared formatters | PASS | createFormatters('de-DE' | 'en-US', currency) — no raw Intl/toLocaleDateString() introduced. useFormatters() refactored to delegate to the same factory, so hook and PDF paths cannot drift. |
| 3.4 Currency locale-formatted, code unchanged | PASS | currency comes from useLocale() (server config) and is independent of reportLanguage; only the locale string varies. Test exercises the real closures: formatCurrency(1234.56) contains 1,234.56 and getCurrencySymbol() is € under en-US. |
| 3.5 No English leakage / no raw keys | PASS | The en/de parity renders assert real German copy (Auftragnehmer, PDF herunterladen, 2 Anhänge: Angebot, Rechnung) — a raw key or an English fallback would fail these. |
| 3.6 Filename convention follows the language | PASS (as written) — see note | Filename is ${useCase}-${slug}-${today}.pdf; the Paperless title is the same string without the extension. useCase is the raw enum (claim/budget-overview/proof-of-funds), slug is user data, today is ISO-8601. There are no translated components and no locale-dependent date formatting, so the conditional clause ("any translated components…") is vacuously satisfied and nothing needed to change. I am accepting this as-is deliberately, not by omission — ISO dates in filenames are sortable and filesystem-safe, and localising them to 31.07.2026 would be a usability regression. If we ever translate the use-case token into the filename, this AC becomes live again. |
| 3.7 Wizard chrome stays in UI locale | PASS | Step4Settings receives the page's UI t; E2E Scenario 12 asserts heading, both stepper labels and Back/Next remain English after switching the report to Deutsch. |
| 3.8 Explicit resolution against the selected bundle | PASS | realRender.test.ts's new production-singleton test is exactly the right closer: it proves the app-wide i18n (not an isolated test instance) has both bundles loaded and that getFixedT('de') resolves real German while ambient stays 'en'. This closes the gap every prior test in that file left open. |
4. Step 5 unchanged — PASS (1/1)
Step5Actions.tsx is Step4Options.tsx minus the two toggle rows (70% rename similarity); preview, download, Paperless upload, mark-claimed and the step4Layout two-column grid all carry over untouched. E2E Scenarios 1, 2, 5, 6, 7 and the expansion spec all still reach and exercise the same actions.
Translations — PASS
Three new keys, both locales, full parity:
| Key | en | de |
|---|---|---|
sourceReports.stepper.settings |
Settings | Einstellungen |
sourceReports.settingsStep.languageHeading |
Report language | Berichtssprache |
sourceReports.settingsStep.languageHelper |
Only affects the exported report — your app language stays the same. | Betrifft nur den exportierten Bericht – die Sprache der App bleibt unverändert. |
Helper copy in both locales reinforces AC 2.6 directly, as specified. Every new t() path resolves in en/budget.json and de/budget.json.
Scope — clean
The WizardStepper.module.css tablet-range rule (768–1023px) is in scope: it is a direct consequence of adding a 5th label and was called out in the ux-designer spec §4 as a required fix. Agent-memory updates ride along per project convention. No undocumented behaviour changes. Out-of-scope items (#1900 preview editing, #1901 AI generation, new locales, cross-run persistence) are all correctly absent.
MUST FIX before merge — AC 2.2
reportLanguage is seeded with a one-shot useState initializer:
const [reportLanguage, setReportLanguage] = useState<ResolvedLocale>(resolvedLocale);That initializer runs only on ReportWizardPage's first render, but resolvedLocale is not stable at that moment. LocaleContext.syncWithServer — fired from App.tsx's LocaleServerSync once user resolves — applies the user's server-stored locale preference asynchronously, and clears localStorage when it does. So on every later hard load readStoredPreference() returns 'system' and resolvedLocale starts at the browser-detected locale before flipping to the stored preference.
Consequence: a user whose stored preference is de but whose browser reports a non-German locale — precisely the "my bank corresponds in a different language than the one I use the app in" persona this story is written for — who lands on /budget/reports via hard load or a ?sourceId= deep link gets the picker pre-selected to English while the UI is German, and it never self-corrects, because the value is captured at mount rather than when Step 4 renders. The AC is explicitly worded "when the language picker renders, then it defaults to my current UI locale."
Suggested fix, which preserves AC 2.5 (retention for the wizard run) and AC 2.6 (picker never drives the app locale):
const [reportLanguageOverride, setReportLanguageOverride] = useState<ResolvedLocale | null>(null);
const reportLanguage = reportLanguageOverride ?? resolvedLocale;with setReportLanguageOverride wired to the picker's onChange. Until the user makes an explicit choice the default tracks the UI locale; once they choose, the choice sticks for the run. Worth a unit test that a late resolvedLocale change updates the default but does not clobber an explicit selection.
Note — not blocking
CI: Quality Gates green. E2E Tests (Shard 5/16) is red, but the two failures are tests/invoices/invoices.spec.ts ("Effective Amount" column, Issue #1876) and tests/navigation/dashboard.spec.ts (card re-enable) — both unrelated to the report wizard and consistent with the known pre-existing shard-5 flake already tracked for triage before the next beta → main promotion. Not attributable to this PR and not a beta merge blocker.
Once the AC 2.2 seeding fix lands, this story is Done from a requirements standpoint.
Seeding useState(resolvedLocale) captured the first-render value only; LocaleContext.syncWithServer applies the stored locale asynchronously, so a hard load could pre-select the wrong report language. The default now derives from resolvedLocale until the user explicitly overrides it. Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
|
🎉 This PR is included in version 2.13.0-beta.32 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
getFixedTfor translated strings andcreateFormattersfor locale-aware date/currency/percent formatting — the app's own UI language is never affected.Fixes #1899
Test plan
Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com
Co-Authored-By: Claude frontend-developer (Haiku 4.5) noreply@anthropic.com
Co-Authored-By: Claude translator (Sonnet 4.5) noreply@anthropic.com
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) noreply@anthropic.com
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) noreply@anthropic.com