Skip to content

Commit 9c1bcda

Browse files
claude[bot]claude
andauthored
tooling(i18n): gate that every declared translation group is walked by the extractor (#14763)
* tooling(i18n): gate that every declared translation group is walked by the extractor A translation key family lands in `packages/spec` with a resolver that reads it, and the CLI extractor that produces the coverage population is updated later or not at all. In between, `os i18n extract` scaffolds no key for the family, so no translator is asked for one, so `check:i18n-coverage` measures a debt of zero and reports OK while the strings ship in English. Five instances so far, every one found by a person noticing rather than by an instrument. `scripts/check-i18n-walk-parity.mjs` is the instrument. It reads the DECLARED side off the built `TranslationDataSchema`'s zod shape (the contract, not the source text) and the WALKED side off `collectExpectedEntries` run over a fixture stack config that authors a member of every group, then asserts `declared \ (walked u ledger) = {}` and that no ledger entry is stale. The exemption ledger ships EMPTY and is shrink-only in the `KNOWN_IMPORT_UNSAFE` shape: every entry carries the reason the group has no extractor face, a blank or pro-forma reason fails, a stale entry names itself, and `LEDGER_CEILING` refuses growth the author did not edit into the same diff. Deliberately NOT a derivation of the extractor from the resolvers: the two enumerations answer different questions over different inputs. The unit is the top-level group, the one name both sides can spell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV * tooling(i18n): mark the exemption-ledger remedy MAINTAINER-ONLY `check:ratchet-remedy-authority` reached the new gate and was right (#8435): its unwalked-group message offered two remedies and one of them EXPANDS a shrink-only registry, which is the author excusing themselves from the check they just failed. Emitting the group in the extractor stays the landing author's remedy and is offered first; adding an exemption is now named with its owner. Registers the gate in that gate's hand-classified control corpus as `marked`, the same reading as check-auth-mount-ledger.mjs and check-exported-any-returns.mts: there IS a legitimate act here — a group keyed by strings no stack config declares genuinely has no extractor face — so refusal would be untrue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV * tooling(i18n): ledger the three unwalked translation groups by maintainer ruling The walk-parity gate's own first red named three declared translation groups with no extractor face: `messages`, `settingsCommon` and `settings`. The maintainer ruled (2026-09-04, #14653 comment 5535827386) that all three enter the gate's exemption ledger in this PR, each with its reason, and that no `settings` emitter is built now. That act is ⛔ MAINTAINER-ONLY by the gate's own failure text, so it is taken under that ruling and nowhere else: messages keyed by arbitrary ids composed at the `i18n.t()` call sites; no registry or stack config enumerates the id set. settingsCommon the Settings UI's own five source-badge labels — the console's words in every app, ruled out of per-app bundles on #7646. settings keyed by `SettingsManifest.namespace`, which is platform code rather than authored metadata. A DEFERRAL, not a fact: the terminal state is held on #15178. `LEDGER_CEILING` moves 0 -> 3 in the same diff, which is the mechanism working as designed. No check is weakened: the reason floor, the placeholder refusal, the both-direction size ratchet, the stale-entry detection, the empty-population refusal and the MAINTAINER-ONLY remedy text are all untouched. The self-test's `ledger is empty` pin is replaced by three that stay true and still bite — the ledger's key set equals the recorded unwalked set, every reason survives the gate's own reason checks, and its size is exactly the ceiling under the real ratchet classifier, so an entry without a ceiling bump (or a ceiling bump without an entry) reddens in the self-test as well as in production. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXyGTWPbbreqXow7Z2pZCk --------- Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5f5511f commit 9c1bcda

5 files changed

Lines changed: 832 additions & 0 deletions

File tree

.github/workflows/lint.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5344,6 +5344,27 @@ jobs:
53445344
- name: Check no new untranslated declared labels
53455345
run: pnpm check:i18n-coverage
53465346

5347+
# The i18n question none of its three neighbours can answer, because it
5348+
# judges THEM rather than the tree. Each of them measures a POPULATION the
5349+
# CLI extractor produces; none can notice that the population is short a
5350+
# whole family. A translation group lands in the spec with a resolver,
5351+
# the extractor is updated later or not at all, and in between `os i18n
5352+
# extract` scaffolds no key for it — so no translator is asked for one, so
5353+
# the coverage ratchet counts a debt of zero and reports OK while the
5354+
# strings ship in English. That has now happened five times, and every
5355+
# instance was found by a person noticing rather than by an instrument.
5356+
#
5357+
# This asserts set parity between the groups `TranslationDataSchema`
5358+
# DECLARES and the groups `collectExpectedEntries` WALKS, with a
5359+
# shrink-only exemption ledger — empty as it ships — for a group that
5360+
# legitimately has no extractor face, each entry carrying its reason.
5361+
#
5362+
# Reads both sides from BUILT output (the spec's schema object, the CLI's
5363+
# walker) rather than from source text, so it belongs here with the other
5364+
# post-build consumer gates.
5365+
- name: Check every declared translation group is walked by the extractor
5366+
run: pnpm check:i18n-walk-parity
5367+
53475368
# The THIRD i18n question, and the one neither step above can answer
53485369
# (#5750). Both of them read STATIC declarations; the Setup app declares a
53495370
# shell of empty group anchors and gets its ~50 menu entries at RUNTIME

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"i18n:extract": "tsx packages/cli/bin/run-dev.js i18n extract packages/platform-objects/scripts/i18n-extract.config.ts --locales=zh-CN,ja-JP,es-ES --fill=default --source-hashes --out=packages/platform-objects/src/apps/translations",
3434
"check:i18n": "node scripts/check-i18n-bundles.mjs --self-test && node scripts/check-i18n-bundles.mjs",
3535
"check:i18n-coverage": "node scripts/check-i18n-coverage.mjs --self-test && node scripts/check-i18n-coverage.mjs",
36+
"check:i18n-walk-parity": "node scripts/check-i18n-walk-parity.mjs --self-test && node scripts/check-i18n-walk-parity.mjs",
3637
"check:i18n-stale-fill": "node scripts/check-i18n-stale-fill.mjs --self-test && node scripts/check-i18n-stale-fill.mjs",
3738
"check:app-nav-i18n": "pnpm --filter @objectstack/cli run check:app-nav-i18n",
3839
"check:nul-bytes": "node scripts/check-nul-bytes.mjs --self-test && node scripts/check-nul-bytes.mjs",

0 commit comments

Comments
 (0)