Skip to content

Generalise the union-into-i18n-key guard: four more interpolation sites render into the bank-facing PDF #2029

Description

@steilerDev

[orchestrator] Raised by product-architect reviewing PR #2028 (#1912). Filed rather than folded in, because the right fix is one small generic rather than N hand-written Record maps — see below.

Problem

#1912 item 1 replaced a template-literal i18n key interpolation in getAttachmentNote with an exhaustive ATTACHMENT_TYPE_KEYS: Record<AttachmentType, string> map, so a fourth AttachmentType member becomes a compile error rather than a raw i18n key printed onto a bank-facing PDF.

That fixed one of five such sites in client/src/lib/reportContent/buildReportContent.ts. The other four all interpolate a union into a reportT() key, and all four render into the same bank-facing PDF:

Line Key Union Members
143 sourceReports.table.title.${useCase} SourceReportType 3
146 sourceReports.sourceType.${report.source.sourceType} BudgetSourceType 5
204 sources.lines.invoiceStatus.${status} InvoiceStatus 4
274 sourceReports.coverLetter.subject.${useCase} SourceReportType 3

Line 204 is the sharpest: it launders its value through const status = invoice.status as InvoiceStatus at line 198 — the exact widening-plus-cast shape #1912 just removed from getAttachmentNote two functions above.

Outside that file, the same AttachmentType union is interpolated at client/src/components/documents/LinkedDocumentsSection.tsx:276, plus roughly a dozen status-union sites app-wide (screen-only, lower stakes).

Why this is latent rather than live

Every one of those key sets was verified complete against both en/budget.json and de/budget.json. Nothing is broken today. The defect arrives when someone adds a union member — which is precisely when nobody is thinking about i18n keys.

A residual gap the Record pattern does not close

Record<AttachmentType, string> enforces union ↔ map parity. It does not enforce map ↔ locale-JSON parity. A fourth member with a map entry but no budget.json key still prints a raw key onto the PDF. The map is also module-private, so no test can iterate it. Whatever generic comes out of this should address that — either by exporting the mapping for a parity test, or by deriving keys in a way the existing i18n.parity.test.ts can see.

Acceptance Criteria

  • 1 A single reusable mechanism — not N hand-written Record maps — makes "union member added without a corresponding i18n key" a compile-time or test-time failure.
  • 2 Applied to the four reportT() sites in buildReportContent.ts (priority: these reach the bank-facing PDF).
  • 3 Applied to LinkedDocumentsSection.tsx:276.
  • 4 The as InvoiceStatus cast at buildReportContent.ts:198 is removed, not preserved around the new mechanism — the cast is the same defect as the key interpolation, one layer down.
  • 5 Map ↔ locale-JSON parity is covered, so a member with a map entry and no translation key fails something.
  • 6 Decide explicitly whether the app-wide screen-only status unions are in scope. They are lower stakes than the PDF path; a deliberate "no" recorded here is a fine answer.

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    tech-debtTechnical debt cleanup work (lint, refactors, etc.)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions