tractatus/scripts/validate-i18n.js
TheFlow afcfc27502 feat: Complete Phase 2 Agent Lightning website integration
- Added Agent Lightning research section to researcher.html with Demo 2 results
- Created comprehensive /integrations/agent-lightning.html page
- Added Agent Lightning link in homepage hero section
- Updated Discord invite links (Tractatus + semantipy) across all pages
- Added feedback.js script to all key pages for live demonstration

Phase 2 of Master Plan complete: Discord setup → Website completion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:38:20 +13:00

189 lines
5.2 KiB
JavaScript
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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