/* 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()