mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
deepMergeFallback in src/i18n/request.ts only substituted the English fallback value when a key was entirely undefined. Keys backfilled by scripts/i18n/sync-ui-keys.mjs with the __MISSING__:<english> sentinel exist on the target object, so they passed through untouched and were rendered raw to the user (395 zh-TW keys, 337 pt-BR keys, systemic across locales). Now any target leaf that still carries the __MISSING__: prefix is treated the same as an absent key, so the clean EN value wins. Does not touch the underlying translation content (395/337 strings) - that is a separate content workstream.
This commit is contained in:
committed by
GitHub
parent
7fcfbcd8fd
commit
12b25d5e0d
1
changelog.d/fixes/7258-zhtw-missing-placeholder.md
Normal file
1
changelog.d/fixes/7258-zhtw-missing-placeholder.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(i18n): treat `__MISSING__:` sync-script placeholders as absent so the EN fallback renders instead of the raw sentinel (#7258)
|
||||
@@ -5,10 +5,25 @@ import type { Locale } from "./config";
|
||||
|
||||
const FALLBACK_LOCALE = "en";
|
||||
|
||||
/**
|
||||
* Sentinel prefix written by `scripts/i18n/sync-ui-keys.mjs` when backfilling a
|
||||
* locale file with an untranslated key: `__MISSING__:<english value>`. Kept in
|
||||
* sync manually with the scripts (plain .mjs, no shared TS module) — see
|
||||
* `scripts/i18n/sync-ui-keys.mjs` and `scripts/i18n/check-ui-keys-coverage.mjs`.
|
||||
*/
|
||||
export const PLACEHOLDER_PREFIX = "__MISSING__:";
|
||||
|
||||
function isUntranslatedPlaceholder(value: unknown): boolean {
|
||||
return typeof value === "string" && value.startsWith(PLACEHOLDER_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge that mutates `target` with values from `source`.
|
||||
* If both have an object at the same key, recurse.
|
||||
* Otherwise prefer the existing value in `target` (locale-specific wins).
|
||||
* Otherwise prefer the existing value in `target` (locale-specific wins) —
|
||||
* unless the target value is an untranslated `__MISSING__:` sentinel written
|
||||
* by the i18n sync script, in which case it is treated as absent so the
|
||||
* clean English fallback value wins instead (#7258).
|
||||
*/
|
||||
export function deepMergeFallback(
|
||||
target: Record<string, unknown>,
|
||||
@@ -27,7 +42,7 @@ export function deepMergeFallback(
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
deepMergeFallback(targetValue as Record<string, unknown>, sourceValue as Record<string, unknown>);
|
||||
} else if (targetValue === undefined) {
|
||||
} else if (targetValue === undefined || isUntranslatedPlaceholder(targetValue)) {
|
||||
target[key] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
123
tests/unit/i18n-missing-placeholder-fallback.test.ts
Normal file
123
tests/unit/i18n-missing-placeholder-fallback.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Regression test for #7258 — zh-TW (and other locales) rendering the raw
|
||||
* `__MISSING__:<english>` sentinel written by `scripts/i18n/sync-ui-keys.mjs`
|
||||
* instead of falling back to the clean English value.
|
||||
*
|
||||
* `deepMergeFallback` (src/i18n/request.ts) previously only substituted the
|
||||
* EN value when a key was entirely `undefined`; a key that existed but still
|
||||
* carried the untranslated placeholder passed through untouched and was
|
||||
* rendered verbatim to the user.
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import { deepMergeFallback, PLACEHOLDER_PREFIX } from "../../src/i18n/request.ts";
|
||||
|
||||
const messagesDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
"..",
|
||||
"src",
|
||||
"i18n",
|
||||
"messages"
|
||||
);
|
||||
|
||||
function loadLocale(locale: string): Record<string, unknown> {
|
||||
const raw = readFileSync(path.join(messagesDir, `${locale}.json`), "utf8");
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function collectPlaceholderLeaves(
|
||||
node: unknown,
|
||||
pathPrefix: string,
|
||||
out: string[]
|
||||
): void {
|
||||
if (node === null || typeof node !== "object") {
|
||||
if (typeof node === "string" && node.startsWith(PLACEHOLDER_PREFIX)) {
|
||||
out.push(pathPrefix);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node)) return;
|
||||
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
||||
collectPlaceholderLeaves(value, pathPrefix ? `${pathPrefix}.${key}` : key, out);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Focused repro: the exact keys from the issue report
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("#7258 repro: zh-TW keys carry a raw __MISSING__: placeholder before the fix is exercised", () => {
|
||||
const zhTW = loadLocale("zh-TW");
|
||||
const leaves: string[] = [];
|
||||
collectPlaceholderLeaves(zhTW, "", leaves);
|
||||
assert.ok(
|
||||
leaves.length > 0,
|
||||
"expected zh-TW.json to still contain __MISSING__: placeholders (translation content backlog)"
|
||||
);
|
||||
});
|
||||
|
||||
test("#7258: deepMergeFallback replaces an untranslated __MISSING__ placeholder with the EN fallback value", () => {
|
||||
const target: Record<string, unknown> = {
|
||||
localUsageCommand: `${PLACEHOLDER_PREFIX}Run this command locally`,
|
||||
};
|
||||
const source: Record<string, unknown> = {
|
||||
localUsageCommand: "Run this command locally",
|
||||
};
|
||||
const result = deepMergeFallback(target, source);
|
||||
assert.equal(result.localUsageCommand, "Run this command locally");
|
||||
assert.ok(!(result.localUsageCommand as string).startsWith(PLACEHOLDER_PREFIX));
|
||||
});
|
||||
|
||||
test("#7258: deepMergeFallback still lets a real (non-placeholder) locale value win", () => {
|
||||
const target: Record<string, unknown> = { greeting: "Hola" };
|
||||
const source: Record<string, unknown> = { greeting: "Hello" };
|
||||
const result = deepMergeFallback(target, source);
|
||||
assert.equal(result.greeting, "Hola");
|
||||
});
|
||||
|
||||
test("#7258: deepMergeFallback replaces nested placeholder leaves too", () => {
|
||||
const target: Record<string, unknown> = {
|
||||
ns: { a: `${PLACEHOLDER_PREFIX}English A`, b: "translated B" },
|
||||
};
|
||||
const source: Record<string, unknown> = {
|
||||
ns: { a: "English A", b: "English B" },
|
||||
};
|
||||
const result = deepMergeFallback(target, source);
|
||||
const ns = result.ns as Record<string, unknown>;
|
||||
assert.equal(ns.a, "English A");
|
||||
assert.equal(ns.b, "translated B", "already-translated sibling key is untouched");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. General regression: for every shipped locale, the REAL production merge
|
||||
// (locale ⟵ EN fallback) leaves zero raw __MISSING__: leaves.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("#7258: after the real EN-fallback merge, no locale has a raw __MISSING__: leaf", () => {
|
||||
const en = loadLocale("en");
|
||||
const locales = readdirSync(messagesDir)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => f.replace(/\.json$/, ""))
|
||||
.filter((locale) => locale !== "en");
|
||||
|
||||
assert.ok(locales.length > 0, "expected at least one non-EN locale file");
|
||||
|
||||
const offenders: Record<string, string[]> = {};
|
||||
for (const locale of locales) {
|
||||
const localeMessages = loadLocale(locale);
|
||||
const merged = deepMergeFallback({ ...localeMessages }, en);
|
||||
const leaves: string[] = [];
|
||||
collectPlaceholderLeaves(merged, "", leaves);
|
||||
if (leaves.length > 0) offenders[locale] = leaves;
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
{},
|
||||
`expected zero __MISSING__: leaves after EN fallback merge, found: ${JSON.stringify(offenders)}`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user