i18n leak detection
i18n specs/i18n/leak-detection.kmd
Detection contract for "i18n leaks" — user-facing string literals in Koder Stack UI surfaces (Flutter widgets, templ templates, TS/ JSX components, Go CLI/TUI) that bypass canonical i18n routing. Defines what counts as a leak, the allowlist categories, per-module opt-in, the audit gate, and the Stack-wide coverage registry.
When this pattern applies
Primary triggers
- Define or revise the i18n leak audit contract
All triggers
- Create or modify user-facing widget / template / CLI text
- Add a new screen to a Koder Stack module
- Audit a module for i18n compliance
Specification body
Spec — i18n leak detection
Status: v0.1.0 Draft (2026-05-28). Companion to
specs/i18n/contract.kmd— that spec defines the i18n contract (locale negotiation, runtime switch, ICU keys); this spec defines how the Stack detects violations of that contract.
The audit pipeline is implemented in
dev/koder-tools/cmd/koder-i18n-audit/ (Go CLI, static scan) +
engines/sdk/koder_test_* (runtime audit via headless render).
R1 — Definition of "leak"
A leak is a user-facing string literal in render code that:
- Is not routed through any of the canonical i18n helpers:
- Dart/Flutter:
KoderL10n.t(…),Intl.message(…), widget-local helpers that internally call them - templ (Go HTML):
loc.T(…) - TypeScript/JSX:
t(…),i18next.t(…) - Go CLI/TUI:
i18n.T(…),cobra.Command.Short/Longwhen populated from translation tables
- Dart/Flutter:
- Is not in the allowlist (R2)
- Is not the
// kds-i18n-allowoperator opt-out comment - Looks like prose: contains at least one whitespace OR is ≥6 chars AND mixed case OR multi-word
The classification is best-effort heuristic; false positives are expected and fixed by extending the allowlist (R2.5) — never by loosening R1.
R2 — Allowlist categories
The allowlist lives at
registries/i18n-allowlist.md.
Strings matching ANY of these categories are not leaks:
R2.1 — Proper names
Brand names of products, services, and visual languages NOT
translated across locales. Examples: Koder, Verge, Material,
GNOME, KDE Breeze, Adwaita, Cyberpunk.
R2.2 — Koder products + services
Product names that ship with English-source identity globally:
Koder Hub, Koder Talk, Koder Flow, Koder Stack,
Koder Kodix, Koder Dek, Koder ID, Koder Jet.
R2.3 — Code identifiers
Class/type/function names that appear in user-facing context for
developer surfaces (KDS docs, code samples, error codes):
KoderApp, KoderUIStyle, KoderSignInButton, KoderL10n.
R2.4 — Units, symbols, technical literals
B, KB, MB, GB, TB, ms, μs, ns, fps, Hz, MHz,
HTTP, HTTPS, TLS, OAuth, OIDC, JSON, YAML, TOML.
R2.5 — Per-module extension
Each module's koder.toml can declare additional allowlist entries
under [i18n_audit] extra_allowlist = ["…", "…"]. Used for
module-specific brand/product names (e.g. Dek-specific terminology).
R2.6 — Operator opt-out comment
// kds-i18n-allow: <reason> on the line above a string literal
marks it as an intentional non-translation (debug log, internal
identifier). The reason text is mandatory and audited (no empty
opt-outs).
R3 — Per-module opt-in
Each Koder Stack module opts into the audit by adding to its
koder.toml:
[i18n_audit]
enabled = true # default false (opt-in per module)
extra_allowlist = ["MyProductName"] # see R2.5
excludes = ["lib/legacy/**"] # paths to skip
max_leaks = 0 # CI gate floor; lower as fixes land
Module opts INTO the gate when ready. Rollout is incremental — no "flag day" where the audit must be green Stack-wide on day 1.
R4 — Audit gate
The koder-i18n-audit CLI runs in two modes:
R4.1 — Report mode (default)
Walks the module's source, classifies every string literal, emits
JSON report at dist/i18n-audit.json (schema kds-i18n-audit-v1)
with per-file + summary. Always exits 0.
R4.2 — Strict mode
koder-i18n-audit --strict exits 1 if summary.leaks > max_leaks
(module's koder.toml gate floor). Used in CI to prevent
regressions while phase-by-phase fixes drive max_leaks to 0.
R4.3 — Runtime mode
engines/sdk/koder_test_widgets/lib/i18n_audit.dart exposes
testKoderI18nNoLeaks(widget, locales, allowlist) for Flutter
widget tests. Per policies/headless-first.kmd §R8, every UI
module uses this SDK rather than rolling its own — runtime audit
catches dynamic strings the static scan misses (concatenation,
helper-returned, computed).
Modules wire it into their test/ directory with one test per
top-level screen and run on every flutter test.
R5 — Coverage matrix
meta/docs/stack/registries/i18n-coverage.md aggregates per-module
audit results into a single table — Stack-wide percentage of modules
passing strict mode, per-locale dictionary completeness, per-module
leak count trend. Updated on every nightly CI run.
| Module | Static leaks | Runtime leaks | Strict mode | Locales covered |
|---|---|---|---|---|
| (example) products/dev/hub | 0 | 0 | ✓ | en-US, pt-BR, es-ES |
| products/horizontal/talk | 12 | — | ✗ | en-US (pt-BR pending) |
R6 — Locales in scope
Wave-1 per specs/i18n/contract.kmd §R7: en-US (source of truth),
pt-BR, es-ES. The audit asserts coverage in pt-BR + es-ES; a string
that's en-US-only with no pt-BR translation IS a leak (the user sees
en-US literal instead of translated text).
Wave-2 locales add columns to R5's matrix incrementally.
R7 — Trigger on commit
Module CI workflow runs koder-i18n-audit --strict on every PR
that touches lib/**/*.dart (Flutter), internal/**/*.templ (Go),
src/**/*.{ts,tsx} (web), or cmd/**/*.go (CLI). Mirrors the
design-gen pattern of --max-broken-links (defensive gate per
ratification baseline).
Cross-link
specs/i18n/contract.kmd— defines the i18n contract this spec auditspolicies/headless-first.kmd— why runtime audit lives inkoder_test_*registries/i18n-allowlist.md— the canonical allowlistdev/koder-tools/cmd/koder-i18n-audit/— the CLI implementation
References
specs/i18n/contract.kmdpolicies/headless-first.kmdregistries/i18n-allowlist.mddev/koder-tools/cmd/koder-i18n-audit/