chore(ci): ratchet locale hygiene - #4817
Conversation
73e42da to
623011d
Compare
623011d to
e03c6b8
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed at e03c6b8. The problem is real: I ran the four rules over main's in-scope files and got 140 hits (62 / 34 / 12 / 32), all genuine, none from comments or strings. A base-relative ratchet with no ledger is the right shape here, Biome cannot carry it (the linter is an explicit allowlist with a zero-violation entry bar, and it cannot express "no growth against base"), and all four sibling PRs only lower the counts, so nothing is blocked. But as written the gate cannot see the lines it was created for.
P1: every rule matches single quotes only, and double quotes are permanent in two thirds of the scope. biome.jsonc excludes apps/desktop/** and packages/ui/** from the formatter and has no quote rule, so " is not a typo there. On main today packages/ui/src/materialize.ts:192 and :706 read locale: UiLocale = "en" and settings-test-result-copy.ts:166 reads locale === "en"; all three pass silent-locale-default / locale-branch untouched. Those two materialize.ts lines are the exact P2 from #4640 that this rule was offered as the guard for. Fix: ' → ['"] in all four patterns (and [^']* → [^'"]*). Please add a fixture that is main's materialize.ts:192 verbatim.
P2: counts are per matching line, not per match. scanSource does pattern.test(line) once per line, so const a = locale === 'en' ? x : y; const b = locale === 'en' ? p : q; counts 1, and splitting browser-message-box.ts:351 (two comparisons on one line) across two lines for readability fails CI with no new branch. Use matchAll with a global pattern and push per match.
P2: locale-branch now has two definitions in the repo, and this is the weaker one. scripts/check-tui-copy.mjs already implements locale-branch and cjk-literal over a @babel/parser AST (ConditionalExpression / IfStatement / SwitchStatement), which is immune to quoting, line wrapping, switch (locale), and comment or string false positives. This step runs after npm ci, so the parser is available. Either wire the scope scan to its exported checkSource and keep only the ratchet layer here, or rename the rule and say in a comment where the two definitions diverge.
Smaller:
- A rename whose source is outside
SCOPEshows up asAin the pathspec'd diff, so base is 0 and every existing hit counts as new. The comment "the same diff pairs moved files with their base path" is only true within scope. - With no
--baseand no resolvable merge base the script prints "skipping" and exits 0;workflow_dispatchtakes that path.ci-workflow-policy.test.mjsalready states the norm: a gate that cannot fail the job is not a gate. - The body's 62 / 35 / 11 / 32 reproduce as 62 / 34 / 12 / 32 with the script's own scope; worth aligning since they are the case for the PR.
- The script needs only
node:built-ins, so it belongs with the install-free gates beforesetup-node, where an install failure cannot hide it.
After #4640 and #4824 land, silent-locale-default is at zero and can become a hard check rather than a ratchet.
Evidence boundary: static read plus the script's own four tests and the scans above, run on a copy of the two files outside the repo. No suite, no build.
AI-assisted review: drafted with Maka; I verified the regex, the biome exclusions, and the materialize.ts escape myself.
e03c6b8 to
ee51f3f
Compare
A new UI locale must only mean a new UiCatalog key. Branching on a locale literal, defaulting the locale parameter, or sniffing CJK in a payload all compile cleanly and render the wrong language instead. Count those per file against the base commit and fail on growth, so existing debt shrinks without any ledger to maintain. The rules accept both quote styles, since biome leaves apps/desktop and packages/ui unformatted, and count per match rather than per line. The step needs only node built-ins, so it runs with the install-free gates. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_01AqdSkg56F2x55wEGRWvzcB
ee51f3f to
e78295c
Compare
|
Thanks for the detailed review — these are good catches. I’ve updated the PR to address them |
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed at e78295c. Everything from last round is closed in code, and I re-ran the checks rather than reading the body:
- Both quote styles: the four patterns take
['"], and thematerialize.ts-shaped fixture (locale: UiLocale = "en") is in the test. The body's 67 / 4 / 12 / 32 reproduce exactly with the script's ownRULESovermain's in-scope files. - Per match, not per line:
matchAllwithg, pinned by the two-comparison fixture. - The rule is renamed
locale-literal-comparewith the comment stating where it diverges fromcheck-tui-copy's AST rule; that is the option I offered, so no duplicate definition. - Diff no longer pathspec-limited,
inScopefilters afterwards, so a move into scope keeps its base. - No base → exit 1 with a message, not a skip. The step now sits with the install-free gates before
setup-node, andfetch-depth: 0is already there soBASE_SHAresolves.
Ran the ratchet from this head against the current heads of #4524 and #4526 with --base at their merge base: both pass. Once #4524 lands, silent-locale-default is at two (subscription-result-message.ts:26, use-oauth-login-flow.ts:331, both #4551's), and after that it can become a hard zero as the body says.
Nothing left to ask for. Order among the locale PRs does not matter for this one; it touches only scripts/, package.json and ci.yml.
Evidence boundary: static read plus the script's five tests and the scans above, run on copies outside the repo; no suite, no build.
AI-assisted review: drafted with Maka; I verified the counts, the CI ordering and the two ratchet runs myself.
Summary
Adding a UI locale today compiles cleanly and still renders the wrong language wherever code branches on a locale literal instead of indexing a
UiCatalog. Onmaintoday the four patterns hit 67locale === '…'comparisons, 4locale: UiLocale = '…'defaults, 12 CJK payload sniffs, and 32 simplified-keyed translation maps (per match, both quote styles), none of which the type system sees.This adds
scripts/check-locale-hygiene.mjs, a ratchet overapps/desktop/src,packages/core/src, andpackages/ui/srcthat counts those four patterns per file and fails CI when any count grows against the base commit. There is no ledger: the base is the merge base (orBASE_SHAin CI), so paying down debt needs no bookkeeping. Only files ingit diff -M30% <base>are scanned, since an untouched file cannot grow a count; the diff is not pathspec-limited, so a file moved into scope keeps its base path. The rules match'and"alike because biome leavesapps/desktopandpackages/uiunformatted, and count per match so splitting a line cannot fail the gate. With no resolvable base the step fails rather than skips. The script needs onlynode:built-ins, so the step sits with the install-free gates beforesetup-node. The open locale PRs all pass it unchanged; the four remainingsilent-locale-defaulthits go with #4524 (two inpackages/ui) and #4551 (two OAuth helpers), after which that rule can become a hard check.Verification
npm run format:check,biome checkon the two scripts, andcheck:asf-headerspass. Ran the ratchet against the eight open locale PR branches: all pass.Review focus
locale-literal-comparematches===/!==against a locale literal andstartsWith('zh'); it is deliberately narrower than the ASTlocale-branchincheck-tui-copy.mjs(which also seesswitch (locale)and if-statements) so this gate stays install-free. No such switch exists in scope today.AI use
Select exactly one:
Tool(s) and scope: Claude Code drafted the script, its tests, and the CI wiring; the rules and baseline were reviewed by hand.
Checklist
Does this PR entail a change in behavior?