#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const LOCALES_DIR = path.join(__dirname, '../src/locales'); const EN_PATH = path.join(LOCALES_DIR, 'en.json'); function getKeys(obj, prefix = '') { const keys = []; for (const key in obj) { const fullKey = prefix ? `${prefix}.${key}` : key; if (typeof obj[key] === 'object' && obj[key] !== null) { keys.push(...getKeys(obj[key], fullKey)); } else { keys.push(fullKey); } } return keys; } function getPlaceholders(str) { const matches = str.match(/\{\{(\w+)\}\}/g) || []; return matches.sort(); } function getValueAtPath(obj, keyPath) { return keyPath.split('.').reduce((o, k) => o && o[k], obj); } let errors = 0; // 1. Validate JSON parsing let en; try { en = JSON.parse(fs.readFileSync(EN_PATH, 'utf-8')); console.log('en.json: valid JSON'); } catch (e) { console.error('en.json: INVALID JSON:', e); process.exit(1); } const enKeys = getKeys(en); console.log(`en.json: ${enKeys.length} keys`); // Read integrated languages from locales/index.ts languageRegistry. // Falls back to auto-discovery if index.ts is missing or unparseable. let langFiles; const indexPath = path.join(LOCALES_DIR, 'index.ts'); if (fs.existsSync(indexPath)) { const indexSrc = fs.readFileSync(indexPath, 'utf-8'); const registryMatch = indexSrc.match( /const languageRegistry\s*=\s*\{([\s\S]*?)\}\s*(?:as const|;)/, ); if (registryMatch) { langFiles = [...registryMatch[1].matchAll(/^\s*(\w+)\s*:/gm)] .map(m => m[1]) .filter(l => l !== 'en'); } } if (!langFiles) { langFiles = fs .readdirSync(LOCALES_DIR) .filter(f => f.endsWith('.json') && f !== 'en.json') .map(f => f.replace('.json', '')); } for (const lang of langFiles) { const langPath = path.join(LOCALES_DIR, `${lang}.json`); let langData; try { langData = JSON.parse(fs.readFileSync(langPath, 'utf-8')); console.log(`${lang}.json: valid JSON`); } catch (e) { console.error(`${lang}.json: INVALID JSON:`, e); errors++; continue; } const langKeys = getKeys(langData); // 2. Check missing keys (warnings, not errors - fallback handles them) const missingKeys = enKeys.filter(k => !langKeys.includes(k)); if (missingKeys.length > 0) { console.warn( `${lang}.json: ${missingKeys.length} missing keys (will fall back to English)`, ); } // 3. Check placeholder consistency (errors - mismatched placeholders are bugs) for (const key of langKeys) { const enValue = getValueAtPath(en, key); const langValue = getValueAtPath(langData, key); if (typeof enValue === 'string' && typeof langValue === 'string') { const enPlaceholders = getPlaceholders(enValue); const langPlaceholders = getPlaceholders(langValue); if (JSON.stringify(enPlaceholders) !== JSON.stringify(langPlaceholders)) { console.error( `${lang}.json: placeholder mismatch at "${key}": ` + `en has [${enPlaceholders.join(', ')}] but ${lang} has [${langPlaceholders.join(', ')}]`, ); errors++; } } } } if (errors > 0) { console.error(`\nValidation failed with ${errors} error(s)`); process.exit(1); } else { console.log('\nAll l10n files valid'); }