172 lines
5.7 KiB
TypeScript
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",
|
|
]);
|
|
});
|
|
});
|