fix(i18n): retranslate the verbatim-English leaves in all 65 catalogs; ratio gate now blocking (#13782)

PR-4 of the locale-expansion plan. 215,363 strings retranslated across the 65 catalogs with the new `sync-ui-keys --retranslate-identical`; the share of leaves still identical to English drops from a mean of 18.3 % to 1.8 % (Spanish 56 → 2.1). No `__MISSING__` marker or missing key is left; zh glossary normalised; pinned product/flag names kept English and allowlisted. Baseline tightened and the CI step `i18n real-translation ratio` is blocking from here on.

⚠️ base-red inherited: #12732
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-09-15 23:19:43 -03:00
committed by GitHub
parent 8f55d85d22
commit cde49c9372
73 changed files with 139191 additions and 138659 deletions

View File

@@ -93,12 +93,14 @@ function parseArgs(argv) {
locales: null,
dryRun: false,
translateMarkers: false,
retranslateIdentical: false,
concurrency: null,
batchSize: 1,
};
for (const arg of argv.slice(2)) {
if (arg === "--dry-run" || arg === "--dryrun") opts.dryRun = true;
else if (arg === "--translate-markers") opts.translateMarkers = true;
else if (arg === "--retranslate-identical") opts.retranslateIdentical = true;
else if (arg.startsWith("--locale=")) {
opts.locales = arg
.slice(9)
@@ -123,6 +125,9 @@ function parseArgs(argv) {
"Usage: node scripts/i18n/sync-ui-keys.mjs [options]",
"",
" --locale=<csv> Target locales (default: all except `en`)",
" --retranslate-identical Flag leaves still identical to English (outside",
" untranslatable-keys.json) as __MISSING__ so they get",
" retranslated — only meaningful with --translate-markers",
" --dry-run Report what would change, write nothing",
" --translate-markers Call the translation backend to translate every",
" __MISSING__:<en> placeholder",
@@ -156,6 +161,34 @@ function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/**
* PR-4 of the locale expansion: a leaf whose value is byte-identical to the English source is
* an untranslated copy unless `untranslatable-keys.json` says otherwise. Swap each one for a
* `__MISSING__:<en>` placeholder so `translatePlaceholders` picks it up, and return how many
* were flagged. Existing placeholders, real translations, empty strings and shape mismatches
* are left untouched. Mutates `merged` in place.
*/
export function markIdenticalAsMissing(merged, source, untranslatable, prefix = "") {
let count = 0;
for (const [key, sourceValue] of Object.entries(source)) {
if (FORBIDDEN_KEYS.has(key)) continue;
const fullKey = prefix ? `${prefix}.${key}` : key;
const targetValue = merged[key];
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
count += markIdenticalAsMissing(targetValue, sourceValue, untranslatable, fullKey);
} else if (
typeof sourceValue === "string" &&
sourceValue !== "" &&
targetValue === sourceValue &&
!untranslatable.has(fullKey)
) {
merged[key] = `${PLACEHOLDER_PREFIX}${sourceValue}`;
count += 1;
}
}
return count;
}
// Defensive: reject any key that could traverse into the object prototype
// chain when we copy/merge values across JSON trees. Our inputs are
// authored JSON we already control, but excluding these keys is a cheap
@@ -357,6 +390,13 @@ async function processLocale(locale, source, config, opts, backend) {
}
const { merged, addedPaths } = mergeMissing(source, target);
if (opts.retranslateIdentical && locale !== SOURCE_LOCALE) {
const allow = new Set(
(await loadJson(path.join(SCRIPT_DIR, "untranslatable-keys.json"))).keys ?? []
);
const flagged = markIdenticalAsMissing(merged, source, allow);
logInfo(`${locale}: ${flagged} English leaves flagged for retranslation`);
}
const placeholderCountBefore = countPlaceholders(merged);
let translateStats = { translated: 0, failed: 0 };

View File

@@ -1,11 +1,11 @@
{
"description": "Keys that should remain untranslated (technical terms, brand names, ICU formats, placeholders, etc.)",
"keys": [
"apiManager.modelsCount",
"a2aDashboard.metadata",
"a2aDashboard.ok",
"a2aDashboard.smokeSendSuccess",
"a2aDashboard.url",
"apiManager.modelsCount",
"cache.behaviorTwoTier",
"cache.dbEntriesSub",
"cache.model",
@@ -64,6 +64,7 @@
"endpoint.cloudflaredTitle",
"endpoint.mcpCardTitle",
"endpoint.rerank",
"featureFlags.definitions.SERVER_OWNED_TOOL_LOOP_ENABLED.label",
"header.a2a",
"header.mcp",
"health.cpu",
@@ -87,29 +88,29 @@
"landing.github",
"landing.npm",
"legal.listSeparator",
"legal.terms",
"legal.privacy",
"legal.terms",
"loggers.inputTokens",
"loggers.modelAZ",
"loggers.modelZA",
"loggers.outputTokens",
"loggers.proxy",
"logs.notAvailable",
"logs.endpoint",
"logs.proxy",
"logs.console",
"logs.request",
"logs.audit",
"logs.console",
"logs.endpoint",
"logs.notAvailable",
"logs.proxy",
"logs.request",
"mcpDashboard.apiKeyIdPlaceholder",
"mcpDashboard.offline",
"mcpDashboard.online",
"mcpDashboard.pid",
"mcpDashboard.tableAudit",
"media.interpolation",
"media.model",
"media.prompt",
"media.interpolation",
"media.upscale",
"media.samples",
"media.upscale",
"modals.awsBuilderId",
"modals.awsIamIdentity",
"modals.awsRegion",
@@ -126,8 +127,8 @@
"providers.chatPathPlaceholder",
"providers.codexAuthAppliedLocal",
"providers.codexAuthApplyFailed",
"providers.codexAuthExported",
"providers.codexAuthExportFailed",
"providers.codexAuthExported",
"providers.compatProtocolClaude",
"providers.compatProtocolOpenAI",
"providers.compatProtocolOpenAIResponses",
@@ -136,17 +137,17 @@
"providers.messagesApi",
"providers.messagesPath",
"providers.millisecondsAbbr",
"providers.modelsPathPlaceholder",
"providers.modeTest",
"providers.oauthLabel",
"providers.modelsPathPlaceholder",
"providers.oauth2Label",
"providers.oauthLabel",
"providers.okShort",
"providers.openRouterModelPlaceholder",
"providers.openai",
"providers.openaiBaseUrlPlaceholder",
"providers.openaiCompatibleModelPlaceholder",
"providers.openaiCompatibleName",
"providers.openaiPrefixPlaceholder",
"providers.openRouterModelPlaceholder",
"providers.prefixLabel",
"providers.proxy",
"providers.proxyConfiguredBySource",
@@ -154,50 +155,59 @@
"providers.responsesApi",
"providers.responsesPath",
"search.domainPlaceholder",
"search.fileSearch",
"search.rawJson",
"search.rerank",
"search.searchTypeWeb",
"search.search",
"search.searchTools",
"search.searchTypeWeb",
"search.webSearch",
"search.fileSearch",
"settings.ai",
"settings.aliasPatternPlaceholder",
"settings.aliasTargetPlaceholder",
"settings.auto",
"settings.currency",
"settings.language",
"settings.model",
"settings.modelNamePlaceholder",
"settings.ms",
"settings.p2c",
"settings.providersCommaSeparatedPlaceholder",
"settings.proxy",
"settings.p2c",
"settings.reasoning",
"settings.roundRobin",
"settings.rpm",
"settings.status",
"settings.whitelabeling",
"settings.theme",
"settings.language",
"settings.currency",
"settings.timezone",
"settings.whitelabeling",
"sidebar.cache",
"sidebar.cacheShort",
"sidebar.cliCode",
"sidebar.cliSection",
"sidebar.compressionStudio",
"sidebar.contextAggressive",
"sidebar.contextHeadroom",
"sidebar.contextLite",
"sidebar.contextSessionDedup",
"sidebar.debug",
"sidebar.debugSection",
"sidebar.helpSection",
"sidebar.primarySection",
"sidebar.systemSection",
"sidebar.trafficInspector",
"sidebar.version",
"stats.errors",
"stats.latency",
"stats.requests",
"stats.tokens",
"stats.latency",
"stats.errors",
"themesPage.dark",
"themesPage.light",
"themesPage.system",
"translator.auto",
"translator.chatTester",
"translator.detect",
"translator.detectedLanguage",
"translator.millisecondsShort",
"translator.model",
"translator.notAvailableSymbol",
@@ -208,21 +218,25 @@
"translator.translate",
"translator.translateFrom",
"translator.translateTo",
"translator.detect",
"translator.detectedLanguage",
"usage.cacheReadTokens",
"usage.cacheWriteTokens",
"usage.columnModel",
"usage.columnStatus",
"usage.completionTokens",
"usage.detailsRegex",
"usage.durationHoursShort",
"usage.durationMillisecondsShort",
"usage.durationMinutesShort",
"usage.durationSecondsShort",
"usage.inputTokens",
"usage.latencyP50",
"usage.latencyP95",
"usage.latencyP99",
"usage.notApplicable",
"usage.notAvailableSymbol",
"usage.outputTokens",
"usage.passSuffix",
"usage.promptTokens",
"usage.proxyTab",
"usage.reasonSeparator",
"usage.tierPlus",
@@ -230,12 +244,6 @@
"usage.tierUltra",
"usage.totalRequests",
"usage.totalTokens",
"usage.inputTokens",
"usage.outputTokens",
"usage.promptTokens",
"usage.completionTokens",
"usage.cacheReadTokens",
"usage.cacheWriteTokens",
"usage.warningThresholdPlaceholder"
]
}