Files
ihasmail-inbuxa/scripts/i18n-strings.mjs
T
jcoffey-dev d282813bc5 Give the i18n scripts a parser again
TypeScript 7 is the native port: the package ships a `tsc` shim over a Go
binary, and `typescript` now exports `version` and `versionMajorMinor` and no
compiler API. Every `ts.createSourceFile` in scripts/ has been throwing
"Cannot read properties of undefined (reading 'Latest')" since the 5.9.3 → 7.0.2
bump -- four of the five i18n scripts dead, only i18n-extract still running.

Nothing noticed because no workflow runs them. The catalogue gate for nine
languages has been dark, and the only signal was running it by hand.

There is no official TS7 API package (@typescript/ast and @typescript/api are
both 404), and the alternative was rewriting 493 lines and 25 distinct AST
calls, including the JSX guards, against a different tree -- in tooling with no
tests of its own. So `typescript-ast` is an npm alias for the last TypeScript
carrying the JS API. It parses; `typescript` still type-checks and builds. Two
entries, two jobs, said so in each script so the next reader does not delete
one as a leftover.

What the gate says now it can speak: catalogues are green and coverage is 100%.
The "16 falling back to English" it reports in every locale are placeholders,
example domains, a product name, a licence id and the quote glyph -- strings
that should stay English. The 41 stale keys per locale are real dead weight and
are left for their own change.
2026-09-10 14:21:42 -07:00

60 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/*
* Every source string a catalogue needs, straight out of the calls.
*
* The English text is the key, so the catalogue'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 catalogue 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`);
}