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/auditsubpath (typescript+svelteas 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.
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.
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).
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:
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:
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.