UNPKG

pick-cn

Version:

A command line tool to translate Chinese text to English and generate JSON mapping files

700 lines (604 loc) 22.2 kB
const fs = require('fs-extra'); const path = require('path'); const glob = require('glob'); const { TranslationManager } = require('./translators'); /** * 执行中文转英文翻译的主函数 * @param {Object} options - 配置选项 * @param {string} options.source - 源目录路径 * @param {string} options.target - 目标目录路径 * @param {string} options.output - 输出文件名 * @param {string} options.translator - 翻译服务 * @param {string} options.apiConfig - API 配置文件路径 */ async function execute(options) { try { const { source, target, output, translator, apiConfig } = options; console.log(`📂 源目录: ${source}`); console.log(`📁 目标目录: ${target || source}`); console.log(`📄 输出文件: ${output}`); console.log(`🌐 翻译服务: ${translator}`); // 加载 API 配置(自动查找并合并配置文件) await loadApiConfig(source, apiConfig); // 查找所有需要处理的文件 const files = await findSourceFiles(source); console.log(`🔍 找到 ${files.length} 个文件需要处理`); // 提取中文文本 const chineseTexts = new Set(); for (const file of files) { const texts = await extractChineseFromFile(file); texts.forEach(text => chineseTexts.add(text)); } console.log(`📝 初步提取到 ${chineseTexts.size} 个中文文本`); // 进一步去重和清理(处理空格、标点符等差异) const uniqueTexts = deduplicateTexts(Array.from(chineseTexts)); console.log(`✨ 去重后剩余 ${uniqueTexts.length} 个唯一中文文本(减少 ${chineseTexts.size - uniqueTexts.length} 个重复项)`); // 生成中英文映射 JSON const mapping = await generateMapping(uniqueTexts, translator); // 保存 JSON 文件 const outputPath = path.join(target || source, output); await fs.writeJson(outputPath, mapping, { spaces: 2 }); console.log(`✅ 翻译完成!JSON 文件已保存到: ${outputPath}`); } catch (error) { console.error('❌ 执行失败:', error.message); process.exit(1); } } /** * 查找源文件 * @param {string} sourcePath - 源目录路径 * @returns {Promise<string[]>} 文件路径数组 */ async function findSourceFiles(sourcePath) { const patterns = [ '**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.vue' ]; const files = []; for (const pattern of patterns) { // 首先扫描 src 目录(主要代码目录) const srcMatches = glob.sync(path.join(sourcePath, 'src', pattern), { ignore: ['**/node_modules/**', '**/dist/**', '**/*.min.js'] }); files.push(...srcMatches); // 如果没有 src 目录或者需要扫描整个目录,则扫描整个指定目录 const allMatches = glob.sync(path.join(sourcePath, pattern), { ignore: ['**/node_modules/**', '**/dist/**', '**/*.min.js', '**/src/**'] // 排除 src 目录避免重复 }); files.push(...allMatches); } return files; } /** * 从文件中提取中文文本 * @param {string} filePath - 文件路径 * @returns {Promise<string[]>} 中文文本数组 */ async function extractChineseFromFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); const chineseTexts = []; // 1. 提取 TypeScript 枚举定义中的中文 value(不处理中文 key) // 匹配格式: KEY = '中文值' 或 KEY = "中文值" 或 KEY = `中文值` const enumValueRegex = /\w+\s*=\s*(['"`])([^'"`)]*[\u4e00-\u9fff][^'"`)]*?)\1/g; let match; while ((match = enumValueRegex.exec(content)) !== null) { const rawText = match[2].trim(); // match[2] 是第二个捕获组(中文内容) const cleanedSegments = cleanChineseText(rawText); // 清理并分割成多个中文片段 // 将每个有效的中文片段添加到结果中 for (const segment of cleanedSegments) { if (segment && isValidChineseText(segment)) { chineseTexts.push(segment); } } } // 2. 提取普通字符串字面量中的中文(排除枚举使用处) const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 跳过枚举使用的行(如 key: EnumType.中文key) if (isEnumUsageLine(line)) { continue; } // 提取该行中的中文字符串 const lineTexts = extractChineseFromLine(line); chineseTexts.push(...lineTexts); } return [...new Set(chineseTexts)]; // 去重 } catch (error) { console.warn(`⚠️ 读取文件失败: ${filePath}`, error.message); return []; } } /** * 检查是否为枚举使用的行 * @param {string} line - 代码行 * @returns {boolean} 是否为枚举使用行 */ function isEnumUsageLine(line) { // 匹配 key: EnumType.中文 或 EnumType.中文 这样的模式 const enumUsagePatterns = [ /\w+\.[\u4e00-\u9fff]/, // EnumType.中文 /key:\s*\w+\.[\u4e00-\u9fff]/, // key: EnumType.中文 ]; return enumUsagePatterns.some(pattern => pattern.test(line)); } /** * 检查是否为 console.log 相关的行 * @param {string} line - 代码行 * @returns {boolean} 是否为 console.log 相关行 */ function isConsoleLogLine(line) { // 去除行首空白字符进行检查 const trimmedLine = line.trim(); // 匹配各种 console 方法调用 const consolePatterns = [ /^console\.log\s*\(/, // console.log( /^console\.warn\s*\(/, // console.warn( /^console\.error\s*\(/, // console.error( /^console\.info\s*\(/, // console.info( /^console\.debug\s*\(/, // console.debug( /^console\.trace\s*\(/, // console.trace( /^console\.table\s*\(/, // console.table( /^console\.dir\s*\(/, // console.dir( /^console\.group\s*\(/, // console.group( /^console\.groupEnd\s*\(/,// console.groupEnd( ]; return consolePatterns.some(pattern => pattern.test(trimmedLine)); } /** * 从单行代码中提取中文文本 * @param {string} line - 代码行 * @returns {string[]} 中文文本数组 */ function extractChineseFromLine(line) { const texts = []; // 过滤掉 console.log 相关的行 if (isConsoleLogLine(line)) { return texts; // 返回空数组,不处理 console.log 中的中文 } // 匹配单引号字符串中的中文 const singleQuoteRegex = /'([^']*[\u4e00-\u9fff][^']*)'/g; // 匹配双引号字符串中的中文 const doubleQuoteRegex = /"([^"]*[\u4e00-\u9fff][^"]*)"/g; // 匹配模板字符串中的中文 const templateRegex = /`([^`]*[\u4e00-\u9fff][^`]*)`/g; let match; // 提取单引号中的中文 while ((match = singleQuoteRegex.exec(line)) !== null) { const rawText = match[1].trim(); const cleanedSegments = cleanChineseText(rawText); // 清理并分割成多个中文片段 // 将每个有效的中文片段添加到结果中 for (const segment of cleanedSegments) { if (segment && isValidChineseText(segment)) { texts.push(segment); } } } // 提取双引号中的中文 while ((match = doubleQuoteRegex.exec(line)) !== null) { const rawText = match[1].trim(); const cleanedSegments = cleanChineseText(rawText); // 清理并分割成多个中文片段 // 将每个有效的中文片段添加到结果中 for (const segment of cleanedSegments) { if (segment && isValidChineseText(segment)) { texts.push(segment); } } } // 提取模板字符串中的中文 while ((match = templateRegex.exec(line)) !== null) { const templateContent = match[1]; // 如果模板字符串不包含表达式,直接处理 if (!templateContent.includes('${')) { const rawText = templateContent.trim(); const cleanedSegments = cleanChineseText(rawText); // 清理并分割成多个中文片段 // 将每个有效的中文片段添加到结果中 for (const segment of cleanedSegments) { if (segment && isValidChineseText(segment)) { texts.push(segment); } } } else { // 如果包含表达式,提取被${}分割的中文片段 const segments = templateContent.split(/\$\{[^}]*\}/); for (const segment of segments) { const rawText = segment.trim(); const cleanedSegments = cleanChineseText(rawText); // 清理并分割成多个中文片段 // 将每个有效的中文片段添加到结果中 for (const cleanedSegment of cleanedSegments) { if (cleanedSegment && isValidChineseText(cleanedSegment)) { texts.push(cleanedSegment); } } } } } return texts; } /** * 检查文本是否包含中文 * @param {string} text - 待检查的文本 * @returns {boolean} 是否包含中文 */ function containsChinese(text) { return /[\u4e00-\u9fff]/.test(text); } /** * 智能去重中文文本(处理空格、标点符等细微差异) * @param {string[]} texts - 中文文本数组 * @returns {string[]} 去重后的中文文本数组 */ function deduplicateTexts(texts) { const seen = new Map(); // 使用 Map 来存储规范化后的文本和原始文本的映射 const result = []; for (const text of texts) { // 规范化文本:去除首尾空格、统一多个空格为一个、去除部分标点符 const normalized = text .trim() // 去除首尾空格 .replace(/\s+/g, ' ') // 多个空格合并为一个 .replace(/[。,;:“”‘’()、《》]/g, '') // 去除常见中文标点符 .toLowerCase(); // 转为小写(对于英文字母) if (!seen.has(normalized)) { seen.set(normalized, text); result.push(text); // 保留原始文本格式 } else { // 如果发现重复,选择更好的版本(更完整或更常见的格式) const existingText = seen.get(normalized); if (text.length > existingText.length || (文本质量评分(text) > 文本质量评分(existingText))) { // 更新为更好的版本 const index = result.indexOf(existingText); if (index !== -1) { result[index] = text; seen.set(normalized, text); } } } } return result; } /** * 评估文本质量分数(用于选择更好的重复文本版本) * @param {string} text - 文本 * @returns {number} 质量分数(越高越好) */ function 文本质量评分(text) { let score = 0; // 长度加分(但不过度偏向长文本) score += Math.min(text.length, 20); // 完整性加分(包含标点符说明更完整) if (/[。?!]$/.test(text)) score += 5; // 以句号结尾 if (/[,、]/.test(text)) score += 2; // 包含逗号 // 减分项(不完整的文本) if (text.startsWith(',') || text.startsWith('、')) score -= 3; // 以逗号开头 if (text.endsWith(',') || text.endsWith('、')) score -= 1; // 以逗号结尾 return score; } /** * 清理中文文本,移除 emoji、特殊符号和标点符号,并分割成多个中文片段 * @param {string} text - 原始文本 * @returns {string[]} 清理后的中文片段数组 */ function cleanChineseText(text) { if (!text) return []; // 第一步:移除所有 emoji 和特殊符号,但保留中文和空格 let cleanedText = text // 移除 emoji(包括各种 Unicode emoji 范围) .replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, ' ') // 移除常见的特殊符号和图标 .replace(/[✅❌⏭️🔍📂📁📄🌐📡📝✨🚀⚠️💡🎯📊🛠️]/g, ' ') // 移除其他常见符号 .replace(/[►▶️⭐🎉🔧📈📉💻🖥️📱⌚]/g, ' ') // 移除常见标点符号(但保留中文标点如“、”等) .replace(/[!@#$%^&*()_+\-=\[\]{}|;':",./<>?`~]/g, ' ') // 移除多余的空格 .replace(/\s+/g, ' ') // 去除首尾空格 .trim(); // 第二步:按空格分割成多个片段,只保留包含中文的片段 const segments = cleanedText.split(/\s+/).filter(segment => { // 检查片段是否包含中文字符 return segment && /[\u4e00-\u9fff]/.test(segment); }); return segments; } /** * 验证是否为有效的中文文本(用于翻译) * @param {string} text - 待检查的文本 * @returns {boolean} 是否为有效的中文文本 */ function isValidChineseText(text) { // 基本检查:必须包含中文 if (!containsChinese(text)) { return false; } // 过滤掉空字符串或只有空白字符的文本 if (!text.trim()) { return false; } // 过滤掉过长的文本(可能包含代码) if (text.length > 50) { return false; } // 过滤掉包含特殊代码字符的文本 const codePatterns = [ /[{}\[\]();]/, // 代码括号 /\\n|\\t/, // 转义字符 /^\s*\/\//, // 注释 /interface|class|function|const|let|var|export|import/i, // 关键字 /\w+\s*:\s*\w+/, // 对象属性定义 ]; for (const pattern of codePatterns) { if (pattern.test(text)) { return false; } } // 过滤掉纯数字或主要是数字的文本 if (/^[\d\s.,,。]+$/.test(text)) { return false; } // 过滤掉只包含标点符号的文本 if (/^[\s\p{P}]+$/u.test(text)) { return false; } return true; } /** * 加载 API 配置文件 * @param {string} sourcePath - 项目源目录路径 * @param {string} customConfigPath - 自定义配置文件路径(可选) */ async function loadApiConfig(sourcePath, customConfigPath) { const configs = []; try { // 1. 如果指定了自定义配置路径,优先加载 if (customConfigPath && await fs.pathExists(customConfigPath)) { const customConfig = await fs.readJson(customConfigPath); configs.push(customConfig); console.log(`📄 加载自定义配置: ${customConfigPath}`); } // 2. 查找项目目录下的 api-config.json const projectConfigPath = path.join(sourcePath, 'api-config.json'); if (await fs.pathExists(projectConfigPath)) { const projectConfig = await fs.readJson(projectConfigPath); configs.push(projectConfig); console.log(`📄 加载项目配置: ${projectConfigPath}`); } // 3. 查找工具目录下的 api-config.json const toolConfigPath = path.join(__dirname, '..', 'api-config.json'); if (await fs.pathExists(toolConfigPath)) { const toolConfig = await fs.readJson(toolConfigPath); configs.push(toolConfig); console.log(`📄 加载工具配置: ${toolConfigPath}`); } if (configs.length === 0) { console.warn('⚠️ 未找到任何 API 配置文件'); return; } // 4. 合并所有配置(后面的配置会覆盖前面的) const mergedConfig = Object.assign({}, ...configs); // 5. 设置环境变量 if (mergedConfig.baidu) { process.env.BAIDU_TRANSLATE_APP_ID = mergedConfig.baidu.appId; process.env.BAIDU_TRANSLATE_SECRET_KEY = mergedConfig.baidu.secretKey; } if (mergedConfig.youdao) { process.env.YOUDAO_TRANSLATE_APP_KEY = mergedConfig.youdao.appKey; process.env.YOUDAO_TRANSLATE_APP_SECRET = mergedConfig.youdao.appSecret; } if (mergedConfig.google) { process.env.GOOGLE_TRANSLATE_API_KEY = mergedConfig.google.apiKey; } console.log('✅ API 配置加载成功,已合并 ' + configs.length + ' 个配置文件'); } catch (error) { console.warn('⚠️ API 配置加载失败:', error.message); } } /** * 生成中英文映射 * @param {string[]} chineseTexts - 中文文本数组 * @param {string} translatorService - 翻译服务名称 * @returns {Promise<Object>} 中英文映射对象 */ async function generateMapping(chineseTexts, translatorService = 'baidu') { console.log('🌐 初始化翻译服务...'); const translationManager = new TranslationManager(); // 设置翻译服务 try { translationManager.setTranslator(translatorService); console.log(`📡 使用翻译服务: ${translatorService}`); } catch (error) { console.warn(`⚠️ 翻译服务设置失败: ${error.message},使用默认服务`); } // 尝试使用第三方翻译 API 进行批量翻译 try { console.log('📡 使用第三方翻译 API 进行批量翻译...'); const apiTranslations = await translationManager.batchTranslate(chineseTexts); const mapping = {}; for (const chineseText of chineseTexts) { const apiTranslation = apiTranslations[chineseText]; if (apiTranslation) { mapping[chineseText] = apiTranslation; console.log(`✅ API翻译: ${chineseText} -> ${apiTranslation}`); } else { // API 翻译失败,使用内置词典或占位符 const fallbackTranslation = await translateText(chineseText); mapping[chineseText] = fallbackTranslation; } } return mapping; } catch (error) { console.warn('⚠️ 第三方翻译 API 不可用,使用内置翻译方案:', error.message); // 如果第三方 API 不可用,回退到原有的翻译逻辑 const mapping = {}; for (const chineseText of chineseTexts) { const englishText = await translateText(chineseText); mapping[chineseText] = englishText; } return mapping; } } /** * 翻译文本 - 将中文翻译为英文 * @param {string} chineseText - 中文文本 * @returns {Promise<string>} 英文文本 */ async function translateText(chineseText) { // 首先尝试从内置词典翻译 const translation = getBuiltinTranslation(chineseText); if (translation) { return translation; } // 如果内置词典没有,尝试使用在线翻译 API try { const onlineTranslation = await translateWithAPI(chineseText); if (onlineTranslation) { return onlineTranslation; } } catch (error) { console.warn(`⚠️ 在线翻译失败: ${chineseText}`, error.message); } // 如果都失败了,返回格式化的占位符 const placeholder = generatePlaceholder(chineseText); console.log(`📝 使用占位符: ${chineseText} -> ${placeholder}`); return placeholder; } /** * 内置中英文词典 * @param {string} chineseText - 中文文本 * @returns {string|null} 英文翻译或 null */ function getBuiltinTranslation(chineseText) { const dictionary = { // 数据统计相关 '总次数': 'Total Count', '总和': 'Sum', '平均值': 'Average', '最大值': 'Maximum', '最小值': 'Minimum', // 订单相关 '订单数量': 'Order Count', '订单总商品数量': 'Total Product Count', '订单实付金额': 'Order Paid Amount', '订单总商品价格': 'Total Product Price', '订单商品数量': 'Order Product Count', '提交订单': 'Submit Order', '订单详情': 'Order Details', // 操作符相关 '等于': 'Equal', '不等于': 'Not Equal', '包含': 'Contains', '不包含': 'Not Contains', '有值': 'Has Value', '没值': 'No Value', '小于': 'Less Than', '大于': 'Greater Than', '小于等于': 'Less Than or Equal', '大于等于': 'Greater Than or Equal', '区间': 'Range', // 布尔值 '为真': 'True', '为假': 'False', // 商品相关 '优惠金额': 'Discount Amount', '商品价格': 'Product Price', '商品数量': 'Product Count', '实付金额': 'Paid Amount', // 性别相关 '性别': 'Gender', '男性': 'Male', '女性': 'Female', // 平台相关 '有赞': 'Youzan', '淘宝': 'Taobao', // 渠道相关 '注册渠道': 'Registration Channel', '好友类别': 'Friend Category', '用户行为事件': 'User Behavior Event', // 行为相关 '做过': 'Done', '未做过': 'Not Done', '已做过': 'Already Done', // 通用 '上传失败': 'Upload Failed', '删除': 'Delete', '编辑': 'Edit', '保存': 'Save', '取消': 'Cancel', '确认': 'Confirm', '提交': 'Submit', '重置': 'Reset', '搜索': 'Search', '查询': 'Query', '添加': 'Add', '新增': 'Add', '修改': 'Modify', '更新': 'Update', '刷新': 'Refresh', '加载': 'Load', '导入': 'Import', '导出': 'Export', '下载': 'Download', '上传': 'Upload', '复制': 'Copy', '粘贴': 'Paste', '剪切': 'Cut', '全选': 'Select All', '清空': 'Clear', '返回': 'Back', '下一步': 'Next', '上一步': 'Previous', '完成': 'Complete', '开始': 'Start', '结束': 'End', '暂停': 'Pause', '继续': 'Continue', '停止': 'Stop', '重新开始': 'Restart', '重试': 'Retry', '跳过': 'Skip', '忽略': 'Ignore', '关闭': 'Close', '打开': 'Open', '展开': 'Expand', '收起': 'Collapse', '显示': 'Show', '隐藏': 'Hide', '启用': 'Enable', '禁用': 'Disable', '激活': 'Activate', '停用': 'Deactivate', }; return dictionary[chineseText] || null; } /** * 使用在线翻译 API 翻译文本 * @param {string} chineseText - 中文文本 * @returns {Promise<string|null>} 英文翻译或 null */ async function translateWithAPI(chineseText) { // TODO: 这里可以集成各种翻译 API // 例如:Google Translate API, 百度翻译 API, 有道翻译 API 等 // 目前返回 null,表示不使用在线翻译 return null; } /** * 生成占位符 * @param {string} chineseText - 中文文本 * @returns {string} 占位符 */ function generatePlaceholder(chineseText) { // 将中文转换为拼音风格的占位符 const placeholder = chineseText .replace(/[\u4e00-\u9fff]/g, 'X') .replace(/\s+/g, '_') .toLowerCase(); return `translate_${placeholder}`; } module.exports = { execute };