Files
HealthLog/src/__tests__/verify-message-catalogs-script.test.ts
T
Marc-André Bombeck 7fda400a8c 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.
2026-10-22 18:43:03 +02:00

64 lines
2.2 KiB
TypeScript

/**
* `scripts/verify-message-catalogs.mjs`, which the image build runs in the
* builder and again in the runner stage, passes the shipped catalogs and fails
* on a missing or corrupt one. A check that cannot fail would let an image
* without its catalogs through, so both directions are pinned.
*/
import { spawnSync } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
const SCRIPT = join(process.cwd(), "scripts", "verify-message-catalogs.mjs");
const CONFIG = join(process.cwd(), "src", "lib", "i18n", "config.ts");
function run(args: string[]) {
return spawnSync(process.execPath, [SCRIPT, ...args], { encoding: "utf8" });
}
let scratch: string | null = null;
afterEach(() => {
if (scratch) rmSync(scratch, { recursive: true, force: true });
scratch = null;
});
describe("verify-message-catalogs", () => {
it("passes the catalogs in the tree and writes the locale list", () => {
scratch = mkdtempSync(join(tmpdir(), "hl-catalog-check-"));
const list = join(scratch, "locales");
const res = run([
"--dir",
join(process.cwd(), "messages"),
"--config",
CONFIG,
"--write",
list,
]);
expect(res.status).toBe(0);
expect(res.stdout).toMatch(/message catalogs ok: .*\ben\b/);
// The runner-stage form, against the list the builder wrote.
expect(
run(["--dir", join(process.cwd(), "messages"), "--locales", list]).status,
).toBe(0);
});
it("fails on a missing catalog and on one that does not parse", () => {
scratch = mkdtempSync(join(tmpdir(), "hl-catalog-check-"));
const dir = join(scratch, "messages");
mkdirSync(dir);
writeFileSync(join(dir, "en.json"), "{}");
writeFileSync(join(dir, "de.json"), '{"common": ');
const list = join(scratch, "locales");
writeFileSync(list, "en\nde\nfr\n");
const res = run(["--dir", dir, "--locales", list]);
expect(res.status).toBe(1);
expect(res.stderr).toMatch(/de\.json: does not parse/);
expect(res.stderr).toMatch(/fr\.json: missing/);
expect(res.stderr).not.toMatch(/en\.json/);
});
});