Files
web-Iot/tmp_replace.cjs
2026-08-14 09:02:52 +08:00

149 lines
7.1 KiB
JavaScript

// 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 = />([^<>`{}()]*[一-鿿][^<>`{}()]*)</g;
const rxAttr = /([A-Za-z_][\w-]*)=(["'`])((?:\\.|(?!\2)[^{}()])*?[一-鿿]+(?:\\.|(?!\2)[^{}()])*?)\2/g;
const rxMsg = /([A-Za-z_$][\w$]*\.(?:error|success|warning|info|loading|open))\(\s*(["'`])((?:\\.|(?!\2)[^{}()])*?[一-鿿]+(?:\\.|(?!\2)[^{}()])*?)\2\)/g;
const rxProp = /(^|[^.\w])((?:title|content|okText|cancelText|message|description|label|placeholder|text|tooltip|subTitle|subtitle)):\s*(["'`])((?:\\.|(?!\3)[^{}()])*?[一-鿿]+(?:\\.|(?!\3)[^{}()])*?)\3/g;
function loaderFor(f) { const e = path.extname(f); return e === '.tsx' ? 'tsx' : e === '.ts' ? 'ts' : e === '.jsx' ? 'jsx' : 'js'; }
const files = walk(root);
const newZh = {}, newEn = {};
const pending = [];
const balanceWarns = [];
let skippedNoT = 0;
for (const f of files) {
const src0 = fs.readFileSync(f, 'utf8');
if (!/useTranslation|[^A-Za-z]t\(/.test(src0)) { continue; }
if (!/[一-鿿]/.test(src0)) continue;
const map = {};
function keyFor(s) {
const t = s.trim();
if (!t) return null;
if (t in map) return map[t];
if (t in valToKey) { map[t] = valToKey[t]; return map[t]; }
const ns = nsForFile(f);
let base = camel(translateZh(t)) || ('s' + Object.keys(map).length);
let cand = ns + '.' + base;
let i = 2;
while (allKeys.has(cand) || cand in (newZh[ns] || {})) cand = ns + '.' + base + (i++);
map[t] = cand;
const leaf = cand.slice(ns.length + 1);
newZh[ns] = newZh[ns] || {}; newEn[ns] = newEn[ns] || {};
newZh[ns][leaf] = t; newEn[ns][leaf] = translateZh(t);
return cand;
}
let s = src0;
let rep = 0;
s = s.replace(rxJSX, (m, inner) => { 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 + '\\', '')));
})();