mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 03:12:36 +03:00
* 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.
216 lines
7.8 KiB
JavaScript
216 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* OmniRoute — Chinese terminology glossary consistency gate (zh-CN + zh-TW).
|
|
*
|
|
* Complements the existing parity (check-ui-keys-coverage.mjs) and ICU
|
|
* (validate_translation.py) checks with a native-quality layer:
|
|
* - glossary-synonym: a value uses a non-canonical synonym for a concept
|
|
* that has an actively-normalized canonical rendering
|
|
* (scripts/i18n/glossary/<locale>.json).
|
|
* - 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=ko
|
|
* node scripts/i18n/check-glossary-consistency.mjs --json
|
|
* node scripts/i18n/check-glossary-consistency.mjs --report # print, always exit 0
|
|
*/
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { hasUnblockedOccurrence } from "./glossary-normalize.mjs";
|
|
|
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(SCRIPT_DIR, "..", "..");
|
|
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),
|
|
// and deliberately conservative: an entry is only added once we've verified
|
|
// it doesn't collide with a legitimate, unrelated use of the same Chinese
|
|
// phrase elsewhere in the real catalog (e.g. a generic word for
|
|
// "authorization" is NOT a safe proxy for "OAuth was translated instead of
|
|
// left verbatim" — it fires on every unrelated auth string). Extend as new,
|
|
// non-colliding mistranslations are discovered in native review.
|
|
const KNOWN_MISTRANSLATIONS = {
|
|
DATA_DIR: ["数据目录"],
|
|
};
|
|
|
|
function logInfo(...parts) {
|
|
console.log(LOG_PREFIX, ...parts);
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function collectLeaves(obj, prefix = []) {
|
|
const leaves = [];
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const next = [...prefix, key];
|
|
if (isPlainObject(value)) {
|
|
leaves.push(...collectLeaves(value, next));
|
|
} else if (typeof value === "string") {
|
|
leaves.push({ path: next.join("."), value });
|
|
}
|
|
}
|
|
return leaves;
|
|
}
|
|
|
|
/**
|
|
* Pure consistency check — no I/O. Mirrors evaluateFileSizes' split in
|
|
* check-file-size.mjs (pure fn + separate CLI wrapper).
|
|
*
|
|
* @param {object} localeMessages - parsed src/i18n/messages/<locale>.json
|
|
* @param {object} glossary - parsed scripts/i18n/glossary/<locale>.json
|
|
* @param {string[]} protectedTerms - flat list from protected-terms.json
|
|
* @returns {{violations: Array<object>}}
|
|
*/
|
|
export function checkGlossaryConsistency(localeMessages, glossary, protectedTerms) {
|
|
const violations = [];
|
|
const leaves = collectLeaves(localeMessages || {});
|
|
|
|
const terms = glossary && glossary.terms ? glossary.terms : {};
|
|
for (const [concept, def] of Object.entries(terms)) {
|
|
const synonyms = Array.isArray(def.synonyms) ? def.synonyms : [];
|
|
const blockedPrefixes = Array.isArray(def.blockedPrefixes) ? def.blockedPrefixes : [];
|
|
for (const synonym of synonyms) {
|
|
if (!synonym) continue;
|
|
for (const leaf of leaves) {
|
|
if (hasUnblockedOccurrence(leaf.value, synonym, blockedPrefixes)) {
|
|
violations.push({
|
|
type: "glossary-synonym",
|
|
concept,
|
|
path: leaf.path,
|
|
found: synonym,
|
|
canonical: def.canonical,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const localeMistranslations = isPlainObject(glossary?.protectedTermMistranslations)
|
|
? glossary.protectedTermMistranslations
|
|
: {};
|
|
const protectedList = Array.isArray(protectedTerms) ? protectedTerms : [];
|
|
for (const term of protectedList) {
|
|
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)) {
|
|
violations.push({
|
|
type: "protected-term-altered",
|
|
term,
|
|
path: leaf.path,
|
|
found: bad,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { violations };
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const opts = { locale: "zh-CN", json: false, report: false };
|
|
for (const arg of argv.slice(2)) {
|
|
if (arg.startsWith("--locale=")) {
|
|
opts.locale = arg.slice(9);
|
|
} else if (arg === "--json") {
|
|
opts.json = true;
|
|
} else if (arg === "--report") {
|
|
opts.report = true;
|
|
} else if (arg === "--help" || arg === "-h") {
|
|
console.log(
|
|
[
|
|
"Usage: node scripts/i18n/check-glossary-consistency.mjs [options]",
|
|
"",
|
|
" --locale=<code> Locale to check (default zh-CN)",
|
|
" --json Emit JSON report to stdout",
|
|
" --report Print violations, exit 0 regardless",
|
|
].join("\n")
|
|
);
|
|
process.exit(0);
|
|
}
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
async function loadJson(filePath) {
|
|
const raw = await fs.readFile(filePath, "utf8");
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
async function main() {
|
|
const opts = parseArgs(process.argv);
|
|
|
|
const messagesPath = path.join(MESSAGES_DIR, `${opts.locale}.json`);
|
|
const glossaryPath = path.join(GLOSSARY_DIR, `${opts.locale}.json`);
|
|
const protectedPath = path.join(GLOSSARY_DIR, "protected-terms.json");
|
|
|
|
const [messages, glossary, protectedData] = await Promise.all([
|
|
loadJson(messagesPath),
|
|
loadJson(glossaryPath),
|
|
loadJson(protectedPath),
|
|
]);
|
|
const protectedTerms = Array.isArray(protectedData.terms) ? protectedData.terms : [];
|
|
|
|
const { violations } = checkGlossaryConsistency(messages, glossary, protectedTerms);
|
|
|
|
if (opts.json) {
|
|
process.stdout.write(
|
|
JSON.stringify({ locale: opts.locale, ok: violations.length === 0, violations }, null, 2) +
|
|
"\n"
|
|
);
|
|
if (violations.length && !opts.report) process.exit(1);
|
|
return;
|
|
}
|
|
|
|
if (violations.length === 0) {
|
|
logInfo(`PASS — ${opts.locale} has no glossary/protected-term drift.`);
|
|
return;
|
|
}
|
|
|
|
logInfo(`FAIL — ${violations.length} violation(s) in ${opts.locale}.`);
|
|
for (const v of violations.slice(0, 50)) {
|
|
if (v.type === "glossary-synonym") {
|
|
console.log(
|
|
` - [${v.concept}] ${v.path}: found "${v.found}", canonical is "${v.canonical}"`
|
|
);
|
|
} else {
|
|
console.log(` - [protected-term] ${v.path}: "${v.term}" rendered as "${v.found}"`);
|
|
}
|
|
}
|
|
if (violations.length > 50) {
|
|
console.log(` ... and ${violations.length - 50} more`);
|
|
}
|
|
if (!opts.report) process.exit(1);
|
|
}
|
|
|
|
const isDirectRun = import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
if (isDirectRun) {
|
|
main().catch((err) => {
|
|
console.error(LOG_PREFIX, "ERROR", err?.stack || err?.message || String(err));
|
|
process.exit(1);
|
|
});
|
|
}
|