import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Configuration const LOCALES_DIR = path.join(__dirname, "..", "src", "i18n", "locales"); const REFERENCE_LANG = "en"; type TranslationData = Record; interface ValidationResult { valid: boolean; missing: string[][]; extra: string[][]; } function getLanguages(): string[] { const entries = fs.readdirSync(LOCALES_DIR, { withFileTypes: true }); return entries .filter((entry) => entry.isDirectory() && entry.name !== REFERENCE_LANG) .map((entry) => entry.name) .sort(); } const LANGUAGES = getLanguages(); // Colors for terminal output const colors: Record = { reset: "\x1b[0m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", }; function colorize(text: string, color: string): string { return `${colors[color]}${text}${colors.reset}`; } function getAllKeyPaths( obj: TranslationData, prefix: string[] = [], ): string[][] { let paths: string[][] = []; for (const key in obj) { if (!Object.hasOwn(obj, key)) continue; const currentPath = prefix.concat([key]); const value = obj[key]; if (typeof value === "object" && value !== null && !Array.isArray(value)) { paths = paths.concat( getAllKeyPaths(value as TranslationData, currentPath), ); } else { paths.push(currentPath); } } return paths; } function hasKeyPath(obj: TranslationData, keyPath: string[]): boolean { let current: unknown = obj; for (const key of keyPath) { if ( typeof current !== "object" || current === null || (current as Record)[key] === undefined ) { return false; } current = (current as Record)[key]; } return true; } function loadTranslationFile(lang: string): TranslationData | null { const filePath = path.join(LOCALES_DIR, lang, "translation.json"); try { const content = fs.readFileSync(filePath, "utf8"); return JSON.parse(content) as TranslationData; } catch (error) { console.error(colorize(`āœ— Error loading ${lang}/translation.json:`, "red")); console.error(` ${(error as Error).message}`); return null; } } function validateTranslations(): void { console.log(colorize("\nšŸŒ Translation Consistency Check\n", "blue")); // Load reference file console.log(`Loading reference language: ${REFERENCE_LANG}`); const referenceData = loadTranslationFile(REFERENCE_LANG); if (!referenceData) { console.error( colorize(`\nāœ— Failed to load reference file (${REFERENCE_LANG})`, "red"), ); process.exit(1); } // Get all key paths from reference const referenceKeyPaths = getAllKeyPaths(referenceData); console.log(`Reference has ${referenceKeyPaths.length} keys\n`); // Track validation results let hasErrors = false; const results: Record = {}; // Validate each language for (const lang of LANGUAGES) { const langData = loadTranslationFile(lang); if (!langData) { hasErrors = true; results[lang] = { valid: false, missing: [], extra: [] }; continue; } // Find missing keys const missing = referenceKeyPaths.filter( (keyPath) => !hasKeyPath(langData, keyPath), ); // Find extra keys (keys in language but not in reference) const langKeyPaths = getAllKeyPaths(langData); const extra = langKeyPaths.filter( (keyPath) => !hasKeyPath(referenceData, keyPath), ); results[lang] = { valid: missing.length === 0 && extra.length === 0, missing, extra, }; if (missing.length > 0 || extra.length > 0) { hasErrors = true; } } // Print results console.log(colorize("Results:", "blue")); console.log("─".repeat(60)); for (const lang of LANGUAGES) { const result = results[lang]; if (result.valid) { console.log( colorize(`āœ“ ${lang.toUpperCase()}: All keys present`, "green"), ); } else { console.log(colorize(`āœ— ${lang.toUpperCase()}: Issues found`, "red")); if (result.missing.length > 0) { console.log( colorize(` Missing ${result.missing.length} keys:`, "yellow"), ); result.missing.slice(0, 10).forEach((keyPath) => { console.log(` - ${keyPath.join(".")}`); }); if (result.missing.length > 10) { console.log( colorize( ` ... and ${result.missing.length - 10} more`, "yellow", ), ); } } if (result.extra.length > 0) { console.log( colorize( ` Extra ${result.extra.length} keys (not in reference):`, "yellow", ), ); result.extra.slice(0, 10).forEach((keyPath) => { console.log(` - ${keyPath.join(".")}`); }); if (result.extra.length > 10) { console.log( colorize(` ... and ${result.extra.length - 10} more`, "yellow"), ); } } console.log(""); } } console.log("─".repeat(60)); // Summary const validCount = Object.values(results).filter((r) => r.valid).length; const totalCount = LANGUAGES.length; if (hasErrors) { console.log( colorize( `\nāœ— Validation failed: ${validCount}/${totalCount} languages passed`, "red", ), ); process.exit(1); } else { console.log( colorize( `\nāœ“ All ${totalCount} languages have complete translations!`, "green", ), ); process.exit(0); } } // Run validation validateTranslations();