chinese2english/scripts/merge-dict.mjs
spearzificial 68a7fc608d fix: translate the API copy that actually renders
Bundle translation worked, but the UI still showed Chinese. The visible text
does not come from plugin bundles at all: plugin descriptors, model and
provider names, and settings descriptions reach the browser as JSON over /api.
Measured in the Plugins view, all 76 distinct Chinese characters came from
/api/pluginManager/listBundles, which the bundle seam cannot see.

Add a second seam on the typert gateway. It resolves its own dispatchRpc per
request, so overriding that instance method intercepts every /api call without
claiming the interceptor slot the gateway already owns.

Rewriting JSON is deliberately narrow. An allowlist covers the six endpoints
that return server-authored copy; everything else, notably /api/session/list,
carries the operator's own prompts and transcripts and is left alone.

Three shapes needed handling, each found by testing against the live server:

- Locale maps ({ zh, en }) resolve to the English sibling, because the author's
  own English beats a dictionary pass.
- Bilingual descriptors joined by " · " keep their English half, which covers
  every such descriptor without a dictionary entry.
- Keys are not always trustworthy: dshmarket publishes its bilingual string
  under `en` and a short English one under `zh`, and one plugin repeats the same
  string under both. A locale map is therefore detected by shape, and an English
  sibling is only used when it is itself free of Chinese.

Also harvest author-written { zh, en } pairs from plugin locale maps (331
entries) as the highest-precedence dictionary source, and extend the curated
supplement with the vocabulary that unlocks whole families at once: "免费" alone
covers about twenty model names.

Measured on a live server: 107 -> 0 visible Chinese characters across the home,
Plugins, and Automation tasks views, 0 residual Chinese across all six
allowlisted endpoints, no page errors, and both bundle combos still compile.
2026-10-15 23:44:17 +05:30

72 lines
2.8 KiB
JavaScript

import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
// Merges the two translation sources already in this repo into one canonical
// dictionary payload. Dependency-free; rerun with `npm run dict`.
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const pairs = new Map();
const marketPath = join(root, "dshmarket_dict.json");
if (existsSync(marketPath)) {
const data = JSON.parse(readFileSync(marketPath, "utf8"));
for (const [zh, en] of Object.entries(data)) pairs.set(zh, en);
}
// patch-plugins.py carries its pairs inline as `"zh": "en",` lines.
const patchPath = join(root, "patch-plugins.py");
if (existsSync(patchPath)) {
const src = readFileSync(patchPath, "utf8");
const body = src.slice(src.indexOf("UI_TRANSLATIONS = {"));
const re = /^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*$/gm;
let m;
while ((m = re.exec(body))) pairs.set(m[1], m[2]);
}
// Curated supplement, measured against the real served bundles. Merged last so
// it overrides anything the harvested sources got wrong.
const extraPath = join(root, "src", "dictionary.extra.json");
if (existsSync(extraPath)) {
const data = JSON.parse(readFileSync(extraPath, "utf8"));
for (const [zh, en] of Object.entries(data)) pairs.set(zh, en);
}
// Author-authored `{ zh, en }` pairs lifted from plugin locale maps. This is the
// highest-quality source: a human wrote both sides, so it beats every
// machine-harvested pair and is merged last.
const localePairsPath = join(root, "locale-pairs.json");
if (existsSync(localePairsPath)) {
const data = JSON.parse(readFileSync(localePairsPath, "utf8"));
for (const [zh, en] of Object.entries(data)) pairs.set(zh, en);
}
// A handful of upstream entries are Malay, not English. Emitting them would
// inject foreign words into an English UI, so they are replaced by name.
const NON_ENGLISH = new Map([
["当前", "Current"],
["本地", "Local"],
]);
const clean = {};
let dropped = 0;
for (const [zh, en] of pairs) {
// Guard the substitution contract: a key must be Chinese and must actually
// differ from its replacement, otherwise rewriting is a no-op with risk.
if (!/[\u4e00-\u9fa5]/.test(zh) || !en || zh === en) {
dropped++;
continue;
}
clean[zh] = NON_ENGLISH.get(zh) ?? en;
}
const sorted = Object.fromEntries(
Object.entries(clean).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0])),
);
writeFileSync(join(root, "src", "dictionary.json"), JSON.stringify(sorted, null, 2) + "\n");
const lengths = Object.keys(sorted).map((k) => k.length);
console.log(
`dictionary: ${Object.keys(sorted).length} entries` +
` (dropped ${dropped}), key length ${Math.min(...lengths)}-${Math.max(...lengths)}`,
);