Skip to main content
Urbicon UI

Auditing & Quality

Three complementary layers catch i18n problems before they ship: a data-level parity audit you assert in a test, a runtime sink that flags raw-key renders, and a source scan for unused keys and hardcoded strings. The first two are dependency-free; the scanner and CLI front them for whole-project checks.

A key-based i18n system has three distinct failure modes, and one tool rarely covers all three. A locale can fall behind the base (a key missing, empty, or with the wrong interpolation params). A defined key can become dead after a rename. And UI copy can bypass i18n entirely as a literal string. Each needs a different lens.

Where each layer lives

  • Data-level audit (auditTranslations) and the runtime sink (onMissingKey) ship from @urbicon-ui/i18n — dependency-free, run them in a Vitest test.
  • The source scanner is the dev-only @urbicon-ui/i18n/audit subpath (typescript + svelte as optional, lazily-imported peers).
  • urbicon i18n (in @urbicon-ui/design) is the filesystem front end over all three — the one you run in CI.

1. Parity & quality (data-level)

auditTranslations diffs a package's locale bundles. Beyond missing/extra keys it sees what a structural diff can't: empty values, interpolation-param drift ({{name}} present in one locale but not another), value-equals-key placeholders, and malformed or CLDR-incomplete _plural objects. It is pure and deterministic — no false positives — so it belongs in a test that fails CI on drift.

Loading...
Loading syntax highlighting...

The richer successor to validatePackageTranslations (kept for back-compat). Toggle individual checks or set a base locale via the third options argument; same-as-base (a target string still identical to the base — probably untranslated) is opt-in because proper nouns make it FP-prone.

2. Runtime misses

A static audit can only see keys that exist as literals. Keys assembled at runtime (t(`errors.${code}`)) are invisible to it — but onMissingKey catches them where it counts: the moment translate resolves a key nowhere and would render its raw string. createMissingKeyCollector packages that sink for a test or E2E run.

Loading...
Loading syntax highlighting...

Off by default (a provider-less render legitimately misses keys), so it only fires once you opt in. Fed back into the scanner as runtimeUsedKeys, the observed keys also prove a dynamic key is reachable — shrinking false "unused" reports.

3. Unused keys & hardcoded strings

urbicon i18n scans your sources (binding-aware, so it follows arbitrarily-named useFooI18n() aliases, member calls, and <T key>). It reports unused keys (defined but referenced nowhere — confirmed when no opaque dynamic call could be hiding them, else suspect), used-but-undefined keys (a typo that renders raw), and hardcoded UI copy in markup. Run it under Bun (it loads .ts locale bundles).

Loading...
Loading syntax highlighting...

It gates (exit 1) on parity errors and used-but-undefined keys — the correctness failures. Unused keys and hardcoded strings are advisory (add --strict to gate them too), mirroring the design gate's correctness-vs-slop split. The same scanning core is importable for programmatic use:

Loading...
Loading syntax highlighting...

The unused check is biased hard toward "used" — a key is flagged only when no static call, no template prefix, no string literal anywhere, no allowlist, and no runtime observation reaches it — so a live key is never wrongly proposed for deletion. Declare dynamic families with --dynamic-keys to clear the suspect tier.

CI integration

One step gates translation correctness alongside your design gate. urbicon init --ci writes it for you; the standalone form:

Loading...
Loading syntax highlighting...

Pair it with the auditTranslations test for the data-level checks and a createMissingKeyCollector assertion in your E2E run for the dynamic keys — three layers, each catching what the others can't.