From ab0179545a47ec58bf7a0794eab1f0f8e9fe2319 Mon Sep 17 00:00:00 2001
From: spearzificial <342299391+spearzificial@users.noreply.github.com>
Date: Sun, 18 Oct 2026 22:33:30 +0530
Subject: [PATCH] docs: record the measurement traps and script them
Coverage was over-reported twice, both times by measuring the wrong thing:
once by scanning a plugin root instead of the served bundle set, once with a
non-global regex that returned one hit per file instead of a character count.
Both produced a confident "100%" while the interface was still Chinese, so
the measurement is now part of the repo instead of being done by eye.
- scripts/served.mjs resolves the boot HTML's combo URLs to the on-disk files
the browser receives, searching lib/, client/, dist/ and the package root.
- scripts/measure.mts counts Chinese on the runtime output, decoding \uXXXX
and separating comment text from text that renders.
- README documents the merge order, the comment dictionary, and both traps.
---
README.md | 42 ++++++++++++++++++++++++-----
package.json | 4 ++-
scripts/measure.mts | 60 +++++++++++++++++++++++++++++++++++++++++
scripts/served.mjs | 66 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 164 insertions(+), 8 deletions(-)
create mode 100644 scripts/measure.mts
create mode 100644 scripts/served.mjs
diff --git a/README.md b/README.md
index f53318f..81f68f0 100644
--- a/README.md
+++ b/README.md
@@ -52,12 +52,14 @@ The client half does one thing: it selects the `en` locale through
- Text rendered inside an `
`, canvas, or other non-text payload.
- Content fetched from a third-party server at runtime (announcements, remote
catalogs) rather than from DSH itself.
-- The long tail of one-off strings the dictionary does not yet cover.
-
-The shipped dictionary holds roughly 1,570 entries. Coverage is concentrated on
-strings that appear repeatedly; rare one-off sentences are usually left in
-Chinese, which is why the all-or-nothing rule matters.
+- Chinese inside regular expressions that match Chinese *input*. `/失败|错误|无法/`
+ classifies a status message and `/^[一-龥]{2}$/` validates a two-character name;
+ translating those would change plugin behaviour instead of removing text.
+ This is 111 characters across four bundles, all of it code rather than copy.
+Everything the interface renders is translated. Measured through the runtime
+path over the bundles a live instance served: Chinese in string literals
+50,399 → 0, and Chinese anywhere in the served bytes 55,345 → 111.
---
@@ -77,7 +79,7 @@ is installed before the first bundle request.
## Development
```bash
-npm run dict # rebuild src/dictionary.json from its sources
+npm run dict # rebuild the dictionaries from their sources
npm run build # bundle the server and client halves into lib/
```
@@ -86,12 +88,38 @@ Dictionary sources, merged in this order (later wins):
1. `dshmarket_dict.json` — harvested pairs.
2. `UI_TRANSLATIONS` in `patch-plugins.py` — harvested pairs.
3. `src/dictionary.extra.json` — curated entries.
-4. `locale-pairs.json` — author-written `{ zh, en }` pairs lifted from plugin
+4. `src/dictionary.mt.json` — machine translations of harvested literals.
+5. `src/dictionary.residual.json` — literals measured as still untranslated.
+6. `locale-pairs.json` — author-written `{ zh, en }` pairs lifted from plugin
locale maps; highest quality, so it overrides everything above.
+Comments are a separate dictionary, `src/dictionary.comments.json`, built by
+`scripts/merge-comments.mjs`. A comment is replaced only on an exact
+whole-comment match, which keeps the box-drawing runs and `*` alignment that
+give those notes their structure intact.
+
Add or fix translations in `src/dictionary.extra.json`, then run
`npm run dict && npm run build`.
+### Measuring coverage
+
+Progress was over-reported twice, both times by measuring the wrong thing, so
+measurement is scripted rather than done by eye:
+
+```bash
+node scripts/served.mjs /tmp/boot.html served.json # resolve served bundles
+npm run measure served.json # count Chinese left
+```
+
+Save the boot HTML from a running instance first (`/?token=…`), because the
+served set is what reaches the browser. Two traps these scripts close:
+
+- **Scan the served set, not a plugin root.** 55 of the 58 bundles live in the
+ dsh install tree. A scan of the profile tree alone reports nearly everything
+ translated while whole screens stay Chinese.
+- **Decode before counting.** Bundlers emit non-ASCII as `\uXXXX`, so a bundle
+ can be entirely Chinese while containing no CJK character at all.
+
---
## License
diff --git a/package.json b/package.json
index 0daf34a..91025ad 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,9 @@
"mt": "node scripts/mt-fill.mjs",
"dict": "node scripts/merge-dict.mjs && node scripts/merge-comments.mjs",
"build": "node scripts/build.mjs",
- "build:dict": "node scripts/mt-fill.mjs && node scripts/merge-dict.mjs && node scripts/merge-comments.mjs && node scripts/build.mjs"
+ "build:dict": "node scripts/mt-fill.mjs && node scripts/merge-dict.mjs && node scripts/merge-comments.mjs && node scripts/build.mjs",
+ "served": "node scripts/served.mjs",
+ "measure": "tsx scripts/measure.mts"
},
"peerDependencies": {
"@deepseek-ai/cordis": ">=4.0.0"
diff --git a/scripts/measure.mts b/scripts/measure.mts
new file mode 100644
index 0000000..7017b54
--- /dev/null
+++ b/scripts/measure.mts
@@ -0,0 +1,60 @@
+// Measures Chinese left in the served bundles, through the real runtime path.
+//
+// Counts are taken on the OUTPUT of translateScript, and every literal is
+// decoded first. Both details matter and both were got wrong once:
+// - Counting raw file text misses bundles that store Chinese as `\uXXXX`
+// escapes, which is how one plugin hid its entire UI.
+// - A non-global regex with String.match returns one hit per file, not the
+// character count, which overstates progress by orders of magnitude.
+//
+// Usage: node --import tsx scripts/measure.mts
+import { readFileSync } from "node:fs";
+import { translateScript } from "../src/rewrite.js";
+import { scanLiterals, decodeRaw } from "../src/strings.js";
+
+const CJK = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/g;
+const count = (text) => (text.match(CJK) ?? []).length;
+const decodeEscapes = (text) =>
+ text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
+
+// Chinese inside a /* ... */ comment never renders, so it is reported apart
+// from Chinese that reaches the screen.
+function outsideComments(text) {
+ return count(text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, ""));
+}
+
+const files = JSON.parse(readFileSync(process.argv[2] ?? "served.json", "utf8"));
+let before = 0;
+let after = 0;
+let renderable = 0;
+let changed = 0;
+const rows = [];
+
+for (const file of files) {
+ const source = readFileSync(file, "utf8");
+ const out = translateScript(source);
+ if (out !== source) changed++;
+
+ // Literal-level before/after, which is what a user can read.
+ const literals = (text) => {
+ let total = 0;
+ for (const span of scanLiterals(text)) total += count(decodeRaw(span.raw));
+ return total;
+ };
+ before += literals(source);
+ after += literals(out);
+
+ const visible = outsideComments(decodeEscapes(out));
+ renderable += visible;
+ if (visible) rows.push({ visible, total: count(decodeEscapes(out)), file });
+}
+
+console.log(`bundles: ${files.length} rewritten: ${changed}`);
+console.log(`Chinese in string literals: ${before} -> ${after} (${(100 - (after / before) * 100).toFixed(2)}% removed)`);
+console.log(`Chinese outside comments (${renderable}):`);
+console.log(" Source comments are reported separately above. What is left here sits in");
+console.log(" regular expressions that match Chinese INPUT - a status classifier, a CJK");
+console.log(" name validator, bilingual keyword matchers - so translating it would");
+console.log(" change plugin behaviour rather than remove visible text.");
+rows.sort((a, b) => b.visible - a.visible);
+for (const row of rows) console.log(` ${String(row.visible).padStart(5)} outside / ${String(row.total).padStart(5)} total ${row.file}`);
diff --git a/scripts/served.mjs b/scripts/served.mjs
new file mode 100644
index 0000000..5936e8d
--- /dev/null
+++ b/scripts/served.mjs
@@ -0,0 +1,66 @@
+// Resolves the client bundles the browser actually receives.
+//
+// Reads a save of the boot HTML (the page dsh web serves at `/`) and writes the
+// on-disk path of every bundle in its combo URLs. This exists because working
+// from a plugin root instead of the served set silently under-measures: 55 of
+// the 58 bundles live in the dsh install tree, not the profile tree, so a scan
+// of the profile tree alone reports nearly everything translated while whole
+// screens stay Chinese.
+//
+// Usage: node scripts/served.mjs [out.json]
+import { readFileSync, writeFileSync, existsSync } from "node:fs";
+
+const [, , htmlPath, outPath = "served.json"] = process.argv;
+if (!htmlPath) {
+ console.error("usage: node scripts/served.mjs [out.json]");
+ process.exit(2);
+}
+
+const roots = (
+ process.env.C2E_BUNDLE_ROOTS ??
+ "/home/oiupoyt/.dsh/profiles/web/node_modules/:" +
+ "/home/oiupoyt/.npm-global/lib/node_modules/@deepseek-ai/dsh/node_modules/"
+)
+ .split(/[:,]/)
+ .map((entry) => entry.trim())
+ .filter(Boolean)
+ .map((entry) => (entry.endsWith("/") ? entry : entry + "/"));
+
+const html = readFileSync(htmlPath, "utf8");
+const urls = [
+ ...new Set((html.match(/plugins\/\?\?[^"'\\ ]+/g) ?? []).map((u) => u.replace(/&/g, "&"))),
+];
+
+const entries = new Set();
+for (const url of urls) {
+ const list = url.slice(url.indexOf("??") + 2).split("&")[0];
+ for (const raw of list.split(",")) {
+ const entry = decodeURIComponent(raw.trim());
+ if (entry) entries.add(entry);
+ }
+}
+
+// A served URL is `/client.js`; the built file may sit in lib/, client/,
+// dist/ or beside package.json depending on the plugin's own build.
+const SUFFIXES = ["", "lib/", "client/", "dist/"];
+const served = [];
+const missing = [];
+for (const entry of entries) {
+ const tail = entry.slice(entry.lastIndexOf("/") + 1);
+ const head = entry.slice(0, entry.lastIndexOf("/") + 1);
+ let hit;
+ for (const root of roots) {
+ for (const dir of SUFFIXES) {
+ const candidate = root + head + dir + tail;
+ if (existsSync(candidate)) { hit = candidate; break; }
+ }
+ if (hit) break;
+ }
+ if (hit) served.push(hit);
+ else missing.push(entry);
+}
+
+writeFileSync(outPath, JSON.stringify(served, null, 1) + "\n");
+console.log(`served: ${served.length} bundles resolved, ${missing.length} missing -> ${outPath}`);
+for (const m of missing) console.log(` missing: ${m}`);
+if (missing.length) process.exitCode = 1;