import { describe, expect, it } from "vitest"; // docs/DESIGN.md "Copy" (E28 S3g): the parts of the copy rule a machine can read, held over every // value in both languages. The rest (one name per feature, one verb per meaning, "…" when a command // asks first) needs a reader and is checked by the DESIGN conformance step. const files = import.meta.glob("./locales/*/*.json", { eager: true, import: "default" }) as Record< string, unknown >; function values(): [string, string][] { const out: [string, string][] = []; const walk = (o: unknown, path: string) => { if (typeof o === "string") out.push([path, o]); else if (o && typeof o === "object") for (const [k, v] of Object.entries(o)) walk(v, path ? `${path}.${k}` : k); }; for (const [file, json] of Object.entries(files)) walk(json, file.replace("./locales/", "").replace(".json", ":")); return out; } const offenders = (test: (v: string) => boolean) => values() .filter(([, v]) => test(v)) .map(([k, v]) => `${k} = ${v}`); describe("the copy rule", () => { it("quotes a name with curly quotes, never straight ones", () => { expect(offenders((v) => v.includes('"'))).toEqual([]); }); it("writes an ellipsis as one character", () => { expect(offenders((v) => v.includes("..."))).toEqual([]); }); it("ends no message with a tick", () => { expect(offenders((v) => v.includes("✓"))).toEqual([]); }); it("spells no unit as a bare letter after a number ('90m', '24h', '{{count}}d', '10 s')", () => { expect(offenders((v) => /(\d|\}\})\s?(m|h|d|s)(?![\p{L}\d/])/u.test(v))).toEqual([]); }); }); describe("one name per thing", () => { // The options of one choice are told apart by their names alone: two playback classes once shared // "Közvetlen lejátszás" in the HU filter (E30 review). A set = a `…Opt` object, or one choice key's // options under config.choices. const sets = (): [string, Record][] => { const out: [string, Record][] = []; const walk = (o: unknown, path: string, parent: string) => { if (!o || typeof o !== "object") return; if (/Opt$/.test(path.split(".").pop() ?? "") || parent.endsWith("/config:.choices")) out.push([path, o as Record]); for (const [k, v] of Object.entries(o)) walk(v, `${path}.${k}`, path); }; for (const [file, json] of Object.entries(files)) walk(json, file.replace("./locales/", "").replace(".json", ":"), ""); return out; }; it("names the options of one choice differently", () => { const found = sets(); expect(found.some(([p]) => p.endsWith("playableOpt"))).toBe(true); expect(found.some(([p]) => p.endsWith("choices.log_level"))).toBe(true); const clashes = found.flatMap(([path, opts]) => { const names = Object.values(opts).filter((v): v is string => typeof v === "string"); return names.length === new Set(names).size ? [] : [path]; }); expect(clashes).toEqual([]); }); });