Repository navigation
feat(ci): block stale UI translations when an English value is rewritten - #8574
Merged
Merged
Conversation
Closes the gap that let #8463 ship. `oauthModal.googleOAuthWarning`'s English value was rewritten when the Antigravity login helper landed (#5203); 39 of 43 locales kept a translation of the PREVIOUS English, which told operators to "copy the full URL and paste it below" — a flow that cannot complete for that provider family. Non-English users read confident, wrong instructions for months and no gate noticed. None of the three existing gates can see this class: - `sync-ui-keys.mjs` only backfills keys that are ABSENT, never ones that are STALE; - `check-ui-keys-coverage.mjs` counts key PRESENCE, so a stale translation scores as fully covered (all 43 locales sat at 99.6% throughout); - `check-translation-drift.mjs` tracks the `docs/i18n/<locale>/**.md` documentation mirrors — it never reads `src/i18n/messages/*.json` at all. (Its `.i18n-state.json` is also absent, so it self-skips, but bootstrapping it would not have helped: wrong surface.) New gate `scripts/i18n/check-ui-value-drift.mjs` is DIFF-AWARE rather than baseline-backed: it compares `en.json` at the merge base against the working tree, and for every key whose English value changed, reports any locale still holding an untouched translation. That choice deliberately freezes pre-existing debt — a diff cannot reveal which old English a long-standing translation came from, so the gate judges only what the current change touches, and unrelated PRs never pay for historical drift. The alternative, a per-key hash baseline over 11207 keys, would have cost a ~600 KB generated file (3x the largest existing baseline) churning on every i18n PR. Two ways to satisfy it: refresh the translations, or set them to `__MISSING__:<new english>` so the runtime serves the corrected English (#7258) while the key queues for translation. When the string's MEANING changes, renaming the key is better still — a new key cannot inherit a stale translation, which is what #8463 did. Wired blocking into the `i18n-ui-coverage` job (the `i18n` job is `continue-on-error: true`, so a gate there could not block anything). That job gains `fetch-depth: 0` because the gate needs the base ref; without it the gate self-skips with `base-unresolved`, mirroring `check-openapi-breaking`. `BASE_REF` is passed via `env:` and reaches git only through `execFileSync` argv — never a shell string. Verified against the real defect: rewriting an English value with translations left behind reports exactly 39 stale locales and exits 1; `--warn` exits 0; an unresolvable base exits 0 with `SKIP reason=base-unresolved`.
HouMinXi
pushed a commit
to HouMinXi/OmniRoute
that referenced
this pull request
Aug 2, 2026
…ten (cheaper-inference#8574) Closes the gap that let cheaper-inference#8463 ship. `oauthModal.googleOAuthWarning`'s English value was rewritten when the Antigravity login helper landed (cheaper-inference#5203); 39 of 43 locales kept a translation of the PREVIOUS English, which told operators to "copy the full URL and paste it below" — a flow that cannot complete for that provider family. Non-English users read confident, wrong instructions for months and no gate noticed. None of the three existing gates can see this class: - `sync-ui-keys.mjs` only backfills keys that are ABSENT, never ones that are STALE; - `check-ui-keys-coverage.mjs` counts key PRESENCE, so a stale translation scores as fully covered (all 43 locales sat at 99.6% throughout); - `check-translation-drift.mjs` tracks the `docs/i18n/<locale>/**.md` documentation mirrors — it never reads `src/i18n/messages/*.json` at all. (Its `.i18n-state.json` is also absent, so it self-skips, but bootstrapping it would not have helped: wrong surface.) New gate `scripts/i18n/check-ui-value-drift.mjs` is DIFF-AWARE rather than baseline-backed: it compares `en.json` at the merge base against the working tree, and for every key whose English value changed, reports any locale still holding an untouched translation. That choice deliberately freezes pre-existing debt — a diff cannot reveal which old English a long-standing translation came from, so the gate judges only what the current change touches, and unrelated PRs never pay for historical drift. The alternative, a per-key hash baseline over 11207 keys, would have cost a ~600 KB generated file (3x the largest existing baseline) churning on every i18n PR. Two ways to satisfy it: refresh the translations, or set them to `__MISSING__:<new english>` so the runtime serves the corrected English (cheaper-inference#7258) while the key queues for translation. When the string's MEANING changes, renaming the key is better still — a new key cannot inherit a stale translation, which is what cheaper-inference#8463 did. Wired blocking into the `i18n-ui-coverage` job (the `i18n` job is `continue-on-error: true`, so a gate there could not block anything). That job gains `fetch-depth: 0` because the gate needs the base ref; without it the gate self-skips with `base-unresolved`, mirroring `check-openapi-breaking`. `BASE_REF` is passed via `env:` and reaches git only through `execFileSync` argv — never a shell string. Verified against the real defect: rewriting an English value with translations left behind reports exactly 39 stale locales and exits 1; `--warn` exits 0; an unresolvable base exits 0 with `SKIP reason=base-unresolved`. Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…ten (cheaper-inference#8574) Closes the gap that let cheaper-inference#8463 ship. `oauthModal.googleOAuthWarning`'s English value was rewritten when the Antigravity login helper landed (cheaper-inference#5203); 39 of 43 locales kept a translation of the PREVIOUS English, which told operators to "copy the full URL and paste it below" — a flow that cannot complete for that provider family. Non-English users read confident, wrong instructions for months and no gate noticed. None of the three existing gates can see this class: - `sync-ui-keys.mjs` only backfills keys that are ABSENT, never ones that are STALE; - `check-ui-keys-coverage.mjs` counts key PRESENCE, so a stale translation scores as fully covered (all 43 locales sat at 99.6% throughout); - `check-translation-drift.mjs` tracks the `docs/i18n/<locale>/**.md` documentation mirrors — it never reads `src/i18n/messages/*.json` at all. (Its `.i18n-state.json` is also absent, so it self-skips, but bootstrapping it would not have helped: wrong surface.) New gate `scripts/i18n/check-ui-value-drift.mjs` is DIFF-AWARE rather than baseline-backed: it compares `en.json` at the merge base against the working tree, and for every key whose English value changed, reports any locale still holding an untouched translation. That choice deliberately freezes pre-existing debt — a diff cannot reveal which old English a long-standing translation came from, so the gate judges only what the current change touches, and unrelated PRs never pay for historical drift. The alternative, a per-key hash baseline over 11207 keys, would have cost a ~600 KB generated file (3x the largest existing baseline) churning on every i18n PR. Two ways to satisfy it: refresh the translations, or set them to `__MISSING__:<new english>` so the runtime serves the corrected English (cheaper-inference#7258) while the key queues for translation. When the string's MEANING changes, renaming the key is better still — a new key cannot inherit a stale translation, which is what cheaper-inference#8463 did. Wired blocking into the `i18n-ui-coverage` job (the `i18n` job is `continue-on-error: true`, so a gate there could not block anything). That job gains `fetch-depth: 0` because the gate needs the base ref; without it the gate self-skips with `base-unresolved`, mirroring `check-openapi-breaking`. `BASE_REF` is passed via `env:` and reaches git only through `execFileSync` argv — never a shell string. Verified against the real defect: rewriting an English value with translations left behind reports exactly 39 stale locales and exits 1; `--warn` exits 0; an unresolvable base exits 0 with `SKIP reason=base-unresolved`. Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
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.
Follow-up to #8463: make the bug that PR fixed impossible to reintroduce silently.
The gap
oauthModal.googleOAuthWarning's English value was rewritten when the Antigravity loginhelper landed (#5203). 39 of 43 locales kept a translation of the previous English:
That flow cannot complete for Google-loopback providers — the consent never redirects, so
there is no URL to copy. Non-English users read confident, wrong instructions for months.
No gate noticed, because none of the three can see this class:
sync-ui-keys.mjscheck-ui-keys-coverage.mjscheck-translation-drift.mjsdocs/i18n/<locale>/**.md— the documentation mirrors. It never readssrc/i18n/messages/*.json.Worth stating plainly: bootstrapping
check-translation-drift's missing.i18n-state.jsonwould not have caught this. Wrong surface — it is a docs-translation gate.
The gate
scripts/i18n/check-ui-value-drift.mjsis diff-aware, not baseline-backed. It comparesen.jsonat the merge base against the working tree; for every key whose English valuechanged, it reports any locale still holding an untouched translation.
Why diff-aware rather than a baseline
It freezes pre-existing debt by construction: a diff cannot reveal which old English a
long-standing translation was derived from, so the gate judges only what the current change
touches and unrelated PRs never pay for historical drift.
The alternative — a per-key hash baseline over 11,207 keys — would have cost a ~600 KB
generated file (3×
file-size-baseline.json, the largest existing baseline) that churns onevery i18n PR. Same protection, none of the artifact.
Consequence to be explicit about: already-stale translations stay invisible. This gate
stops the bleeding; it does not retroactively audit history.
CI wiring
Blocking in
i18n-ui-coverage, noti18n— thei18njob iscontinue-on-error: true, so a gate placed there could not block anything.That job gains
fetch-depth: 0: the gate needs the base ref, and on a shallow clone itself-skips with
SKIP reason=base-unresolved(mirroringcheck-openapi-breaking, whichcarries the same comment for the same reason).
BASE_REFis passed viaenv:and reachesgit only through
execFileSyncargv — never interpolated into a shell string.Tests
11 tests, written failing-first, against the exported pure core:
__MISSING__:→ not stale;frozen-debt semantics, asserted rather than assumed);
enis never its own target, and every affected locale is reported;innocuous diff);
null) compare without throwing.End-to-end verification against the real defect
Rewrote an English value in the working tree, leaving translations untouched:
check-ui-value-drift(strict)--warn--json{"ok": false, …}with the resolveddiffBaseBASE_REF=refs/heads/nao-existeSKIP reason=base-unresolvedValidation
i18n:check-value-drifton this branchi18n:check-ui-coveragecheck-docs-synccheck:any-budget:t11/check:tracked-artifactseslintcheck-workflowsratchet — pre-existing, not from this PR. It reports180 zizmor findings > baseline 176. Measuredorigin/release/v3.8.49in a detachedworktree: also 180. My workflow edit adds zero findings; the +4 is base drift that
predates this branch and needs either a fix or a re-baseline independently.
Docs
docs/architecture/QUALITY_GATES.md— thei18n-ui-coveragetable gains the gate, plus asubsection covering the incident, why the other three gates miss it, the diff-aware
trade-off, and the two remedies.