const fs = require('fs'); const path = require('path'); const root = 'src'; const exts = ['.tsx', '.ts', '.jsx', '.js']; const excludeDirs = ['locales', 'node_modules']; const cjk = /[\u4e00-\u9fff]/; function walk(dir) { const out = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (excludeDirs.includes(entry.name)) continue; out.push(...walk(full)); } else if (exts.includes(path.extname(entry.name))) { out.push(full); } } return out; } // Load locale keys (flattened) function load(localePath) { let src = fs.readFileSync(localePath, 'utf8'); src = src.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {'); src = src.replace(/export\s+default\s+\w+;?/, ''); src = src.replace(/\bconst\b/g, 'var'); return new Function(src + '\nreturn obj;')(); } function flatten(obj, prefix = '', res = {}) { for (const k of Object.keys(obj)) { const key = prefix ? prefix + '.' + k : k; if (obj[k] && typeof obj[k] === 'object' && !Array.isArray(obj[k])) flatten(obj[k], key, res); else res[key] = obj[k]; } return res; } const zhF = flatten(load('src/locales/zh/index.ts')); const enF = flatten(load('src/locales/en/index.ts')); // 1) Find all t('...') / i18n.t('...') referenced keys const files = walk(root); const refKeys = new Set(); const refRegex = /(?:i18n\.t|[^a-zA-Z]t)\(\s*['"`]([a-zA-Z][a-zA-Z0-9_.]*?)['"`]/g; for (const f of files) { const content = fs.readFileSync(f, 'utf8'); let m; while ((m = refRegex.exec(content))) { refKeys.add(m[1]); } } const refArr = [...refKeys].sort(); const missingInZh = refArr.filter(k => !(k in zhF)); const missingInEn = refArr.filter(k => !(k in enF)); console.log('=== t() referenced keys missing in ZH (' + missingInZh.length + ') ==='); console.log(missingInZh.join('\n')); console.log('\n=== t() referenced keys missing in EN (' + missingInEn.length + ') ==='); console.log(missingInEn.join('\n')); console.log('\nTotal referenced keys:', refArr.length); // 2) Find user-facing hardcoded Chinese strings. // Exclude lines that are only comments or console.* (developer-facing). const userHits = []; for (const f of files) { const lines = fs.readFileSync(f, 'utf8').split('\n'); lines.forEach((line, i) => { const t = line.trim(); if (!cjk.test(line)) return; if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return; if (/^\{\/\*/.test(t)) return; // {/* ... */} // exclude console.* (browser console only) if (/\bconsole\.(log|error|warn|info|debug)\s*\(/.test(line)) return; userHits.push({ file: f, line: i + 1, text: t.slice(0, 220) }); }); } console.log('\n=== USER-FACING hardcoded CJK lines (' + userHits.length + ') ==='); for (const h of userHits) { console.log(h.file + ':' + h.line + ' | ' + h.text); }