Files
shoot-miniprograms/scripts/localization-inventory.cjs
T
2026-11-05 18:25:03 +08:00

64 lines
3.6 KiB
JavaScript

// 只读提取源码中的中文候选;不扫描图片、生成物或第三方依赖。
const fs = require('node:fs');
const path = require('node:path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const sfc = require('@vue/compiler-sfc');
const dom = require('@vue/compiler-dom');
const han = /[\p{Script=Han}]/u;
const root = path.resolve(__dirname, '../src');
const entries = [];
function scripts(code, file, offset = 0, context = 'script') {
if (context === 'expression') { code = '(' + code + ')'; offset -= 1; }
const ast = parser.parse(code, {sourceType: 'module', plugins: ['typescript'], errorRecovery: true});
traverse(ast, {
StringLiteral(p) { add(p, p.node.value, 'string'); },
TemplateLiteral(p) {
if (!p.node.quasis.some(q => han.test(q.value.cooked || q.value.raw))) return;
const text = p.node.quasis.map((q, i) => (q.value.cooked ?? q.value.raw) + (i < p.node.expressions.length ? `{p${i}}` : '')).join('');
add(p, text, 'template');
},
});
function add(p, text, kind) {
if (!han.test(text) || /^https?:/.test(text)) return;
const ancestors = [];
let q = p;
for (let i = 0; q && i < 5; i++, q = q.parentPath) {
const n = q.node;
ancestors.push(n.type + (n.key?.name ? ':' + n.key.name : '') + (n.type === 'CallExpression' ? ':' + code.slice(n.callee.start, n.callee.end) : ''));
}
entries.push({file, start: offset + p.node.start, end: offset + p.node.end, kind, text, context, ancestors: ancestors.join(' > '), snippet: code.slice(Math.max(0, p.node.start - 65), Math.min(code.length, p.node.end + 65))});
}
}
function inspect(file) {
const code = fs.readFileSync(path.join(root, file), 'utf8');
if (file.endsWith('.js')) return scripts(code, file);
const {descriptor} = sfc.parse(code);
for (const block of [descriptor.script, descriptor.scriptSetup]) if (block) scripts(block.content, file, block.loc.start.offset);
const block = descriptor.template;
if (!block) return;
const ast = dom.baseParse(block.content);
function walk(n) {
if (n.type === 2 && han.test(n.content)) entries.push({file, start: block.loc.start.offset + n.loc.start.offset, end: block.loc.start.offset + n.loc.end.offset, kind:'text', text:n.content, context:'template'});
if (n.type === 5 && han.test(n.content.content)) scripts(n.content.content, file, block.loc.start.offset + n.content.loc.start.offset, 'expression');
for (const prop of n.props || []) {
if (prop.type === 6 && prop.value && han.test(prop.value.content) && !/^(src|class|id|style|key|name|value)$/.test(prop.name)) entries.push({file, start:block.loc.start.offset + prop.loc.start.offset, end:block.loc.start.offset + prop.loc.end.offset, kind:'attribute', text:prop.value.content, attribute:prop.name, context:'template'});
if (prop.type === 7 && prop.exp && han.test(prop.exp.content)) {
try { scripts(prop.exp.content, file, block.loc.start.offset + prop.exp.loc.start.offset, 'expression'); } catch (e) { /* v-for 等非独立表达式保留人工检查 */ }
}
}
for (const child of n.children || []) walk(child);
}
walk(ast);
}
function walk(dir) {
for (const item of fs.readdirSync(path.join(root, dir), {withFileTypes:true})) {
const file = path.posix.join(dir, item.name);
if (item.isDirectory()) { if (item.name !== 'static' && item.name !== 'locales') walk(file); }
else if (/\.(vue|js)$/.test(file) && !/match\.min\.js|audioManager\.js/.test(file)) inspect(file);
}
}
walk('');
if (require.main === module) process.stdout.write(JSON.stringify(entries, null, 2));
module.exports = entries;