crush-level-web/scripts/reset-and-apply-translation...

368 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2025-11-13 08:38:25 +00:00
/*
CommonJS runtime for resetting files and applying translations from Excel to source code.
This script first resets files to their original state, then applies translations.
*/
const path = require('node:path');
const fs = require('node:fs');
const { execSync } = require('child_process');
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 resetFiles() {
console.log('🔄 重置文件到原始状态...');
try {
// 使用 git 重置所有修改的文件
execSync('git checkout -- .', { cwd: WORKDIR, stdio: 'inherit' });
console.log('✅ 文件重置完成');
} catch (error) {
console.error('❌ 重置文件失败:', error.message);
process.exit(1);
}
}
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 findTextInFile(sourceFile, translation) {
const { text, 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;
// 在文件中查找匹配的文本
const 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. 重置文件到原始状态
resetFiles();
// 2. 读取翻译数据
const translations = loadTranslations();
// 3. 按文件分组
const fileGroups = groupByFile(translations);
// 4. 处理每个文件
for (const [filePath, fileTranslations] of fileGroups) {
processFile(filePath, fileTranslations);
}
// 5. 生成报告
generateReport();
// 6. 打印总结
printSummary();
} catch (error) {
console.error('❌ 执行失败:', error);
process.exitCode = 1;
}
}
main();