// 只读提取源码中的中文候选;不扫描图片、生成物或第三方依赖。 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;