The plugin never translated anything. Its client half monkey-patched locale internals that do not mutate the locale snapshot, and its DOM walker was disabled after it crashed the marketplace UI. Nothing else was left. Move translation to the bundle transport instead. Every third-party bundle is served through /plugins/ by the clientModules service, so wrapping bundleResource reaches all of them before any code runs, with no client-side ordering race and no React tree mutation. Two bugs surfaced while verifying against the live server: - The script check tested the path before the query string, but combo requests are /plugins/??a/client.js,b/client.js&rev=... where the path is just /plugins/. Every request was rejected. - bundleResource yields a Buffer, not a string, so the string guard rejected every request a second time. Rewriting is all-or-nothing per literal: Chinese has no word boundaries, so replacing known keys wherever they appear corrupts larger words (当前工作区 became "Saat IniWorkspace"). A literal is translated only when every Chinese character is covered by a dictionary entry. Two upstream entries were Malay rather than English and are now corrected. Each rewritten bundle is parsed with new Function before being served; if that fails the original bytes are returned, so a bad dictionary entry cannot break the UI. Verified against the live bundles: both combos compile, and a browser A/B shows 105 Chinese characters in the Plugins view without the plugin and 0 with it. The dictionary grows to 1,195 entries (45% of literals, zero partial rewrites) via a curated supplement merged last so it overrides harvested sources.
63 lines
2.3 KiB
JavaScript
63 lines
2.3 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);
|
|
}
|
|
|
|
// 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)}`,
|
|
);
|