Files
2026-07-13 12:22:50 +08:00

116 lines
3.2 KiB
JavaScript

#!/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');
}