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.
This commit is contained in:
parent
23327d2bd5
commit
ab0179545a
4 changed files with 164 additions and 8 deletions
42
README.md
42
README.md
|
|
@ -52,12 +52,14 @@ The client half does one thing: it selects the `en` locale through
|
||||||
- Text rendered inside an `<img>`, canvas, or other non-text payload.
|
- Text rendered inside an `<img>`, canvas, or other non-text payload.
|
||||||
- Content fetched from a third-party server at runtime (announcements, remote
|
- Content fetched from a third-party server at runtime (announcements, remote
|
||||||
catalogs) rather than from DSH itself.
|
catalogs) rather than from DSH itself.
|
||||||
- The long tail of one-off strings the dictionary does not yet cover.
|
- Chinese inside regular expressions that match Chinese *input*. `/失败|错误|无法/`
|
||||||
|
classifies a status message and `/^[一-龥]{2}$/` validates a two-character name;
|
||||||
The shipped dictionary holds roughly 1,570 entries. Coverage is concentrated on
|
translating those would change plugin behaviour instead of removing text.
|
||||||
strings that appear repeatedly; rare one-off sentences are usually left in
|
This is 111 characters across four bundles, all of it code rather than copy.
|
||||||
Chinese, which is why the all-or-nothing rule matters.
|
|
||||||
|
|
||||||
|
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
|
## Development
|
||||||
|
|
||||||
```bash
|
```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/
|
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.
|
1. `dshmarket_dict.json` — harvested pairs.
|
||||||
2. `UI_TRANSLATIONS` in `patch-plugins.py` — harvested pairs.
|
2. `UI_TRANSLATIONS` in `patch-plugins.py` — harvested pairs.
|
||||||
3. `src/dictionary.extra.json` — curated entries.
|
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.
|
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
|
Add or fix translations in `src/dictionary.extra.json`, then run
|
||||||
`npm run dict && npm run build`.
|
`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
|
## License
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@
|
||||||
"mt": "node scripts/mt-fill.mjs",
|
"mt": "node scripts/mt-fill.mjs",
|
||||||
"dict": "node scripts/merge-dict.mjs && node scripts/merge-comments.mjs",
|
"dict": "node scripts/merge-dict.mjs && node scripts/merge-comments.mjs",
|
||||||
"build": "node scripts/build.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": {
|
"peerDependencies": {
|
||||||
"@deepseek-ai/cordis": ">=4.0.0"
|
"@deepseek-ai/cordis": ">=4.0.0"
|
||||||
|
|
|
||||||
60
scripts/measure.mts
Normal file
60
scripts/measure.mts
Normal file
|
|
@ -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 <served.json>
|
||||||
|
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}`);
|
||||||
66
scripts/served.mjs
Normal file
66
scripts/served.mjs
Normal file
|
|
@ -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 <boot.html> [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 <boot.html> [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 `<pkg>/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;
|
||||||
Loading…
Add table
Reference in a new issue