import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; // Pure Node-context checks (no browser needed): every locale must define exactly the // same set of translation keys. A missing key would otherwise silently fall back to // showing the raw key string in production -- this test makes that impossible to ship. const __dirname = path.dirname(fileURLToPath(import.meta.url)); const LOCALES_DIR = path.resolve(__dirname, "../src/i18n/locales"); const LANGUAGES = ["nl-BE", "en-GB", "fr-BE"]; function collectKeyPaths(value: unknown, prefix = ""): string[] { if (value === null || typeof value !== "object") { return [prefix]; } return Object.entries(value as Record).flatMap(([key, nested]) => collectKeyPaths(nested, prefix ? `${prefix}.${key}` : key), ); } function loadNamespace(language: string, namespace: string): Record { const filePath = path.join(LOCALES_DIR, language, `${namespace}.json`); return JSON.parse(fs.readFileSync(filePath, "utf-8")); } const namespaces = fs .readdirSync(path.join(LOCALES_DIR, "nl-BE")) .filter((f) => f.endsWith(".json")) .map((f) => f.replace(/\.json$/, "")); test("every locale defines the same translation keys as nl-BE, for every namespace", () => { expect(namespaces.length).toBeGreaterThan(0); for (const namespace of namespaces) { const referenceKeys = collectKeyPaths(loadNamespace("nl-BE", namespace)).sort(); for (const language of LANGUAGES) { if (language === "nl-BE") continue; const keys = collectKeyPaths(loadNamespace(language, namespace)).sort(); const missing = referenceKeys.filter((k) => !keys.includes(k)); const extra = keys.filter((k) => !referenceKeys.includes(k)); expect( missing, `${language}/${namespace}.json is missing keys present in nl-BE: ${missing.join(", ")}`, ).toEqual([]); expect( extra, `${language}/${namespace}.json has extra keys not present in nl-BE: ${extra.join(", ")}`, ).toEqual([]); } } }); test("no locale file contains an empty string value", () => { for (const language of LANGUAGES) { for (const namespace of namespaces) { const data = loadNamespace(language, namespace); const keys = collectKeyPaths(data); for (const keyPath of keys) { const value = keyPath.split(".").reduce((acc, part) => { if (acc && typeof acc === "object") return (acc as Record)[part]; return undefined; }, data); if (typeof value === "string") { expect(value.trim().length, `${language}/${namespace}.json:${keyPath} is empty`).toBeGreaterThan(0); } } } } });