From a687e2b7e3032ffc4e546445a00e8c08ed52ee00 Mon Sep 17 00:00:00 2001 From: Michael YC JO Date: Tue, 11 Aug 2026 16:23:46 +0900 Subject: [PATCH] [v3.8.50] feat(ci): extend i18n glossary-consistency gate to ko (#8244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(dashboard): correct machine-translated Korean UI strings in ko.json Fix 527 mistranslated values in the Korean locale, all verified against the en.json source: - Restore protected product/protocol names garbled by machine translation (응록→ngrok, 인류/인류학→Anthropic, 쌍둥이자리→Gemini, 반중력→Antigravity, 꼬리비늘 깔때기→Tailscale Funnel, 진공→VACUUM, 우편번호→ZIP) - Fix wrong-sense homonym translations (달리기→실행 중 for Running, 장애인→비활성화됨 for Disabled, 열쇠→키 for Key, 안타→적중 for Hits, 유물→아티팩트 for Artifacts, 건강검진→상태 확인 for Healthcheck) - Repair translated identifiers that broke literal values (양말5→socks5, 볼록-세션-id→convex-session-id, 채팅/완료→chat/completions, 메시지/보내기→message/send JSON-RPC methods) - Replace key-name dumps shipped as values ("Table Name", "Overview Title", "Cli Tools Redirect Title" etc.) with real Korean translations - Unify ngrok casing (Ngrok→ngrok) and trailing punctuation with the English source; align terminology across fixes (공급자, 폴백, 사용자 정의) All {placeholder} tokens, markdown, and protected terms preserved verbatim; i18n UI coverage and ko validation gates pass. * feat(ci): extend i18n glossary-consistency gate to ko Follow-up to #8224 (ko.json mistranslation cleanup): the glossary gate only checked zh-CN, leaving the Korean catalog unguarded against the next machine-translation run reintroducing the garbage it fixed. - Add scripts/i18n/glossary/ko.json: 9 canonical concepts (provider, fallback, running/disabled states, key, export, healthcheck, port, artifacts) plus protectedTermMistranslations for 10 verified garbled renderings (응록→ngrok, 인류→Anthropic, 쌍둥이자리→Gemini, 반중력→Antigravity, 꼬리비늘→Tailscale, 진공→VACUUM, 양말5→socks5, 우편번호→ZIP, 클로드→Claude, 옴니루트→OmniRoute) - Extend check-glossary-consistency.mjs to merge per-locale protectedTermMistranslations from the glossary file with the legacy zh-CN KNOWN_MISTRANSLATIONS map (behavior for zh-CN unchanged) - Add ngrok/Anthropic/Claude/Gemini/Antigravity/Tailscale/VACUUM/ socks5/ZIP to protected-terms.json - Wire --locale=ko into the i18n-glossary CI job and add the i18n:check-glossary:ko npm script - Tests: merge semantics (3 new unit tests), #8224 regression guards for src + bin/cli ko catalogs, and real-file pass assertions for ko Every enforced synonym/mistranslation was verified to have zero occurrences in both real ko catalogs; collision-prone candidates (안타 ⊂ 안타깝게도, 배우 ⊂ 배우기) were deliberately excluded. --- .github/workflows/ci.yml | 8 ++- package.json | 1 + scripts/i18n/check-glossary-consistency.mjs | 18 ++++- scripts/i18n/glossary/ko.json | 55 ++++++++++++++ scripts/i18n/glossary/protected-terms.json | 13 +++- .../i18n-glossary-consistency-check.test.ts | 72 +++++++++++++++++++ 6 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 scripts/i18n/glossary/ko.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feddcc6cd4..01facdc7c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -501,11 +501,13 @@ jobs: BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }} run: node scripts/i18n/check-ui-value-drift.mjs - # #8038: cheap single-locale glossary/protected-terms consistency gate — + # #8038: cheap glossary/protected-terms consistency gate — # complements i18n-ui-coverage (key parity) and the ICU `i18n` job below # without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage. + # ko added after the #8224 ko.json mistranslation cleanup so the fixed + # terminology cannot silently regress on the next machine-translation run. i18n-glossary-zhcn: - name: i18n Glossary (zh-CN) + name: i18n Glossary (zh-CN, ko) runs-on: ubuntu-latest needs: changes if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }} @@ -1334,7 +1336,7 @@ jobs: echo "| Lint | $(status '${{ needs.lint.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Docs Sync (Strict) | $(status '${{ needs.docs-sync-strict.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| i18n UI Coverage | $(status '${{ needs.i18n-ui-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY" - echo "| i18n Glossary (zh-CN) | $(status '${{ needs.i18n-glossary-zhcn.result }}') |" >> "$GITHUB_STEP_SUMMARY" + echo "| i18n Glossary (zh-CN, ko) | $(status '${{ needs.i18n-glossary-zhcn.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY" diff --git a/package.json b/package.json index 1cfcd8ae62..a3834cadd1 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "i18n:check-value-drift": "node scripts/i18n/check-ui-value-drift.mjs", "i18n:check-value-drift:warn": "node scripts/i18n/check-ui-value-drift.mjs --warn", "i18n:check-glossary": "node scripts/i18n/check-glossary-consistency.mjs", + "i18n:check-glossary:ko": "node scripts/i18n/check-glossary-consistency.mjs --locale=ko", "check:node-runtime": "node --import tsx scripts/check/check-supported-node-runtime.ts", "check:pack-artifact": "node --import tsx scripts/build/validate-pack-artifact.ts", "check:pack-boot": "node scripts/check/check-pack-boot.mjs", diff --git a/scripts/i18n/check-glossary-consistency.mjs b/scripts/i18n/check-glossary-consistency.mjs index 8acfc288e0..e09ebba708 100644 --- a/scripts/i18n/check-glossary-consistency.mjs +++ b/scripts/i18n/check-glossary-consistency.mjs @@ -10,10 +10,13 @@ * - protected-term-altered: a value renders a protected product/provider/ * protocol/CLI/env identifier (scripts/i18n/glossary/protected-terms.json) * using a known incorrect translation instead of leaving it verbatim. + * Known incorrect renderings come from the legacy KNOWN_MISTRANSLATIONS + * map below (zh-CN) merged with the optional per-locale + * `protectedTermMistranslations` object in the locale's glossary file (ko). * * Usage: * node scripts/i18n/check-glossary-consistency.mjs # zh-CN, exit 1 on drift - * node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN + * node scripts/i18n/check-glossary-consistency.mjs --locale=ko * node scripts/i18n/check-glossary-consistency.mjs --json * node scripts/i18n/check-glossary-consistency.mjs --report # print, always exit 0 */ @@ -30,6 +33,9 @@ const MESSAGES_DIR = path.join(ROOT, "src", "i18n", "messages"); const GLOSSARY_DIR = path.join(SCRIPT_DIR, "glossary"); const LOG_PREFIX = "[i18n-glossary]"; +// Legacy zh-CN map of known incorrect renderings for protected terms — newer +// locales (ko) keep theirs in `protectedTermMistranslations` inside their +// scripts/i18n/glossary/.json instead of growing this constant. // Small, maintained map of known incorrect renderings for protected terms — // identifiers that must survive translation verbatim. NOT exhaustive by // design (a full back-translation model is out of scope for a static gate), @@ -97,10 +103,16 @@ export function checkGlossaryConsistency(localeMessages, glossary, protectedTerm } } + const localeMistranslations = isPlainObject(glossary?.protectedTermMistranslations) + ? glossary.protectedTermMistranslations + : {}; const protectedList = Array.isArray(protectedTerms) ? protectedTerms : []; for (const term of protectedList) { - const badRenderings = KNOWN_MISTRANSLATIONS[term]; - if (!badRenderings || badRenderings.length === 0) continue; + const fromGlossary = Array.isArray(localeMistranslations[term]) + ? localeMistranslations[term] + : []; + const badRenderings = [...(KNOWN_MISTRANSLATIONS[term] || []), ...fromGlossary]; + if (badRenderings.length === 0) continue; for (const bad of badRenderings) { for (const leaf of leaves) { if (leaf.value.includes(bad)) { diff --git a/scripts/i18n/glossary/ko.json b/scripts/i18n/glossary/ko.json new file mode 100644 index 0000000000..4deb69a721 --- /dev/null +++ b/scripts/i18n/glossary/ko.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "locale": "ko", + "description": "Canonical ko terminology for recurring OmniRoute concepts. Consumed by scripts/i18n/check-glossary-consistency.mjs. Each concept lists the canonical translation plus any non-canonical synonym that is actively normalized (drift enforced by the consistency gate). Concepts whose `synonyms` array is empty are seeded for documentation only — the catalog still uses more than one legitimate rendering for them today (e.g. 공급자/제공자, 폴백/대체), so enforcement is deferred to a follow-up normalization pass. Every enforced synonym and mistranslation below was verified to have zero legitimate occurrences in the real src + bin/cli ko catalogs before being added (collision policy mirrors the KNOWN_MISTRANSLATIONS note in the checker script — e.g. 안타 (Hits) is deliberately NOT enforced because it is a substring of the legitimate 안타깝게도).", + "terms": { + "provider": { + "canonical": "공급자", + "synonyms": [] + }, + "fallback": { + "canonical": "폴백", + "synonyms": [] + }, + "running (status)": { + "canonical": "실행 중", + "synonyms": ["달리기"] + }, + "disabled (status)": { + "canonical": "비활성화됨", + "synonyms": ["장애인"] + }, + "key (credential)": { + "canonical": "키", + "synonyms": ["열쇠"] + }, + "export (action)": { + "canonical": "내보내기", + "synonyms": ["수출"] + }, + "healthcheck": { + "canonical": "상태 확인", + "synonyms": ["건강검진"] + }, + "port (network)": { + "canonical": "포트", + "synonyms": ["항구"] + }, + "artifacts": { + "canonical": "아티팩트", + "synonyms": ["유물"] + } + }, + "protectedTermMistranslations": { + "ngrok": ["응록"], + "Anthropic": ["인류", "앤트로픽"], + "Claude": ["클로드"], + "Gemini": ["쌍둥이자리"], + "Antigravity": ["반중력"], + "OmniRoute": ["옴니루트"], + "Tailscale": ["꼬리비늘"], + "VACUUM": ["진공"], + "socks5": ["양말5"], + "ZIP": ["우편번호"] + } +} diff --git a/scripts/i18n/glossary/protected-terms.json b/scripts/i18n/glossary/protected-terms.json index b99d4aebe0..4d30facb49 100644 --- a/scripts/i18n/glossary/protected-terms.json +++ b/scripts/i18n/glossary/protected-terms.json @@ -1,5 +1,5 @@ { - "description": "Product/provider/model/protocol/header/CLI/env/identifier names that must appear verbatim (untranslated) inside any zh-CN localized string that mentions them. Distinct from untranslatable-keys.json, which excludes whole KEYS from drift checks at key-granularity; this list is consumed by scripts/i18n/check-glossary-consistency.mjs to flag a VALUE that mentions the concept but altered/translated the protected term itself.", + "description": "Product/provider/model/protocol/header/CLI/env/identifier names that must appear verbatim (untranslated) inside any localized string that mentions them (gated locales: zh-CN, ko). Distinct from untranslatable-keys.json, which excludes whole KEYS from drift checks at key-granularity; this list is consumed by scripts/i18n/check-glossary-consistency.mjs to flag a VALUE that mentions the concept but altered/translated the protected term itself. Known incorrect renderings live per-locale: legacy zh-CN entries in the checker's KNOWN_MISTRANSLATIONS map, newer locales in `protectedTermMistranslations` inside scripts/i18n/glossary/.json.", "terms": [ "OmniRoute", "OAuth", @@ -20,6 +20,15 @@ "CLI", "Docker", "Electron", - "Playwright" + "Playwright", + "ngrok", + "Anthropic", + "Claude", + "Gemini", + "Antigravity", + "Tailscale", + "VACUUM", + "socks5", + "ZIP" ] } diff --git a/tests/unit/i18n-glossary-consistency-check.test.ts b/tests/unit/i18n-glossary-consistency-check.test.ts index 0b9b8a8f2d..b52ffe7e7a 100644 --- a/tests/unit/i18n-glossary-consistency-check.test.ts +++ b/tests/unit/i18n-glossary-consistency-check.test.ts @@ -97,6 +97,78 @@ test("regression: bin/cli/locales/zh-CN.json no longer contains 提供商", () = assert.equal(raw.includes("提供商"), false); }); +test("glossary-file protectedTermMistranslations are merged into the protected-term check", () => { + const koGlossary = { + version: 1, + locale: "ko", + terms: {}, + protectedTermMistranslations: { + ngrok: ["응록"], + }, + }; + const messages = { endpoint: { ngrokTitle: "응록 터널" } }; + const { violations } = checkGlossaryConsistency(messages, koGlossary, ["ngrok"]); + assert.equal(violations.length, 1); + assert.equal(violations[0].type, "protected-term-altered"); + assert.equal(violations[0].term, "ngrok"); + assert.equal(violations[0].found, "응록"); +}); + +test("protectedTermMistranslations for a term absent from protected-terms.json are inert", () => { + const koGlossary = { + version: 1, + locale: "ko", + terms: {}, + protectedTermMistranslations: { + ngrok: ["응록"], + }, + }; + const messages = { endpoint: { ngrokTitle: "응록 터널" } }; + // "ngrok" not in the protected list → the glossary entry alone must not fire. + const { violations } = checkGlossaryConsistency(messages, koGlossary, ["DATA_DIR"]); + assert.deepEqual(violations, []); +}); + +test("legacy KNOWN_MISTRANSLATIONS still fire when the glossary has no mistranslation map", () => { + const messages = { settings: { dataDirHint: "存储在 数据目录 中" } }; + const { violations } = checkGlossaryConsistency(messages, glossary, ["DATA_DIR"]); + assert.equal(violations.length, 1); + assert.equal(violations[0].term, "DATA_DIR"); +}); + +// Regression guard for the #8224 ko.json mistranslation cleanup: the garbled +// product names and wrong-sense homonyms must not reappear in either ko catalog +// (e.g. via a future machine-translation run). +for (const badTerm of ["응록", "인류", "쌍둥이자리", "반중력", "달리기", "장애인"]) { + test(`regression: src/i18n/messages/ko.json no longer contains ${badTerm}`, () => { + const raw = readFileSync(path.join(ROOT, "src/i18n/messages/ko.json"), "utf8"); + assert.equal(raw.includes(badTerm), false); + }); + + test(`regression: bin/cli/locales/ko.json no longer contains ${badTerm}`, () => { + const raw = readFileSync(path.join(ROOT, "bin/cli/locales/ko.json"), "utf8"); + assert.equal(raw.includes(badTerm), false); + }); +} + +test("real ko.json + real ko glossary + real protected terms pass the gate", () => { + const realMessages = JSON.parse( + readFileSync(path.join(ROOT, "src/i18n/messages/ko.json"), "utf8") + ); + const realGlossary = JSON.parse( + readFileSync(path.join(ROOT, "scripts/i18n/glossary/ko.json"), "utf8") + ); + const realProtected = JSON.parse( + readFileSync(path.join(ROOT, "scripts/i18n/glossary/protected-terms.json"), "utf8") + ); + const { violations } = checkGlossaryConsistency( + realMessages, + realGlossary, + realProtected.terms + ); + assert.deepEqual(violations, []); +}); + test("real zh-CN.json + real glossary + real protected terms pass the gate", () => { const realMessages = JSON.parse( readFileSync(path.join(ROOT, "src/i18n/messages/zh-CN.json"), "utf8")