Files
ps5upload/scripts/build-console-strings.mjs
T
phantomptrandClaude Opus 5.5 d435ac27db console page: load only the active language's strings
The page embedded all 21 languages' cw_* strings (174 KiB of the 220 KiB gzip
budget). Now it embeds English only; every other language is a separate
build artifact (dist-console/strings/<lang>.json) gzip'd into the helper and
served at GET /strings/<lang>.json (Host/Origin checked, no token, immutable
year-long cache; the page asks with ?v=<hash of the strings>). The page shows
English until the chosen language arrives, falls back to English per key, and
stays in English (logged once) when a load fails.

Page: 607.1 KiB raw / 174.4 KiB gzip -> 357.5 KiB raw / 112.5 KiB gzip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
2026-10-17 10:49:30 -07:00

92 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
* The console web page's translations: every `cw_*` key from all 21 locale
* files under client/src/i18n/locales/, written one file per locale to
* client/src/console/locales/<locale>.json as { "cw_…": "…" }.
*
* The page (client/src/console/strings.ts) embeds only en.json; the build
* (vite.console.config.ts, consoleStrings) emits every other locale as
* dist-console/strings/<locale>.json, which payload/Makefile gzips into the
* helper and the page fetches from GET /strings/<locale>.json. `npm run
* build:console` runs this first; the files are committed so typecheck and
* tests work without a build, and strings.test.ts fails when they are stale.
*
* Locale files are read the way scripts/i18n-coverage.mjs reads them: the
* `const <id>: Translations = { … }` literal evaluated in an isolated vm
* context, never regex-parsed for values. Keep the matcher in sync with that
* script if the locale-file shape changes.
*
* Usage: node scripts/build-console-strings.mjs [--check]
* --check exit 1 when the committed file differs, write nothing.
*/
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const LOCALES_DIR = path.join(repoRoot, "client/src/i18n/locales");
const OUT_DIR = path.join(repoRoot, "client/src/console/locales");
const PREFIX = "cw_";
function loadLocale(fp) {
const src = fs.readFileSync(fp, "utf8");
const startMatch = src.match(/^const\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*Translations\s*=\s*/m);
if (!startMatch) throw new Error(`${fp}: could not find 'const <name>: Translations = ' declaration`);
const startIdx = startMatch.index + startMatch[0].length;
const endMatch = src.slice(startIdx).match(/\n};\s*\n\s*export default/);
if (!endMatch) throw new Error(`${fp}: could not find end of literal (};\\nexport default)`);
const literalSrc = src.slice(startIdx, startIdx + endMatch.index + "\n}".length);
return vm.runInNewContext(`(${literalSrc})`, Object.create(null), { timeout: 1000 });
}
/** { locale: { cw_key: value } }, locales and keys sorted, English required. */
export function extractConsoleStrings() {
const files = fs
.readdirSync(LOCALES_DIR)
.filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"))
.sort();
const out = {};
for (const f of files) {
const dict = loadLocale(path.join(LOCALES_DIR, f));
const picked = {};
for (const key of Object.keys(dict).sort()) {
if (key.startsWith(PREFIX)) picked[key] = dict[key];
}
out[f.replace(/\.ts$/, "")] = picked;
}
if (!out.en) throw new Error(`${LOCALES_DIR} has no en.ts`);
return out;
}
/** { "<locale>.json": file text } for every locale, as committed under OUT_DIR. */
export function consoleStringFiles() {
const out = {};
for (const [code, map] of Object.entries(extractConsoleStrings())) {
out[`${code}.json`] = `${JSON.stringify(map, null, 2)}\n`;
}
return out;
}
function main() {
const want = consoleStringFiles();
const have = fs.existsSync(OUT_DIR) ? fs.readdirSync(OUT_DIR).filter((f) => f.endsWith(".json")) : [];
const stale = Object.keys(want).filter((f) => !have.includes(f) || fs.readFileSync(path.join(OUT_DIR, f), "utf8") !== want[f]);
const extra = have.filter((f) => !(f in want));
if (process.argv.includes("--check")) {
if (stale.length || extra.length) {
console.error(`${path.relative(repoRoot, OUT_DIR)} is stale (${[...stale, ...extra].join(", ")}): run node scripts/build-console-strings.mjs`);
process.exit(1);
}
return;
}
// Rewrite only what changed, so the payload's make rule does not see a new mtime for nothing.
fs.mkdirSync(OUT_DIR, { recursive: true });
for (const f of stale) fs.writeFileSync(path.join(OUT_DIR, f), want[f]);
for (const f of extra) fs.unlinkSync(path.join(OUT_DIR, f));
const n = Object.keys(JSON.parse(want["en.json"])).length;
console.log(`console strings: ${n} keys × ${Object.keys(want).length} locales`);
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();