mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
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`. Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
// @ts-expect-error — plain .mjs gate script, no type declarations by design.
|
|
import { findStaleTranslations, flattenLeaves } from "../../scripts/i18n/check-ui-value-drift.mjs";
|
|
|
|
// Why this gate exists (the #8463 defect):
|
|
//
|
|
// `googleOAuthWarning`'s English value was rewritten when the Antigravity login helper
|
|
// shipped (#5203). Nothing noticed that 39 of 43 locales still carried 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. Users saw confident, wrong instructions
|
|
// in their own language for months.
|
|
//
|
|
// Neither existing gate can see this:
|
|
// - `sync-ui-keys.mjs` only fills keys that are ABSENT, never ones that are STALE;
|
|
// - `check-ui-keys-coverage.mjs` counts key presence, so a stale translation scores
|
|
// as 100% covered;
|
|
// - `check-translation-drift.mjs` tracks `docs/i18n/<locale>/**.md` (documentation
|
|
// mirrors), not the UI message catalogs at all.
|
|
//
|
|
// So: when a PR edits an English VALUE, every locale still holding a translation derived
|
|
// from the old English is stale and must either be refreshed or flagged
|
|
// `__MISSING__:` (which makes the runtime fall back to the correct English, #7258).
|
|
|
|
test("flattenLeaves walks nested catalogs into dotted paths", () => {
|
|
assert.deepEqual(flattenLeaves({ a: { b: "x" }, c: "y" }), {
|
|
"a.b": "x",
|
|
c: "y",
|
|
});
|
|
});
|
|
|
|
test("an edited EN value with an untouched translation is STALE", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "old english" } },
|
|
headEn: { m: { k: "new english" } },
|
|
baseLocales: { "pt-BR": { m: { k: "português antigo" } } },
|
|
headLocales: { "pt-BR": { m: { k: "português antigo" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, [{ key: "m.k", locale: "pt-BR" }]);
|
|
});
|
|
|
|
test("a translation refreshed in the same diff is NOT stale", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "old english" } },
|
|
headEn: { m: { k: "new english" } },
|
|
baseLocales: { "pt-BR": { m: { k: "português antigo" } } },
|
|
headLocales: { "pt-BR": { m: { k: "português novo" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, []);
|
|
});
|
|
|
|
test("a translation demoted to __MISSING__: is NOT stale (runtime falls back to EN)", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "old english" } },
|
|
headEn: { m: { k: "new english" } },
|
|
baseLocales: { "pt-BR": { m: { k: "português antigo" } } },
|
|
headLocales: { "pt-BR": { m: { k: "__MISSING__:new english" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, []);
|
|
});
|
|
|
|
test("a locale that never had the key is NOT stale — that is coverage's business", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "old english" } },
|
|
headEn: { m: { k: "new english" } },
|
|
baseLocales: { de: {} },
|
|
headLocales: { de: {} },
|
|
});
|
|
|
|
assert.deepEqual(stale, []);
|
|
});
|
|
|
|
test("a BRAND-NEW EN key cannot be stale anywhere", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: {},
|
|
headEn: { m: { k: "brand new" } },
|
|
baseLocales: { "pt-BR": {} },
|
|
headLocales: { "pt-BR": { m: { k: "tradução nova" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, []);
|
|
});
|
|
|
|
test("an unchanged EN value never flags, however stale the translation actually is", () => {
|
|
// This is the deliberate "freeze existing debt" semantics: a diff-aware gate cannot
|
|
// know what a pre-existing translation was derived from, so it only judges what the
|
|
// current change touches. Pre-existing drift stays invisible instead of failing every
|
|
// unrelated PR.
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "same english" } },
|
|
headEn: { m: { k: "same english" } },
|
|
baseLocales: { "pt-BR": { m: { k: "tradução obsoleta de outra era" } } },
|
|
headLocales: { "pt-BR": { m: { k: "tradução obsoleta de outra era" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, []);
|
|
});
|
|
|
|
test("a renamed key (delete + add) flags nothing — the fix pattern used by #8463", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { oldName: "old english" } },
|
|
headEn: { m: { newName: "new english" } },
|
|
baseLocales: { "pt-BR": { m: { oldName: "português antigo" } } },
|
|
headLocales: { "pt-BR": { m: { newName: "__MISSING__:new english" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, []);
|
|
});
|
|
|
|
test("every affected locale is reported, and en itself is never a target", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "old" } },
|
|
headEn: { m: { k: "new" } },
|
|
baseLocales: {
|
|
en: { m: { k: "old" } },
|
|
"pt-BR": { m: { k: "antigo" } },
|
|
de: { m: { k: "alt" } },
|
|
fr: { m: { k: "__MISSING__:old" } },
|
|
},
|
|
headLocales: {
|
|
en: { m: { k: "new" } },
|
|
"pt-BR": { m: { k: "antigo" } },
|
|
de: { m: { k: "alt" } },
|
|
fr: { m: { k: "__MISSING__:old" } },
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(
|
|
stale.map((s) => s.locale).sort(),
|
|
["de", "pt-BR"],
|
|
"en must be excluded and __MISSING__ locales skipped"
|
|
);
|
|
});
|
|
|
|
test("a value that only changes whitespace still counts as an edit", () => {
|
|
// Conservative on purpose: trailing-space churn is rare, and treating it as a no-op
|
|
// would let a real reword slip through behind an innocuous-looking diff.
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { k: "hello" } },
|
|
headEn: { m: { k: "hello " } },
|
|
baseLocales: { de: { m: { k: "hallo" } } },
|
|
headLocales: { de: { m: { k: "hallo" } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, [{ key: "m.k", locale: "de" }]);
|
|
});
|
|
|
|
test("non-string leaves (numbers, booleans, null) are compared without throwing", () => {
|
|
const stale = findStaleTranslations({
|
|
baseEn: { m: { n: 1, b: true, z: null } },
|
|
headEn: { m: { n: 2, b: true, z: null } },
|
|
baseLocales: { de: { m: { n: 1, b: true, z: null } } },
|
|
headLocales: { de: { m: { n: 1, b: true, z: null } } },
|
|
});
|
|
|
|
assert.deepEqual(stale, [{ key: "m.n", locale: "de" }]);
|
|
});
|