mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 03:02:14 +03:00
[v3.8.50] feat(ci): extend i18n glossary-consistency gate to ko (#8244)
* 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.
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/<locale>.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)) {
|
||||
|
||||
55
scripts/i18n/glossary/ko.json
Normal file
55
scripts/i18n/glossary/ko.json
Normal file
@@ -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": ["우편번호"]
|
||||
}
|
||||
}
|
||||
@@ -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/<locale>.json.",
|
||||
"terms": [
|
||||
"OmniRoute",
|
||||
"OAuth",
|
||||
@@ -20,6 +20,15 @@
|
||||
"CLI",
|
||||
"Docker",
|
||||
"Electron",
|
||||
"Playwright"
|
||||
"Playwright",
|
||||
"ngrok",
|
||||
"Anthropic",
|
||||
"Claude",
|
||||
"Gemini",
|
||||
"Antigravity",
|
||||
"Tailscale",
|
||||
"VACUUM",
|
||||
"socks5",
|
||||
"ZIP"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user