setup i18n and ported all tools to standalone pages
This commit is contained in:
236
scripts/check-translations.js
Executable file
236
scripts/check-translations.js
Executable file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Translation Checker for BentoPDF
|
||||
*
|
||||
* This script compares translation files across languages and reports:
|
||||
* - Missing keys (keys present in English but absent in other languages)
|
||||
* - Extra keys (keys present in other languages but not in English)
|
||||
* - Untranslated keys (keys with the same value as English)
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-translations.js
|
||||
* node scripts/check-translations.js --verbose
|
||||
* node scripts/check-translations.js --lang=de
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const LOCALES_DIR = path.join(__dirname, '../public/locales');
|
||||
const REFERENCE_LANG = 'en';
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
dim: '\x1b[2m',
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const verbose = args.includes('--verbose');
|
||||
const specificLang = args.find(arg => arg.startsWith('--lang='))?.split('=')[1];
|
||||
|
||||
/**
|
||||
* Flatten nested JSON object into dot notation
|
||||
* { a: { b: 'value' } } => { 'a.b': 'value' }
|
||||
*/
|
||||
function flattenObject(obj, prefix = '') {
|
||||
const flattened = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
Object.assign(flattened, flattenObject(value, newKey));
|
||||
} else {
|
||||
flattened[newKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse a translation file
|
||||
*/
|
||||
function loadTranslation(lang) {
|
||||
const filePath = path.join(LOCALES_DIR, lang, 'common.json');
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}✗ Error parsing ${lang}/common.json:${colors.reset}`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available languages
|
||||
*/
|
||||
function getAvailableLanguages() {
|
||||
if (!fs.existsSync(LOCALES_DIR)) {
|
||||
console.error(`${colors.red}✗ Locales directory not found: ${LOCALES_DIR}${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return fs.readdirSync(LOCALES_DIR)
|
||||
.filter(item => {
|
||||
const itemPath = path.join(LOCALES_DIR, item);
|
||||
return fs.statSync(itemPath).isDirectory();
|
||||
})
|
||||
.filter(lang => {
|
||||
// Only include if common.json exists
|
||||
return fs.existsSync(path.join(LOCALES_DIR, lang, 'common.json'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two sets of keys and report differences
|
||||
*/
|
||||
function compareKeys(refKeys, targetKeys, refLang, targetLang, refFlat, targetFlat) {
|
||||
const missing = refKeys.filter(key => !targetKeys.includes(key));
|
||||
const extra = targetKeys.filter(key => !refKeys.includes(key));
|
||||
const untranslated = refKeys.filter(key =>
|
||||
targetKeys.includes(key) && refFlat[key] === targetFlat[key] && typeof refFlat[key] === 'string'
|
||||
);
|
||||
|
||||
return { missing, extra, untranslated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Print section header
|
||||
*/
|
||||
function printHeader(text) {
|
||||
console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`);
|
||||
console.log(`${colors.cyan}${text}${colors.reset}`);
|
||||
console.log(`${colors.cyan}${'='.repeat(60)}${colors.reset}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print section
|
||||
*/
|
||||
function printSection(title, items, color = colors.yellow) {
|
||||
if (items.length === 0) return;
|
||||
|
||||
console.log(`${color}${title} (${items.length}):${colors.reset}`);
|
||||
items.forEach(item => {
|
||||
console.log(` ${colors.dim}•${colors.reset} ${item}`);
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main() {
|
||||
console.log(`${colors.blue}🌍 BentoPDF Translation Checker${colors.reset}\n`);
|
||||
|
||||
const languages = getAvailableLanguages();
|
||||
|
||||
if (languages.length === 0) {
|
||||
console.error(`${colors.red}✗ No translation files found in ${LOCALES_DIR}${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load reference language (English)
|
||||
const refTranslation = loadTranslation(REFERENCE_LANG);
|
||||
if (!refTranslation) {
|
||||
console.error(`${colors.red}✗ Reference language (${REFERENCE_LANG}) not found${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const refFlat = flattenObject(refTranslation);
|
||||
const refKeys = Object.keys(refFlat);
|
||||
|
||||
console.log(`${colors.green}✓ Reference language (${REFERENCE_LANG}): ${refKeys.length} keys${colors.reset}`);
|
||||
console.log(`${colors.dim} Available languages: ${languages.join(', ')}${colors.reset}\n`);
|
||||
|
||||
// Filter languages to check
|
||||
const langsToCheck = specificLang
|
||||
? languages.filter(lang => lang === specificLang)
|
||||
: languages.filter(lang => lang !== REFERENCE_LANG);
|
||||
|
||||
if (langsToCheck.length === 0) {
|
||||
console.log(`${colors.yellow}⚠ No languages to check${colors.reset}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let hasIssues = false;
|
||||
|
||||
// Check each language
|
||||
for (const lang of langsToCheck) {
|
||||
printHeader(`Checking: ${lang.toUpperCase()}`);
|
||||
|
||||
const translation = loadTranslation(lang);
|
||||
if (!translation) {
|
||||
hasIssues = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetFlat = flattenObject(translation);
|
||||
const targetKeys = Object.keys(targetFlat);
|
||||
|
||||
const { missing, extra, untranslated } = compareKeys(
|
||||
refKeys,
|
||||
targetKeys,
|
||||
REFERENCE_LANG,
|
||||
lang,
|
||||
refFlat,
|
||||
targetFlat
|
||||
);
|
||||
|
||||
// Summary
|
||||
console.log(`${colors.dim}Total keys: ${targetKeys.length} / ${refKeys.length}${colors.reset}\n`);
|
||||
|
||||
// Missing keys
|
||||
if (missing.length > 0) {
|
||||
hasIssues = true;
|
||||
printSection(`Missing Keys`, missing, colors.red);
|
||||
}
|
||||
|
||||
// Extra keys
|
||||
if (extra.length > 0) {
|
||||
hasIssues = true;
|
||||
printSection(`Extra Keys (not in English)`, extra, colors.yellow);
|
||||
}
|
||||
|
||||
// Untranslated keys (same as English)
|
||||
if (verbose && untranslated.length > 0) {
|
||||
printSection(`Possibly Untranslated (same as English)`, untranslated, colors.cyan);
|
||||
} else if (untranslated.length > 0) {
|
||||
console.log(`${colors.cyan}Possibly Untranslated: ${untranslated.length}${colors.reset}`);
|
||||
console.log(`${colors.dim} (use --verbose to see details)${colors.reset}\n`);
|
||||
}
|
||||
|
||||
// All good
|
||||
if (missing.length === 0 && extra.length === 0) {
|
||||
console.log(`${colors.green} No missing or extra keys!${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Final summary
|
||||
console.log(`${colors.cyan}${'='.repeat(60)}${colors.reset}\n`);
|
||||
|
||||
if (!hasIssues) {
|
||||
console.log(`${colors.green} All translations are in sync!${colors.reset}\n`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`${colors.yellow} Issues found. Please review the output above.${colors.reset}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user