#!/usr/bin/env node /** * Locale key parity check (Fase 20.2.2). * * Compares the JSON message bundles under apps/web/messages/ against the * source-of-truth bundle (`en.json`). Fails (exit 1) if any of the * non-source locales has missing or extra keys. * * The `$schema` property is intentionally ignored — it's a JSON-Schema * pointer, not a translatable message. * * Usage: * node scripts/check-locales.mjs # check all * node scripts/check-locales.mjs eu # check only eu * * Run via the Justfile recipe `check-locales` or `pnpm -F @colectivo/web …` * — kept dependency-free on purpose so it works at any tooling level. */ import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const MESSAGES_DIR = join(__dirname, '..', 'apps', 'web', 'messages'); const SOURCE_LOCALE = 'en'; const IGNORED_KEYS = new Set(['$schema']); function loadLocale(name) { const path = join(MESSAGES_DIR, `${name}.json`); const raw = readFileSync(path, 'utf8'); const obj = JSON.parse(raw); const keys = new Set(Object.keys(obj).filter((k) => !IGNORED_KEYS.has(k))); return { name, path, keys, obj }; } function diffSets(reference, candidate) { const missing = []; const extra = []; for (const k of reference) { if (!candidate.has(k)) missing.push(k); } for (const k of candidate) { if (!reference.has(k)) extra.push(k); } return { missing, extra }; } function main() { const filter = process.argv.slice(2); const allFiles = readdirSync(MESSAGES_DIR).filter((f) => f.endsWith('.json')); const allLocales = allFiles.map((f) => f.replace(/\.json$/, '')); if (!allLocales.includes(SOURCE_LOCALE)) { console.error(`Source locale ${SOURCE_LOCALE}.json not found in ${MESSAGES_DIR}`); process.exit(2); } const source = loadLocale(SOURCE_LOCALE); const targets = allLocales .filter((l) => l !== SOURCE_LOCALE) .filter((l) => filter.length === 0 || filter.includes(l)) .map(loadLocale); let failed = false; for (const t of targets) { const { missing, extra } = diffSets(source.keys, t.keys); if (missing.length === 0 && extra.length === 0) { console.log(`✓ ${t.name}: ${t.keys.size} keys match ${SOURCE_LOCALE}`); continue; } failed = true; console.error(`✗ ${t.name}: key drift vs ${SOURCE_LOCALE}`); if (missing.length > 0) { console.error(` missing (${missing.length}):`); for (const k of missing) console.error(` - ${k}`); } if (extra.length > 0) { console.error(` extra (${extra.length}):`); for (const k of extra) console.error(` + ${k}`); } } if (failed) { process.exit(1); } } main();