feat(scripts): value-compare createSafeTranslation defaults tables against the en pack (objectui#7567) - #7870
Merged
os-sam merged 2 commits intoSep 6, 2026
Conversation
…ainst the en pack (objectui#7567)
`check-i18n-call-site-keys` already held one flavour of provider-less English
to the pack — an inline `t(key, { defaultValue })` — and never read the other.
A `createSafeTranslation(DEFAULT_TRANSLATIONS, …)` row carries the identical
promise one indirection away: `fallbackT` resolves `defaults[key] ||
defaultValue || key`, so the table stands in for the pack value on a
provider-less host. A row that says something else is the same control labelled
two ways depending on where it is mounted, which is exactly what objectui#7454
measured.
Class 8, `factory-default-drift`. The population is read from the source —
every `createSafeTranslation` / `createSafeTranslationHook` invocation — rather
than from a registry, so a 33rd factory is covered the day it is written. It
generalises `defaults-maps-mirror-en-pack.test.tsx` (objectui#4401), which
covers the three tables listed in it by hand.
Blocking from day one, with no ledger section, because the census says it can
be: 32 factory sites over 32 distinct tables, 846 rows, 841 comparable, 0
drifted. objectui#7454's instance — the one known member of the class — landed
in objectui#7574 before this rule did, so a ledger would have nothing in it and
would read exactly like a populated one.
The abstention counts are printed on every run and asserted by the collapse
guard, because "0 drifted" and "0 rows compared" read identically otherwise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
This was referenced Sep 6, 2026
os-sam
marked this pull request as ready for review
September 6, 2026 01:07
os-sam
enabled auto-merge
September 6, 2026 01:07
os-sam
disabled auto-merge
September 6, 2026 01:08
os-sam
enabled auto-merge
September 6, 2026 01:08
…compare Conflict: `scripts/check-i18n-call-site-keys.mjs`, against objectui#7592's `84c117f0d` — and it is a real interaction, not a textual collision. Both commits widened the SAME file-walk pre-filter for the SAME structural reason: a module the walk was dropping unparsed because it holds no `t(` spelling. objectui#7592 needed key-builder modules; objectui#7567 needs factory-and-table modules (`useTimelineTranslation.ts` and its siblings). Resolved as a three-way union rather than by taking a side: three independent reasons to parse, and each leg guarded by its OWN predicate, never by "this file was parsed". That last part is load-bearing. A leg gated on the union would silently inherit the other legs' widenings, and each card's measured blast radius would stop being a reading about that leg. Verified: the merged run still reports `1 head(s) read off a key-building helper`, exactly the one builder objectui#7592 measured. Re-measured on the merged tree, not carried over: 32 createSafeTranslation sites over 32 distinct tables, 0 unreadable, 841 rows compared, 841 matching, 5 on a key `en` does not define. Unchanged in every figure, which is what the blocking-with-no-ledger ruling rests on. Control re-run against the correct baseline (plain `origin/main` at 565817a, not the stale pre-merge base): exactly two lines of the gate's summary differ — the new factory census line and the green verdict sentence. Every pre-existing counter is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
os-sam
deleted the
claude/issue-7567-i18n-factory-defaults-value-compare
branch
September 6, 2026 01:46
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7567
check-i18n-call-site-keysalready held one flavour of provider-less English to theenpack and never read the other:enbefore this PRt(key, { defaultValue })inline literalcreateSafeTranslation(DEFAULT_TRANSLATIONS, …)table rowThey are the same promise one indirection apart.
packages/i18n/src/useSafeTranslation.ts'sfallbackTresolvesdefaults[key] || defaultValue || key, and its own comment calls the table "the pack value's stand-in here … it takes the pack's position in i18next's own order". So a row that differs fromenis the same control labelled one way on a provider-less host and another in the console — #7454's measured instance.This adds class 8,
factory-default-drift.Which branch of the ruling this landed on, and on what evidence
Neither of the two the dispatch anticipated. The ruling was: one known instance ⇒ blocking + a named ledger citing #7454; more than one ⇒ report-only. The census reports zero, so it landed blocking with no ledger at all.
The reason is dated: #7454's instance landed on 2026-09-04 as
b8fc1e2bf("fix(plugin-calendar): align the calendar.allDay default to the pack and drop its dead ternary", #7574), two days before this card was dispatched.packages/plugin-calendar/src/CalendarView.tsx:84now reads'calendar.allDay': 'All Day', byte-equal toen, andCalendarView.allDaySpelling-7454.test.tsxpins it. A ledger built for that entry would have been empty on the day it shipped, and an empty ledger reads exactly like a populated one — so there is no ledger section. That is also the gate's own precedent for classes 3, 4, 5 and 7, each of which is stated to be hard from day one because its first full run found no debt for a ratchet to hold.The census — the comparison surface, not just the hit count
Measured with a scratch instrument before the rule was written, then reproduced by the shipped rule (identical numbers):
createSafeTranslation/createSafeTranslationHookinvocationsenendoes not definetimeline.relative.*envalueThe blind spot, sized rather than implied. The abstentions above total 5 rows out of 846 (0.6%), and all five are one shape:
timeline.relative.today/tomorrow/yesterday/inDays/daysAgoinpackages/plugin-timeline/src/useTimelineTranslation.ts. Noenleaf and — checked repo-wide including tests — no caller anywhere. Class 1 cannot reach them either, because it judges call sites and these have none. Filed separately rather than touched here.What this class does not reach, measured rather than assumed. Three tables hand-roll
fallbackTinstead of taking the factory (HAND_ROLLED_TABLESinpackages/i18n/src/__tests__/fallback-placeholder-spelling-3512.test.ts).TIMELINE_DEFAULT_TRANSLATIONSalso reaches the factory and is covered; the other two are not. They were measured while writing this:GANTT_DEFAULT_TRANSLATIONS80 rows / 80 compared / 0 drifted,IMPORT_DEFAULT_TRANSLATIONS135 / 135 / 0. Reaching them needs a declared registry — nothing in the source says which local function is afallbackT— which is a second thing to keep from rotting, so it is named in the header and left to a follow-up rather than folded in. The residue is real, named, and currently clean.Controls
Positive control — on disk, #7454's exact drift reintroduced.
'calendar.allDay': 'All Day'→'all-day'inCalendarView.tsx. Mutation proven landed before running (anchor count flipped1→0for the pack spelling and0→1for the mutated one, blob hash moved389b6e11…→5d44b909…); restore wasgit checkout HEAD --plus the absolute path inside atrap … EXIT INT TERMarmed only after the implementation was committed, and proven by state afterwards (worktree hash back to389b6e11…= the HEAD blob, anchors back to1/0,git status --porcelainandgit diff HEAD --statboth empty).The gate exited 1 and said:
Note the shape of the change:
comparedstayed 841,matchingwent 841 → 840. The row moved from matching into a finding rather than the surface moving — which is what says the instrument was already looking at that row.⛔ No ablation rebuild leg, and that is a statement about the resolution path rather than an omission: the subject is a
.mjsgate run directly by node, and it reads the mutated file as source (collectSourceFileswalkspackages/). Nothing here resolves through a packageexportsfield or adist/directory, so there is no artefact between the edit and the reading.Negative control: the 841 aligned rows are silent, and
is silent when every row copies the pack value byte for bytepins it synthetically withfactoryMatchingRowsasserted, not merely "no findings".The whole-run control. Diffing the gate's summary before and after the change, the only two lines that move are the new census line and the green verdict sentence. Every pre-existing counter — 3937 call sites, 2728/2728 keys, 996/993 inline defaults, 2702 parity comparisons, 25 families, 2 sibling fallbacks — is byte-identical, which is the reading that says restructuring the file walk's pre-filter did not disturb any existing class.
Design notes worth reviewing
packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx(detail.editFieldsInlinerenders two different labels depending on whether an I18nProvider is mounted — the defaults map says "Edit fields inline", the en pack says "Edit fields" #4401) already asserts exactly this rule forDETAIL_/LIST_/DESIGNER_DEFAULT_TRANSLATIONS— the three tables somebody listed in it by hand. This class takes everycreateSafeTranslationinvocation instead, so the 33rd factory is covered the day it is written.plugin-detailre-exportscreateSafeTranslationHook = createSafeTranslation; a rule that knew only the canonical name would have skippedDETAIL_DEFAULT_TRANSLATIONS, the very tabledetail.editFieldsInlinerenders two different labels depending on whether an I18nProvider is mounted — the defaults map says "Edit fields inline", the en pack says "Edit fields" #4401 was written for. Same set as the createSafeTranslation 的无 provider 回退插值只认{{name}}一种写法,i18next 还认带空格 / 带格式化 / 嵌套三种 #3512 test uses, pinned equal to it.t(key) || 'English'— 24 sites, all on keysendefines, and #3810's new rule cannot see any of them #4117 shape again. The walk parsed only files matching/\btt?\s*(?:\?\.)?\s*\(/. Nine of this repo's factory modules (useTimelineTranslation.tsand siblings) hold a table and a factory call and never writet(at all — they would have been dropped silently, out of the whole class at once. Pinned by a test that asserts the fixture does not match that regex and is still scanned.{{name}}一种写法,i18next 还认带空格 / 带格式化 / 嵌套三种 #3512 test's table reader would be wrong:fallbackTdoes a flatdefaults[key]lookup, so{ calendar: { today: 'Today' } }is a row nothing reads at runtime, and comparing it againsten.calendar.todaywould green-light a dead row. It counts as unreadable instead. 0 such rows today; the abstention is what stops the first one reading as healthy.t(key, { defaultValue })strings, which fallbackT also reads, carry no transitive pin #4905 added for its own:factoryComparedRows < 500exits 1 with a message. The verdict here is "0 drifted", and 0 is also what a resolver that stopped resolving reports.en.tsto match it.## Five failure classesabove a list of seven. Corrected toEight, with a parenthetical saying it was stale, rather than left to grow by one more.Tests
13 new cases in
scripts/__tests__/check-i18n-call-site-keys.test.ts,122 passed (122)for that file. Every abstention is pinned as a count, not as "no finding" — because the verdict onmainis zero and a resolver that stopped resolving reports zero too.Merge resolution against
84c117f0d(objectui#7592), and the re-measurement it obligedorigin/maingained objectui#7592's key-builder leg (84c117f0d), which touches exactly these two files. Merged withgit merge origin/main— no rebase, no amend, no force-push — as merge commit6ee07ba2a.It was a real interaction, not a textual collision. Both commits widened the same file-walk pre-filter for the same structural reason: a module the walk was dropping unparsed because it holds no
t(spelling. objectui#7592 needed key-builder modules (tool-display.ts); this PR needs factory-and-table modules (useTimelineTranslation.tsand its siblings). Neither side was "taken" — the pre-filter is now a three-way union:⭐ The load-bearing half is that each leg stays guarded by its OWN predicate, never by "this file was parsed." A leg gated on the union would silently inherit the other legs' widenings, and each card's measured blast radius would stop being a reading about that leg. Verified rather than assumed: the merged run still reports
1 head(s) read off a key-building helper rather than a call site— exactly the one builder objectui#7592 measured.Re-measured on the merged tree, not carried forward
6ee07ba2a)enendoes not define (timeline.relative.*, objectui#7874)envalueEvery figure unchanged, including the two the ruling depends on:
0 unreadableand0 drifted. The collapse guard's threshold is still live against the new population — 841 compared against a floor of 500.The whole-run control, re-established against the correct baseline
The pre-merge version of this control compared against a base that is now 19 commits stale, so it was re-run against plain
origin/mainat565817a72in a throwaway detached worktree. Diffing that against the merged branch, exactly two lines move: the new factory census line, and the green verdict sentence. Every pre-existing counter is byte-identical — 3938 call sites, 2728/2728 keys, 996/993 inline defaults, 2702 parity comparisons, 26 families, 1 key-builder head, 2 sibling fallbacks.The positive control, re-run on the merged tree
A control proved on the pre-merge tree is not a control on this one, so objectui#7454's drift was reintroduced again on the merged tree. Same discipline: mutation proven landed first (anchors flipped
1→0/0→1, blob hash389b6e11…→5d44b909…), restore viagit checkout HEAD --plus the absolute path inside atrap … EXIT INT TERMarmed only after the merge commit existed, restoration proven by state (hash back to the HEAD blob, anchors back to1/0,git status --porcelainandgit diff HEAD --statboth empty). The gate exited 1 and named itself, with the population moving exactly as before —comparedstayed 841,matchingwent 841 → 840.Verification run — all re-run at
6ee07ba2a, the merge commitnode scripts/check-i18n-call-site-keys.mjspnpm exec vitest run— this file plus the three consumer suites (check-i18n-dead-keys.test.ts,scripts-type-check.test.ts,CapabilityMultiSelectField.specParity-6285.test.tsx)Test Files 4 passed (4)·Tests 181 passed (181)— includes objectui#7592's own 8 new cases, which pass unchangednode scripts/check-i18n-dead-keys.mjs(the only other module importinganalyze)pnpm type-check:scriptspnpm lint:root33 problems (0 errors, 33 warnings), all pre-existing, none in either touched filepnpm check:control-bytes·check:entry-guard·check:shell-escape-residue·check:vi-mock-specifiers·check:vi-mock-inheritgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both touched filesnode scripts/check-changeset-presence.mjsNo source or published contract of a released package changed in this range, so no changeset is owed.(merge-base now565817a72, still 2 files changed — mine)Lint scope is complete, not narrowed.
pnpm lintisturbo run lint, which runs each package's own lint over its own directory;scripts/**belongs to no package, solint:rootis the only lint configuration that reads either touched file, and it ran in full (14s).What I did not run: the rest of the repo's per-package test and lint farm, and CI itself. I also did not update the branch against the 4 commits
maingained after565817a72— none of them touches either file andgit merge-treereports a clean merge, so thebehindstate is the queue's to resolve, not mine. Neither touched file is imported by any package source — the only two importers ofanalyzearescripts/check-i18n-dead-keys.mjsand onepackages/fieldstest, both run above.ci.ymluntouched, as instructed. The gate already has its CI step (.github/workflows/ci.yml:487,run: pnpm check:i18n-keys), andscripts/__tests__/check-i18n-call-site-keys.test.tsalready pins that it runs after the install it needs. Nothing to add.Clause-② stayed
no: two files, both underscripts/. No runtime change, no locale-pack change, andpackages/plugin-calendaris byte-identical toHEAD(proven above).🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code