i18n keys
pnpm lint:i18n-keys (tools/nx-plugins/enforce-i18n-keys) checks that every key used in
features/, domain/ and shared/ exists in the English catalog of each app consuming the
package: Desktop static/i18n/en/app.json, Mobile src/locales/en/common.json.
.web.* files are checked against Desktop only, .native.* against Mobile only.
Other locales are not checked: only edit English.
Principle: the feature owns its keys
A feature resolves its own wording with useTranslation(). Apps must not inject keys or labels
into features: pass a domain id or variant (stage, status) and let the feature map it to wording.
A key prop coming from apps/* is a smell; key props between components of the same package are fine.
Write keys the check can verify
A key is verified when its type is a finite set of literals. A key typed string is
unverifiable: it is counted and the count may never go up (unverifiable-baseline.json).
Prefer, in this order:
- Literal at the call site:
t("payTab.login.title"). Greppable, needs no types. - Domain union, key derived in one place: a new member fails the check until its key exists.
ts
type Stage = "intro" | "login"; declare const stage: Stage; t(`payTab.login.${stage}.title`); // every Stage member is expanded and checked - Internal prop typed as a union of full keys: last resort, it duplicates key strings in a type.
ts
type Props = { titleKey: "x.intro.title" | "x.login.title" };
What widens a key to string and hides it:
tsconst COPY: Record<Id, { titleKey: string }> = { ... }; // annotation widens: use as const satisfies const rows = [{ key: "a" }, { key: "b" }]; // add as const function labelKey(kind: Kind): string { ... } // annotate the literal union
Template spans must be unions or literal consts, not string or number. Template keys are not
greppable: the check, not search, protects a "dead" key from deletion.
When the check fails
- missing in an app: add the key to that app's English catalog.
- unverifiable count went up: narrow the type (above), never raise the baseline.
- count went down (CI fails until the baseline is lowered): run
node tools/nx-plugins/enforce-i18n-keys/validate.js --update-baseline. - a union member has no key in an app: add the key, or remove the member if that app never uses it.
allowMissinginunverifiable-baseline.jsonlists known gaps for the owning product team to fix; do not add to it without a reason in the PR. - stale
allowMissingentry: the gap is fixed, remove the entry.


