/* CommonJS runtime for applying translations from Excel to source code. */ const path = require('node:path'); const fs = require('node:fs'); const { Project, SyntaxKind, Node } = require('ts-morph'); const XLSX = require('xlsx'); const WORKDIR = process.cwd(); const TRANSLATES_FILE = path.join(WORKDIR, 'scripts', 'translates.xlsx'); const REPORT_FILE = path.join(WORKDIR, 'scripts', 'translation-report.json'); const CONFLICTS_FILE = path.join(WORKDIR, 'scripts', 'translation-conflicts.xlsx'); // 统计信息 const stats = { total: 0, success: 0, conflicts: 0, fileNotFound: 0, textNotFound: 0, multipleMatches: 0 }; // 冲突列表 const conflicts = []; // 成功替换列表 const successfulReplacements = []; function loadTranslations() { console.log('📖 读取翻译数据...'); const wb = XLSX.readFile(TRANSLATES_FILE); const ws = wb.Sheets[wb.SheetNames[0]]; const data = XLSX.utils.sheet_to_json(ws, { defval: '' }); // 筛选出需要替换的条目 let translations = data.filter(row => row.text && row.corrected_text && row.text !== row.corrected_text ); // 去重:按 file + line + text 去重,保留第一个 const seen = new Set(); translations = translations.filter(row => { const key = `${row.file}:${row.line}:${row.text}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); console.log(`📊 找到 ${translations.length} 条需要替换的翻译(已去重)`); stats.total = translations.length; return translations; } function groupByFile(translations) { const groups = new Map(); for (const translation of translations) { const filePath = path.join(WORKDIR, translation.file); if (!groups.has(filePath)) { groups.set(filePath, []); } groups.get(filePath).push(translation); } return groups; } function findTextInNode(node, targetText, kind) { if (!node) return null; // 处理 JSX 文本节点 if (Node.isJsxText(node)) { const text = node.getText().replace(/\s+/g, ' ').trim(); if (text === targetText) return node; } // 处理字符串字面量 if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) { const text = node.getLiteralText(); if (text === targetText) return node; } // 处理 JSX 表达式中的字符串 if (Node.isJsxExpression(node)) { const expr = node.getExpression(); if (expr && (Node.isStringLiteral(expr) || Node.isNoSubstitutionTemplateLiteral(expr))) { const text = expr.getLiteralText(); if (text === targetText) return node; } } return null; } function findTextInFile(sourceFile, translation) { const { text, line, kind } = translation; const matches = []; sourceFile.forEachDescendant((node) => { // 根据 kind 类型进行不同的匹配 if (kind === 'text') { // 查找 JSX 文本节点 if (Node.isJsxText(node)) { const nodeText = node.getText().replace(/\s+/g, ' ').trim(); if (nodeText === text) { matches.push({ node, type: 'jsx-text', line: node.getStartLineNumber() }); } } // 查找 JSX 表达式中的字符串 if (Node.isJsxExpression(node)) { const expr = node.getExpression(); if (expr && (Node.isStringLiteral(expr) || Node.isNoSubstitutionTemplateLiteral(expr))) { const nodeText = expr.getLiteralText(); if (nodeText === text) { matches.push({ node: expr, type: 'jsx-expression', line: node.getStartLineNumber() }); } } } } else if (['placeholder', 'title', 'alt', 'label', 'aria'].includes(kind)) { // 查找 JSX 属性 if (Node.isJsxAttribute(node)) { const name = node.getNameNode().getText().toLowerCase(); const value = getStringFromInitializer(node); if (value === text) { matches.push({ node, type: 'jsx-attribute', line: node.getStartLineNumber() }); } } } else if (['toast', 'dialog', 'error', 'validation'].includes(kind)) { // 查找函数调用中的字符串参数 if (Node.isCallExpression(node)) { const args = node.getArguments(); for (const arg of args) { if (Node.isStringLiteral(arg) || Node.isNoSubstitutionTemplateLiteral(arg)) { const nodeText = arg.getLiteralText(); if (nodeText === text) { matches.push({ node: arg, type: 'function-arg', line: node.getStartLineNumber() }); } } } } } }); return matches; } function getStringFromInitializer(attr) { const init = attr.getInitializer(); if (!init) return undefined; if (Node.isStringLiteral(init) || Node.isNoSubstitutionTemplateLiteral(init)) { return init.getLiteralText(); } if (Node.isJsxExpression(init)) { const expr = init.getExpression(); if (!expr) return undefined; if (Node.isStringLiteral(expr) || Node.isNoSubstitutionTemplateLiteral(expr)) { return expr.getLiteralText(); } } return undefined; } function replaceText(node, newText, type) { try { if (type === 'jsx-text') { // JSX 文本节点需要特殊处理,保持空白字符 const originalText = node.getText(); const newTextWithWhitespace = originalText.replace(/\S+/g, newText); node.replaceWithText(newTextWithWhitespace); } else if (type === 'jsx-expression' || type === 'function-arg') { // 字符串字面量 if (Node.isStringLiteral(node)) { node.replaceWithText(`"${newText}"`); } else if (Node.isNoSubstitutionTemplateLiteral(node)) { node.replaceWithText(`\`${newText}\``); } } else if (type === 'jsx-attribute') { // JSX 属性值 const init = node.getInitializer(); if (init) { if (Node.isStringLiteral(init)) { init.replaceWithText(`"${newText}"`); } else if (Node.isNoSubstitutionTemplateLiteral(init)) { init.replaceWithText(`\`${newText}\``); } else if (Node.isJsxExpression(init)) { const expr = init.getExpression(); if (expr && (Node.isStringLiteral(expr) || Node.isNoSubstitutionTemplateLiteral(expr))) { if (Node.isStringLiteral(expr)) { expr.replaceWithText(`"${newText}"`); } else { expr.replaceWithText(`\`${newText}\``); } } } } } return true; } catch (error) { console.error(`❌ 替换失败: ${error.message}`); return false; } } function processFile(filePath, translations) { if (!fs.existsSync(filePath)) { console.log(`❌ 文件不存在: ${path.relative(WORKDIR, filePath)}`); translations.forEach(t => { conflicts.push({ ...t, conflictType: 'FILE_NOT_FOUND', conflictReason: '文件不存在' }); }); stats.fileNotFound += translations.length; return; } console.log(`📝 处理文件: ${path.relative(WORKDIR, filePath)}`); try { const project = new Project({ tsConfigFilePath: path.join(WORKDIR, 'tsconfig.json'), skipAddingFilesFromTsConfig: true }); const sourceFile = project.addSourceFileAtPath(filePath); for (const translation of translations) { const { text, corrected_text, line, kind } = translation; // 首先在指定行附近查找 let matches = findTextInFile(sourceFile, translation); // 如果没找到,在整个文件中搜索 if (matches.length === 0) { matches = findTextInFile(sourceFile, translation); } if (matches.length === 0) { conflicts.push({ ...translation, conflictType: 'TEXT_NOT_FOUND_IN_FILE', conflictReason: '在文件中找不到匹配的文本' }); stats.textNotFound++; continue; } if (matches.length > 1) { conflicts.push({ ...translation, conflictType: 'MULTIPLE_MATCHES', conflictReason: `找到 ${matches.length} 个匹配项,需要人工确认` }); stats.multipleMatches++; continue; } // 执行替换 const match = matches[0]; const success = replaceText(match.node, corrected_text, match.type); if (success) { successfulReplacements.push({ ...translation, actualLine: match.line, replacementType: match.type }); stats.success++; console.log(`✅ 替换成功: "${text}" -> "${corrected_text}" (行 ${match.line})`); } else { conflicts.push({ ...translation, conflictType: 'REPLACEMENT_FAILED', conflictReason: '替换操作失败' }); stats.conflicts++; } } // 保存修改后的文件 sourceFile.saveSync(); } catch (error) { console.error(`❌ 处理文件失败: ${filePath}`, error.message); translations.forEach(t => { conflicts.push({ ...t, conflictType: 'PARSE_ERROR', conflictReason: `文件解析失败: ${error.message}` }); }); stats.conflicts += translations.length; } } function generateReport() { console.log('\n📊 生成报告...'); // 生成成功替换报告 const report = { timestamp: new Date().toISOString(), stats, successfulReplacements, conflicts: conflicts.map(c => ({ file: c.file, line: c.line, text: c.text, corrected_text: c.corrected_text, conflictType: c.conflictType, conflictReason: c.conflictReason })) }; fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2)); console.log(`📄 成功替换报告已保存: ${REPORT_FILE}`); // 生成冲突报告 Excel if (conflicts.length > 0) { const conflictRows = conflicts.map(c => ({ file: c.file, line: c.line, text: c.text, corrected_text: c.corrected_text, conflictType: c.conflictType, conflictReason: c.conflictReason, route: c.route, componentOrFn: c.componentOrFn, kind: c.kind, keyOrLocator: c.keyOrLocator })); const ws = XLSX.utils.json_to_sheet(conflictRows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'conflicts'); XLSX.writeFile(wb, CONFLICTS_FILE); console.log(`📄 冲突报告已保存: ${CONFLICTS_FILE}`); } } function printSummary() { console.log('\n📈 处理完成!'); console.log(`总翻译条目: ${stats.total}`); console.log(`✅ 成功替换: ${stats.success}`); console.log(`❌ 文件不存在: ${stats.fileNotFound}`); console.log(`❌ 文本未找到: ${stats.textNotFound}`); console.log(`❌ 多处匹配: ${stats.multipleMatches}`); console.log(`❌ 其他冲突: ${stats.conflicts}`); console.log(`\n成功率: ${((stats.success / stats.total) * 100).toFixed(1)}%`); } async function main() { console.log('🚀 开始应用翻译...\n'); try { // 1. 读取翻译数据 const translations = loadTranslations(); // 2. 按文件分组 const fileGroups = groupByFile(translations); // 3. 处理每个文件 for (const [filePath, fileTranslations] of fileGroups) { processFile(filePath, fileTranslations); } // 4. 生成报告 generateReport(); // 5. 打印总结 printSummary(); } catch (error) { console.error('❌ 执行失败:', error); process.exitCode = 1; } } main();