import fs from "node:fs"; import path from "node:path"; const root = process.cwd(); const localesDir = path.join(root, "src", "i18n", "locales"); const locales = fs .readdirSync(localesDir) .filter((entry) => { const fullPath = path.join(localesDir, entry); return fs.statSync(fullPath).isDirectory(); }) .sort((left, right) => left.localeCompare(right)); if (!locales.includes("en")) { console.error('i18n-check: expected base locale directory "en"'); process.exit(1); } function loadJson(filePath) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`i18n-check: failed to load ${path.relative(root, filePath)}: ${message}`); } } function collectKeyPaths(obj, prefix = "") { if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return prefix ? [prefix] : []; } const keys = Object.keys(obj); if (keys.length === 0) { return prefix ? [prefix] : []; } const paths = []; for (const key of keys) { const nextPrefix = prefix ? `${prefix}.${key}` : key; const value = obj[key]; if (value && typeof value === "object" && !Array.isArray(value)) { paths.push(...collectKeyPaths(value, nextPrefix)); } else { paths.push(nextPrefix); } } return paths; } const baseLocaleDir = path.join(localesDir, "en"); const namespaceFiles = fs .readdirSync(baseLocaleDir) .filter((file) => file.endsWith(".json")) .sort((left, right) => left.localeCompare(right)); let hasErrors = false; for (const namespaceFile of namespaceFiles) { const baseData = loadJson(path.join(baseLocaleDir, namespaceFile)); const baseKeys = new Set(collectKeyPaths(baseData)); for (const locale of locales) { if (locale === "en") continue; const localeFile = path.join(localesDir, locale, namespaceFile); if (!fs.existsSync(localeFile)) { console.error(`i18n-check: missing namespace file ${locale}/${namespaceFile}`); hasErrors = true; continue; } const localeData = loadJson(localeFile); const localeKeys = new Set(collectKeyPaths(localeData)); for (const key of baseKeys) { if (!localeKeys.has(key)) { console.error(`i18n-check: missing key ${locale}/${namespaceFile}:${key}`); hasErrors = true; } } for (const key of localeKeys) { if (!baseKeys.has(key)) { console.error(`i18n-check: extra key ${locale}/${namespaceFile}:${key}`); hasErrors = true; } } } } if (hasErrors) { process.exit(1); } console.log("i18n-check: locale files are structurally consistent");