// Conservative hardcoded-Chinese -> t() replacement with esbuild syntax gate. // Scope: files already using t(); only unambiguous UI contexts; braces/parens excluded; // files that fail esbuild transform are NOT written. const fs = require('fs'); const path = require('path'); const esbuild = require('esbuild'); const root = 'src'; const exts = ['.tsx', '.ts', '.jsx', '.js']; const excludeDirs = ['locales', 'node_modules', 'i18n']; function walk(dir) { const out = []; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, e.name); if (e.isDirectory()) { if (!excludeDirs.includes(e.name)) out.push(...walk(full)); } else if (exts.includes(path.extname(e.name))) out.push(full); } return out; } function load(p) { let s = fs.readFileSync(p, 'utf8'); s = s.replace(/const\s+\w+\s*=\s*\{/, 'var obj = {'); s = s.replace(/export\s+default\s+\w+;?/, ''); s = s.replace(/\bconst\b/g, 'var'); return new Function(s + '\nreturn obj;')(); } function flat(o, p, r) { p = p || ''; r = r || {}; for (const k of Object.keys(o)) { const key = p ? p + '.' + k : k; if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) flat(o[k], key, r); else r[key] = o[k]; } return r; } const zhMap = flat(load('src/locales/zh/index.ts')); const enMap = flat(load('src/locales/en/index.ts')); const valToKey = {}; const valToEn = {}; for (const k of Object.keys(zhMap)) { if (typeof zhMap[k] === 'string') { if (!(zhMap[k] in valToKey)) valToKey[zhMap[k]] = k; if (typeof enMap[k] === 'string') valToEn[zhMap[k]] = enMap[k]; } } const allKeys = new Set([...Object.keys(zhMap), ...Object.keys(enMap)]); const phraseKeys = Object.keys(valToEn).sort((a, b) => b.length - a.length); function translateZh(zh) { if (valToEn[zh]) return valToEn[zh]; let s = zh; for (const k of phraseKeys) { if (k.length >= 2) s = s.split(k).join('\u0001' + valToEn[k] + '\u0001'); } return s.split('\u0001').map(seg => valToEn[seg] || seg).join(' ').replace(/\s+/g, ' ').trim() || zh; } function nsForFile(f) { const rel = f.replace(root + '\\', '').replace(/\\/g, '/').toLowerCase(); if (rel.includes('systemsetting')) return 'systemSetting'; if (rel.includes('devices/') || rel.includes('device')) return 'devices'; if (rel.includes('alerts')) return 'alerts'; if (rel.includes('aianalysis') || rel.includes('aidiagnos')) return 'aiAnalysis'; if (rel.includes('clean')) return 'clean'; if (rel.includes('report')) return 'report'; if (rel.includes('workorder')) return 'workOrder'; if (rel.includes('login')) return 'login'; if (rel.includes('profile')) return 'profile'; if (rel.includes('users')) return 'users'; if (rel.includes('roles')) return 'roles'; if (rel.includes('home')) return 'home'; if (rel.includes('video')) return 'video'; if (rel.includes('router') || rel.includes('layout')) return 'layout'; return 'page'; } function camel(s) { s = s.replace(/[^A-Za-z0-9]+/g, ' ').trim().toLowerCase(); const parts = s.split(' ').filter(Boolean); if (!parts.length) return ''; return parts.map((p, i) => i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)).join(''); } const SKIP_ATTR = new Set(['key', 'id', 'path', 'href', 'src', 'to', 'type', 'name', 'value', 'className', 'data', 'mode', 'status', 'field', 'prop', 'ref']); const UI_PROP = new Set(['title', 'content', 'okText', 'cancelText', 'message', 'description', 'label', 'placeholder', 'text', 'tooltip', 'subTitle', 'subtitle']); const rxJSX = />([^<>`{}()]*[一-鿿][^<>`{}()]*) { const k = keyFor(inner); if (!k) return m; rep++; return '>{t(\'' + k + '\')}<'; }); s = s.replace(rxAttr, (m, attr, q, val) => { if (SKIP_ATTR.has(attr)) return m; if (val.includes('${')) return m; const k = keyFor(val); if (!k) return m; rep++; return attr + '={t(\'' + k + '\')}'; }); s = s.replace(rxMsg, (m, fn, q, val) => { if (fn.startsWith('console.')) return m; const k = keyFor(val); if (!k) return m; rep++; return fn + "(t('" + k + "'))"; }); s = s.replace(rxProp, (m, pre, prop, q, val) => { if (!UI_PROP.has(prop)) return m; const k = keyFor(val); if (!k) return m; rep++; return pre + prop + ": t('" + k + "')"; }); if (rep > 0) { const ob = (s.match(/[({]/g) || []).length, cb = (s.match(/[)}]/g) || []).length; const o0 = (src0.match(/[({]/g) || []).length, c0 = (src0.match(/[)}]/g) || []).length; if (ob !== cb) balanceWarns.push(f.replace(root + '\\', '') + ' braces ' + ob + '/' + cb + ' (orig ' + o0 + '/' + c0 + ')'); pending.push({ f, s, rep }); } } (async () => { const modified = []; let totalRep = 0; for (const p of pending) { let ok = false; let errMsg = ''; try { await esbuild.transform(p.s, { loader: loaderFor(p.f), target: 'es2020' }); ok = true; } catch (err) { errMsg = String(err.message); } if (ok) { if (!process.env.DRY) fs.writeFileSync(p.f, p.s, 'utf8'); modified.push(p.f); totalRep += p.rep; } else { console.log(' --- detail for ' + p.f.replace(root + '\\', '') + ' ---'); console.log(errMsg); } } console.log('Files written/verified:', modified.length); console.log('Replacements made:', totalRep); console.log('Balance warnings (informational):', balanceWarns.length); balanceWarns.forEach(w => console.log(' [WARN] ' + w)); console.log('New locale keys (zh):', Object.values(newZh).reduce((a, o) => a + Object.keys(o).length, 0)); fs.writeFileSync('tmp_newkeys.json', JSON.stringify({ zh: newZh, en: newEn }, null, 2)); console.log('Modified files:'); modified.forEach(f => console.log(' ' + f.replace(root + '\\', ''))); })();