Skip to content

i18n: catalogParity compares key SETS, so a translation that drifts in MEANING passes every gate #688

Description

@mforce

Found during #678 (the #666/#667 date-filter slice), where it nearly shipped.

The hole

web/src/i18n/catalogParity.test.ts enforces that en, es and tl carry the same key set for every namespace in TRANSLATED_NAMESPACES. That is a real guard and it works — a missing or extra key fails.

It says nothing about what the strings mean. So a translated string can:

  • call a UI element a name that element never uses in that locale, or
  • contradict another string in the same locale about the same thing,

and pass catalogParity, typecheck, the full Vitest suite, CodeQL, and review.

What actually happened

A help-text rewrite in #678 changed the word for the expiry field. Nothing caught it:

locale the UI label (inventory.expiryLabel) what the help text said
en Expiry "expiry" ✓
es Vencimiento "caducidad" in one string, "vencimiento" in another — it disagreed with itself
tl Expiry "pagkaluma" — becoming old / obsolete, which is not an expiry date

A Tagalog user would have read help text naming a field something the field never calls itself. Caught only because the driver happened to grep how the catalog says "expiry" elsewhere — not by any gate.

This is also the class the repo already worries about: help text exists to point at controls, and it silently stopped pointing.

Why the existing guards can't see it

  • catalogParity — key sets only, by construction.
  • i18n.test.ts / enums.test.ts — structure and enum coverage, not prose.
  • badgeCase.test.ts (AGENTS.md: three conventions earned while shipping #651 + #652 #662) — precedent that this repo does pin string content when it matters, but only for casing.
  • Review — three locales rewritten in one pass; a reviewer who does not read Tagalog cannot catch it, and the driver's own summary described the change correctly while the text said something else.

Proposal

A small guard pairing help/glossary strings that name a UI element against that element's own label key, per locale. Concretely: a table of { helpKey, labelKey } pairs, asserting the help string contains the label's term in each locale.

Deliberately narrow to start — the handful of help strings that name a specific labelled control. A broad "every noun must match" check would be unmaintainable and would fight legitimate grammar (Spanish inflects, Tagalog uses different particles).

Open question for whoever picks this up, and it is the crux: the assertion has to survive inflection. "vencimiento" appears inside "fecha de vencimiento", which is fine, but a stemming-free includes() will eventually fight a locale that declines the word. If that turns out to be unworkable, the honest fallback is to document the pairs and check them in review — say so rather than shipping a guard that has to be skipped later.

Not in scope

Related

#182 (translate-now policy), #662 (badgeCase.test.ts — precedent for pinning string content), #678 (where this surfaced).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions