149 lines
7.1 KiB
JavaScript
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 + '\\', '')));
|
|
})();
|