- 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>
189 lines
5.2 KiB
JavaScript
Executable file
189 lines
5.2 KiB
JavaScript
Executable file
#!/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);
|
||
}
|