Files
ihasmail/scripts/i18n-strings.mjs

60 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/*
* Every source string a catalog needs, straight out of the calls.
*
* The English text is the key, so the catalog's keys are not a list somebody
* maintains -- they are whatever t(), tNode() and plural() are actually asked
* for. Reading them from the code means a catalog can never drift out of
* step with the app in the one direction that matters: a key that no longer
* exists is dead weight, but a call with no key is an untranslated string
* nobody noticed.
*/
/*
* The parser, not the compiler.
*
* TypeScript 7 is the native port: its package ships a `tsc` shim over a Go
* binary and nothing else, so `typescript` now exports `version` and
* `versionMajorMinor` and no compiler API at all. Every `ts.createSourceFile`
* in this directory started throwing "Cannot read properties of undefined
* (reading 'Latest')" the day the bump landed, and nothing noticed, because no
* workflow runs these.
*
* `typescript-ast` is an npm alias for the last TypeScript that carries the JS
* API (see package.json). It parses; `typescript` still type-checks and builds.
* Two entries, two jobs -- not a version someone forgot to remove.
*/
import ts from "typescript-ast";
import { readFileSync, globSync } from "node:fs";
const strings = new Set();
const plurals = new Set();
for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("__tests__"))) {
const src = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const visit = (node) => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
const fn = node.expression.text;
const a0 = node.arguments[0];
if ((fn === "t" || fn === "translate" || fn === "tNode") && a0 && ts.isStringLiteral(a0)) strings.add(a0.text);
if (fn === "plural" && node.arguments[1] && ts.isObjectLiteralExpression(node.arguments[1])) {
const other = node.arguments[1].properties.find((p) => ts.isPropertyAssignment(p) && p.name.getText(src) === "other");
const forms = {};
for (const p of node.arguments[1].properties) {
if (ts.isPropertyAssignment(p) && ts.isStringLiteral(p.initializer)) forms[p.name.getText(src)] = p.initializer.text;
}
if (other) plurals.add(JSON.stringify(forms));
}
}
ts.forEachChild(node, visit);
};
visit(src);
}
const out = { strings: [...strings].sort(), plurals: [...plurals].map((p) => JSON.parse(p)) };
if (process.argv.includes("--json")) console.log(JSON.stringify(out, null, 2));
else {
console.log(`${out.strings.length} strings, ${out.plurals.length} plural sets`);
const short = out.strings.filter((s) => s.length <= 30).length;
console.log(` ${short} short (<=30 chars), ${out.strings.length - short} longer`);
}