#!/usr/bin/env node /** * I18n Validation Script * Ensures all data-i18n keys in HTML files have corresponding translations * in all locale JSON files * * Usage: node scripts/validate-i18n.js [--verbose] * Exit codes: * 0 - All i18n keys validated successfully * 1 - Missing translations found * 2 - Script error */ const fs = require('fs'); const path = require('path'); const verbose = process.argv.includes('--verbose'); // Configuration const PUBLIC_DIR = path.join(__dirname, '../public'); const LOCALES_DIR = path.join(PUBLIC_DIR, 'locales'); const SUPPORTED_LOCALES = ['en', 'de', 'fr']; const HTML_FILES = [ 'index.html', 'researcher.html', 'implementer.html', 'leader.html', 'docs.html', 'faq.html', 'about.html' ]; // Extract all data-i18n keys from HTML files function extractI18nKeys(htmlContent) { const keys = new Set(); const regex = /data-i18n="([^"]+)"/g; const regexHtml = /data-i18n-html="([^"]+)"/g; let match; while ((match = regex.exec(htmlContent)) !== null) { keys.add(match[1]); } while ((match = regexHtml.exec(htmlContent)) !== null) { keys.add(match[1]); } return Array.from(keys); } // Check if a key exists in a nested JSON object function keyExists(obj, keyPath) { const parts = keyPath.split('.'); let current = obj; for (const part of parts) { if (!current || typeof current !== 'object' || !(part in current)) { return false; } current = current[part]; } return true; } // Main validation function validateI18n() { console.log('šŸŒ Validating i18n translations...\n'); let totalKeys = 0; let missingTranslations = []; // Process each HTML file for (const htmlFile of HTML_FILES) { const htmlPath = path.join(PUBLIC_DIR, htmlFile); if (!fs.existsSync(htmlPath)) { if (verbose) console.log(`āš ļø Skipping ${htmlFile} (not found)`); continue; } const htmlContent = fs.readFileSync(htmlPath, 'utf8'); const keys = extractI18nKeys(htmlContent); if (keys.length === 0) { if (verbose) console.log(`ā„¹ļø ${htmlFile}: No i18n keys found`); continue; } console.log(`šŸ“„ ${htmlFile}: Found ${keys.length} i18n keys`); totalKeys += keys.length; // Check each key in all locales for (const key of keys) { for (const locale of SUPPORTED_LOCALES) { // Determine which JSON file to check // homepage.html -> homepage.json // index.html keys (hero, community, etc) -> homepage.json let jsonFile; if (htmlFile === 'index.html') { jsonFile = 'homepage.json'; } else { const baseName = htmlFile.replace('.html', ''); jsonFile = `${baseName}.json`; } const localePath = path.join(LOCALES_DIR, locale, jsonFile); if (!fs.existsSync(localePath)) { // Try common.json as fallback const fallbackPath = path.join(LOCALES_DIR, locale, 'common.json'); if (fs.existsSync(fallbackPath)) { const translations = JSON.parse(fs.readFileSync(fallbackPath, 'utf8')); if (!keyExists(translations, key)) { missingTranslations.push({ file: htmlFile, key: key, locale: locale, issue: `Key not found in ${jsonFile} or common.json` }); } } else { missingTranslations.push({ file: htmlFile, key: key, locale: locale, issue: `Translation file not found: ${jsonFile}` }); } continue; } const translations = JSON.parse(fs.readFileSync(localePath, 'utf8')); if (!keyExists(translations, key)) { missingTranslations.push({ file: htmlFile, key: key, locale: locale, issue: 'Key not found in translation file' }); } } } } // Report results console.log(`\nšŸ“Š Validation Summary:`); console.log(` Total i18n keys: ${totalKeys}`); console.log(` Locales checked: ${SUPPORTED_LOCALES.join(', ')}`); if (missingTranslations.length === 0) { console.log(`\nāœ… All i18n keys have translations in all locales!\n`); return 0; } console.log(`\nāŒ Found ${missingTranslations.length} missing translations:\n`); // Group by file and locale const byFile = {}; for (const item of missingTranslations) { if (!byFile[item.file]) byFile[item.file] = {}; if (!byFile[item.file][item.locale]) byFile[item.file][item.locale] = []; byFile[item.file][item.locale].push(item); } for (const [file, locales] of Object.entries(byFile)) { console.log(`šŸ“„ ${file}:`); for (const [locale, items] of Object.entries(locales)) { console.log(` ${locale}: ${items.length} missing`); for (const item of items) { console.log(` - ${item.key}: ${item.issue}`); } } console.log(''); } console.log('āš ļø Please add missing translations before deploying.\n'); return 1; } // Run validation try { const exitCode = validateI18n(); process.exit(exitCode); } catch (error) { console.error('āŒ Validation script error:', error.message); if (verbose) console.error(error.stack); process.exit(2); }