#!/usr/bin/env node /** * Validate translations completeness and HTML output */ const fs = require('fs'); const path = require('path'); const LANGUAGES = ['zh-cn', 'zh-tw', 'ja', 'ko', 'th', 'vi', 'id', 'ms', 'hi']; const REQUIRED_KEYS = [ 'TITLE', 'META_DESC', 'NAV_HOW', 'NAV_MISSIONS', 'NAV_EARNINGS', 'NAV_PARTNERS', 'NAV_FAQ', 'CTA_START', 'HERO_BADGE', 'HERO_H1', 'HERO_H1_EMPH', 'HERO_SUB', 'EMAIL_PLACEHOLDER', 'TRUST_LINE', 'STAT1_NUM', 'STAT1_LBL', 'STAT2_NUM', 'STAT2_LBL', 'STAT3_NUM', 'STAT3_LBL', 'STATS_1_VAL', 'STATS_1_LBL', 'STATS_2_VAL', 'STATS_2_LBL', 'STATS_3_VAL', 'STATS_3_LBL', 'STATS_4_VAL', 'STATS_4_LBL', 'HOW_TITLE', 'HOW_SUB', 'STEP1_TITLE', 'STEP1_DESC', 'STEP2_TITLE', 'STEP2_DESC', 'STEP3_TITLE', 'STEP3_DESC', 'MISSIONS_TITLE', 'MISSIONS_SUB', 'M1_TITLE', 'M1_DESC', 'M1_CHIP', 'M2_TITLE', 'M2_DESC', 'M2_CHIP', 'M3_TITLE', 'M3_DESC', 'M3_CHIP', 'EARNINGS_TITLE', 'EARNINGS_SUB', 'TIER1_NAME', 'TIER1_MISSIONS', 'TIER1_F1', 'TIER1_F2', 'TIER1_F3', 'TIER2_NAME', 'TIER2_MISSIONS', 'TIER2_F1', 'TIER2_F2', 'TIER2_F3', 'TIER3_NAME', 'TIER3_MISSIONS', 'TIER3_F1', 'TIER3_F2', 'TIER3_F3', 'PARTNER_TITLE', 'PARTNER_SUB', 'PARTNER_HERO_DESC', 'PARTNER_MODEL_TITLE', 'PARTNER_MODEL_SUB', 'PARTNER_M1_TITLE', 'PARTNER_M1_DESC', 'PARTNER_M2_TITLE', 'PARTNER_M2_DESC', 'PARTNER_M3_TITLE', 'PARTNER_M3_DESC', 'PARTNER_M4_TITLE', 'PARTNER_M4_DESC', 'PARTNER_REQ_TITLE', 'PARTNER_REQ_SUB', 'PARTNER_R1_TITLE', 'PARTNER_R1_DESC', 'PARTNER_R2_TITLE', 'PARTNER_R2_DESC', 'PARTNER_R3_TITLE', 'PARTNER_R3_DESC', 'PARTNER_R4_TITLE', 'PARTNER_R4_DESC', 'PARTNER_R5_TITLE', 'PARTNER_R5_DESC', 'PARTNER_R6_TITLE', 'PARTNER_R6_DESC', 'PARTNER_AUTH_TITLE', 'PARTNER_AUTH_SUB', 'PARTNER_A1_TITLE', 'PARTNER_A1_DESC', 'PARTNER_A2_TITLE', 'PARTNER_A2_DESC', 'PARTNER_A3_TITLE', 'PARTNER_A3_DESC', 'PARTNER_A4_TITLE', 'PARTNER_A4_DESC', 'PARTNER_A5_TITLE', 'PARTNER_A5_DESC', 'PARTNER_A6_TITLE', 'PARTNER_A6_DESC', 'PARTNER_CTA', 'PARTNER_CTA_SUB', 'FAQ_TITLE', 'FAQ_SUB', 'FAQ_Q1', 'FAQ_A1', 'FAQ_Q2', 'FAQ_A2', 'FAQ_Q3', 'FAQ_A3', 'FAQ_Q4', 'FAQ_A4', 'FAQ_Q5', 'FAQ_A5', 'FAQ_Q6', 'FAQ_A6', 'FAQ_Q7', 'FAQ_A7', 'FOOTER_TAGLINE', 'FOOTER_COL1_TITLE', 'FOOTER_COL1_L1', 'FOOTER_COL1_L2', 'FOOTER_COL1_L3', 'FOOTER_COL1_L4', 'FOOTER_COL2_TITLE', 'FOOTER_COL2_L1', 'FOOTER_COL2_L2', 'FOOTER_COL2_L3', 'FOOTER_COL3_TITLE', 'FOOTER_COL3_L1', 'FOOTER_COL3_L2', 'FOOTER_COL3_L3', 'COPYRIGHT', 'LANG_LABEL', 'PHONE_MISSION', 'PHONE_ADDR', 'PHONE_REWARD', 'PHONE_META', ]; let errors = 0; console.log('šŸ” Validating translations...\n'); for (const lang of LANGUAGES) { const transPath = path.join(__dirname, `../src/translations/${lang}.json`); if (!fs.existsSync(transPath)) { console.error(`āŒ Missing: ${lang}.json`); errors++; continue; } const trans = JSON.parse(fs.readFileSync(transPath, 'utf8')); const missing = REQUIRED_KEYS.filter(k => !trans[k] || trans[k].trim() === ''); if (missing.length > 0) { console.error(`āŒ ${lang}: Missing ${missing.length} keys`); for (const key of missing.slice(0, 5)) { console.error(` - ${key}`); } if (missing.length > 5) console.error(` ... and ${missing.length - 5} more`); errors++; } else { console.log(`āœ… ${lang}: Complete`); } } // Check for placeholder leaks const distDir = path.join(__dirname, '../dist'); if (fs.existsSync(distDir)) { console.log('\nšŸ” Checking for placeholder leaks in dist/...'); for (const lang of LANGUAGES) { const htmlPath = path.join(distDir, lang, 'index.html'); if (fs.existsSync(htmlPath)) { const html = fs.readFileSync(htmlPath, 'utf8'); const leaks = html.match(/\{\{\w+\}\}/g); if (leaks) { console.error(`āŒ ${lang}: ${leaks.length} placeholders leaked`); errors++; } } } } if (errors > 0) { console.log(`\nāŒ ${errors} error(s) found`); process.exit(1); } else { console.log('\nāœ… All validations passed'); }