mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(i18n): serve English for an unreadable catalog, refuse to boot without it
The server reads messages/<locale>.json from disk on first use. A missing or corrupt file threw, and since the matchers iterate every locale, one bad non-English catalog turned every request that touched them into a 500. That locale is now served the English catalog, and the failure is logged once as i18n.catalog.fallback with the locale and the reason. English is the fallback itself: a process whose messages/en.json is missing or does not parse refuses to start, with the file and the reason in the message. The image build checks the catalogs twice with scripts/verify-message-catalogs.mjs: in the builder against the locale list in the source, and in the runner stage against the copies the image carries.
This commit is contained in:
1 parent
ac31130bc0
commit
7fda400a8c
6 files changed
+365
-7
No files matched your search
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Checks that every shipped locale has a message catalog that parses.
|
||||
*
|
||||
* The server reads `messages/<locale>.json` from disk at run time
|
||||
* (`src/lib/i18n/server-catalogs.ts`); nothing is bundled, so an image that
|
||||
* lost the directory, or one file of it, builds and starts clean and only
|
||||
* shows the gap when a page asks for that locale. The Dockerfile runs this
|
||||
* twice: in the builder against the locale list in `src/lib/i18n/config.ts`,
|
||||
* writing that list out, and in the runner stage against the catalogs the
|
||||
* image actually carries.
|
||||
*
|
||||
* node scripts/verify-message-catalogs.mjs --dir messages \
|
||||
* --config src/lib/i18n/config.ts [--write .message-locales]
|
||||
* node verify-message-catalogs.mjs --dir /app/messages \
|
||||
* --locales .message-locales
|
||||
*
|
||||
* Exits 1 with every problem listed when a catalog is missing, does not parse
|
||||
* or is not a JSON object, or when the locale list cannot be read.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
function arg(name) {
|
||||
const index = process.argv.indexOf(`--${name}`);
|
||||
return index === -1 ? null : (process.argv[index + 1] ?? null);
|
||||
}
|
||||
|
||||
function localesFromConfig(path) {
|
||||
const source = readFileSync(path, "utf8");
|
||||
const block = /export const locales = \[([^\]]*)\]/.exec(source);
|
||||
if (!block) throw new Error(`no \`export const locales = [...]\` in ${path}`);
|
||||
const list = [...block[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
||||
if (list.length === 0) throw new Error(`the locale list in ${path} is empty`);
|
||||
return list;
|
||||
}
|
||||
|
||||
function localesFromFile(path) {
|
||||
const list = readFileSync(path, "utf8").split("\n").filter(Boolean);
|
||||
if (list.length === 0) throw new Error(`the locale list in ${path} is empty`);
|
||||
return list;
|
||||
}
|
||||
|
||||
const dir = arg("dir");
|
||||
const config = arg("config");
|
||||
const listFile = arg("locales");
|
||||
if (!dir || (!config && !listFile)) {
|
||||
console.error(
|
||||
"usage: verify-message-catalogs.mjs --dir <messages> (--config <config.ts> | --locales <file>) [--write <file>]",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let locales;
|
||||
try {
|
||||
locales = config ? localesFromConfig(config) : localesFromFile(listFile);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const problems = [];
|
||||
for (const locale of locales) {
|
||||
const path = join(dir, `${locale}.json`);
|
||||
let text;
|
||||
try {
|
||||
text = readFileSync(path, "utf8");
|
||||
} catch (error) {
|
||||
problems.push(
|
||||
`${path}: ${error.code === "ENOENT" ? "missing" : error.message}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (
|
||||
parsed === null ||
|
||||
typeof parsed !== "object" ||
|
||||
Array.isArray(parsed)
|
||||
) {
|
||||
problems.push(`${path}: not a JSON object`);
|
||||
}
|
||||
} catch (error) {
|
||||
problems.push(`${path}: does not parse (${error.message})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length > 0) {
|
||||
console.error(
|
||||
"ERROR: message catalogs the server reads at run time are broken:",
|
||||
);
|
||||
for (const problem of problems) console.error(` ${problem}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const write = arg("write");
|
||||
if (write) writeFileSync(write, `${locales.join("\n")}\n`);
|
||||
console.log(`message catalogs ok: ${locales.join(", ")} in ${dir}`);
|
||||
Reference in new issue
Block a user