Files
suaveplan/packages/ui/react/react-ui/scripts/check-i18n-keys.test.ts
2026-05-27 23:47:53 +01:00

172 lines
5.7 KiB
TypeScript

/**
* Tests for `check-i18n-keys.ts`.
*
* Build a fixtures filesystem on disk under `mkdtemp` and call the
* exported helpers (`extractKeysFromSource`, `loadBundleKeys`,
* `diff`) directly. The script's `main()` integrates with
* `process.argv` / `process.exit` so it is exercised end-to-end via
* a `Bun.spawn` round-trip in the last test.
*/
import { afterEach, beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
diff,
extractKeysFromSource,
loadBundleKeys,
} from "./check-i18n-keys.js";
let workDir: string;
beforeEach(async () => {
workDir = await mkdtemp(join(tmpdir(), "i18n-keys-"));
});
afterEach(async () => {
await rm(workDir, { recursive: true, force: true });
});
async function writeFixture(rel: string, content: string): Promise<string> {
const full = join(workDir, rel);
await mkdir(join(full, ".."), { recursive: true });
await writeFile(full, content, "utf8");
return full;
}
describe("extractKeysFromSource", () => {
it("captures t() calls with double quotes", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const out = t("react-ui.foo.bar");`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.foo.bar")).toBe(true);
});
it("captures t() calls with single quotes", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const out = t('react-ui.foo.bar');`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.foo.bar")).toBe(true);
});
it("captures i18nKey JSX attribute", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`<FormattedMessage i18nKey="react-ui.greeting" />`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.greeting")).toBe(true);
});
it("captures the DEFAULT_*_KEY const idiom", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const DEFAULT_LABEL_KEY = "react-ui.thing.label";`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.thing.label")).toBe(true);
});
it("captures `?? \"react-ui.…\"` fallback shorthand", {
timeout: 5000,
}, async () => {
const f = await writeFixture(
"a.tsx",
`const k = labelKey ?? "react-ui.fallback.label";`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.fallback.label")).toBe(true);
});
it("records the source location for each key", {
timeout: 5000,
}, async () => {
const f = await writeFixture(
"deep/nested.tsx",
`// header\nconst k = t("react-ui.line.two");`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.get("react-ui.line.two")).toMatch(/deep\/nested\.tsx:2$/);
});
it("ignores non-namespace strings", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const k = t("other.namespace.key");`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.size).toBe(0);
});
});
describe("loadBundleKeys", () => {
it("loads only keys in the react-ui namespace", {
timeout: 5000,
}, async () => {
const path = join(workDir, "bundle.json");
await writeFile(
path,
JSON.stringify({
"react-ui.foo": "Foo",
"react-ui.bar": "Bar",
"other.namespace.key": "other",
}),
"utf8"
);
const keys = await loadBundleKeys(path);
expect(keys.has("react-ui.foo")).toBe(true);
expect(keys.has("react-ui.bar")).toBe(true);
expect(keys.has("other.namespace.key")).toBe(false);
});
});
describe("diff", () => {
it("reports MISSING for code-only keys", { timeout: 5000 }, () => {
const codeKeys = new Map([["react-ui.code.only", "src/x.tsx:1"]]);
const bundleKeys = new Set(["react-ui.bundled"]);
const diags = diff(codeKeys, bundleKeys);
expect(
diags.find((d) => d.kind === "MISSING")?.key
).toBe("react-ui.code.only");
});
it("reports ORPHAN for bundle-only keys", { timeout: 5000 }, () => {
const codeKeys = new Map<string, string>();
const bundleKeys = new Set(["react-ui.bundle.only"]);
const diags = diff(codeKeys, bundleKeys);
expect(
diags.find((d) => d.kind === "ORPHAN")?.key
).toBe("react-ui.bundle.only");
});
it("returns empty diagnostics on a clean source/bundle pair", {
timeout: 5000,
}, () => {
const codeKeys = new Map([["react-ui.k", "src/x.tsx:1"]]);
const bundleKeys = new Set(["react-ui.k"]);
expect(diff(codeKeys, bundleKeys)).toEqual([]);
});
it("orders MISSING before ORPHAN, then alphabetically", {
timeout: 5000,
}, () => {
const codeKeys = new Map([
["react-ui.b", "x:1"],
["react-ui.a", "x:2"],
]);
const bundleKeys = new Set(["react-ui.zzz", "react-ui.aaa"]);
const diags = diff(codeKeys, bundleKeys);
expect(diags.map((d) => `${d.kind}:${d.key}`)).toEqual([
"MISSING:react-ui.a",
"MISSING:react-ui.b",
"ORPHAN:react-ui.aaa",
"ORPHAN:react-ui.zzz",
]);
});
});