5 Commits

Author SHA1 Message Date
Maurice b31c2ff1d2 ci(client): hold the en keys nothing reaches to a baseline that only shrinks
lint:i18n-keys only listed unused en keys as information, and it looked for
references in client/src alone, so every key the server sends (system notice
and notification channel keys in its registries) read as dead. The reference
search now also reads server/src and plugin-sdk/src; those references only
mark keys as reached and are not held to the client's key rules. The unused
keys per en domain file are counted against scripts/i18n-unused-baseline.json,
which only shrinks and fails on an entry above its count until --update
lowers it. Today's 841 unused keys in 44 files are frozen, none deleted.
2026-10-10 21:33:29 +02:00
Maurice 13584d1bf6 style(client): format the i18n key check and its test
The new lint:format ratchet admits no unformatted file outside its
baseline, and these two arrived after it was taken.
2026-10-08 13:27:38 +02:00
Maurice c0516dd9bf fix(client): check each literal branch of a template key
A template key was read as a pattern in which every interpolation is a
wildcard, so t(`roadtrip.window.${a ? 'onRoute' : 'onRoutTypo'}`) passed
as long as some other roadtrip.window key existed, and the typo rendered
as raw text.

An interpolation that can only produce string literals (a ternary chain
of them, in parentheses or not) is now expanded, and each resulting key is
checked like a literal one. Thirteen call sites become literal keys that
way. Where data is involved, a literal fallback (`${role ?? 'editor'}`)
is still checked as a key, with the template kept as a pattern under
its own allow-list site. Literals in a condition (`phase === 'start'`)
are never taken for results.

Narrow templates over data still pass without an allow-list entry. The
script header now says plainly what that leaves unchecked: a runtime
value with no key under a narrow prefix, which needs the value's type.
2026-10-08 13:04:42 +02:00
Maurice 33f004ed04 fix(client): a wide template key needs a reviewed allow-list entry
lint:i18n-keys let any template key through as long as its fixed parts
matched one en key, and each interpolation could stand for any run of
characters, dots included. `settings.${mode}` therefore reached all 574
settings keys and `help.guide.${id}.${part}` nearly 2000, so a wrong
suffix behind such a prefix could never fail, and none of the 109
templates had been looked at.

An interpolation now stands for one key segment, and a template passes
by itself only when it reaches between 1 and MAX_IMPLICIT_MATCHES (30)
en keys, the size of an enum such as the docsync error codes. A wider
one fails unless DYNAMIC_ALLOWED has an entry for that file and template
saying what bounds the value. The twelve wide templates in use today
get one each: the help key builders (checked by the registry test),
literal tuples beside the call, typed unions, the generated plugin
permissions and the seeded photo provider fields. An entry whose file no
longer uses the template, or whose template has become narrow enough to
pass alone, fails, so the list stays short and current.

Listing all 109 templates instead was considered and left out: the
narrow ones would only restate their own prefix, and the bound already
keeps a mistake inside one small namespace.
2026-10-08 13:04:42 +02:00
Maurice f00c354d0d fix(i18n): add the two keys the client named without en having them
BudgetPanel titled its add button with budget.addCategory (since April)
and the trip form labelled its cover remove button with common.remove
(since August). Neither key was ever in en, and t() returns an unknown
key unchanged, so both rendered as the raw key in every language. Both
are added to all 27 locales.

Locale parity compares the locales with en, never en with the code, so
nothing could catch this. client/scripts/i18n-keys.mjs (npm run
lint:i18n-keys) now resolves every literal key src/ names against en:
the first argument of t(), tHtml() and tr() (both sides of a conditional
included), <TransHtml html>, the fallback of translateApiError and any
*Key property or prop. A template key, a prefix with the rest appended
and the help registry's key builders pass when their fixed parts match
an en key; the one template with no fixed part is on an allow-list that
names the keys it resolves to, and an entry nothing uses fails. With
--unused it lists the en keys nothing reaches, as information only.

en is read with shared/scripts/i18n-catalog.mjs, a reader for the
locale tables as text that the parity tooling will share. It refuses a
value that is not one string literal instead of skipping the key.
2026-10-08 13:04:41 +02:00