import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { EN_MESSAGES, KO_MESSAGES, MESSAGE_CATALOGS } from "../src/presentation/i18n/catalog.ts"; const fixtureMode = process.argv.includes("--fixture"); const failures: string[] = []; const sourceExtensions = /\.(?:ts|tsx|mts|cts)$/; const koreanLiteral = /[가-힣]/; const rawFailureRender = /(? { const result: string[] = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const target = path.join(directory, entry.name); if (entry.isDirectory()) result.push(...(await listSourceFiles(target))); else if (sourceExtensions.test(entry.name)) result.push(target); } return result; } function placeholders(template: string): string[] { return [...template.matchAll(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g)] .map((match) => match[1]) .sort(); } if (!fixtureMode) { const canonicalKeys = Object.keys(KO_MESSAGES).sort(); for (const [locale, catalog] of Object.entries(MESSAGE_CATALOGS)) { const readableCatalog = catalog as Readonly>; const canonicalCatalog = KO_MESSAGES as Readonly>; const keys = Object.keys(catalog).sort(); if (JSON.stringify(keys) !== JSON.stringify(canonicalKeys)) { failures.push(`${locale} catalog keys do not match ko-KR`); } for (const key of canonicalKeys) { if ( JSON.stringify(placeholders(readableCatalog[key] ?? "")) !== JSON.stringify(placeholders(canonicalCatalog[key] ?? "")) ) { failures.push(`${locale}:${key} interpolation parameters do not match`); } } } if (Object.keys(EN_MESSAGES).length !== canonicalKeys.length) { failures.push("en-US catalog key count does not match ko-KR"); } } const commonDirectories = [ "src/presentation/boundaries", "src/presentation/components", "src/presentation/design-system", "src/presentation/forms", "src/presentation/layouts", "src/presentation/routes", "src/presentation/templates", ]; const sources = fixtureMode ? await listSourceFiles("tests/fixtures/i18n/forbidden") : ( await Promise.all(commonDirectories.map((directory) => listSourceFiles(directory))) ).flat(); for (const file of sources) { const source = await readFile(file, "utf8"); if (koreanLiteral.test(source)) { failures.push(`hardcoded common user-facing locale literal in ${file}`); } if (rawFailureRender.test(source)) { failures.push(`raw backend/error message rendered in ${file}`); } if (source.includes("dangerouslySetInnerHTML")) { failures.push(`untrusted HTML interpolation boundary in ${file}`); } } const report = { schemaVersion: 1, mode: fixtureMode ? "negative-fixture" : "source", localeCount: Object.keys(MESSAGE_CATALOGS).length + 1, messageKeyCount: Object.keys(KO_MESSAGES).length, checkedFiles: sources.length, failures, passed: failures.length === 0, }; await mkdir("artifacts/quality", { recursive: true }); await writeFile( fixtureMode ? "artifacts/quality/i18n-fixture.json" : "artifacts/quality/i18n.json", `${JSON.stringify(report, null, 2)}\n`, ); if (failures.length > 0) { process.stderr.write(`I18n contract failed:\n${failures.join("\n")}\n`); process.exit(1); } process.stdout.write( `I18n contract: ${report.messageKeyCount} keys across ${report.localeCount} locales PASS\n`, );