UNPKG

pick-cn

Version:

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

349 lines (301 loc) 10.9 kB
const axios = require('axios'); const crypto = require('crypto'); const querystring = require('querystring'); /** * 翻译服务管理器 */ class TranslationManager { constructor() { this.translators = { baidu: new BaiduTranslator(), youdao: new YoudaoTranslator(), google: new GoogleTranslator() }; this.currentTranslator = 'baidu'; // 默认使用百度翻译 } /** * 设置当前使用的翻译服务 * @param {string} service - 翻译服务名称 (baidu, youdao, google) */ setTranslator(service) { if (this.translators[service]) { this.currentTranslator = service; } else { throw new Error(`不支持的翻译服务: ${service}`); } } /** * 翻译文本 * @param {string} text - 要翻译的中文文本 * @returns {Promise<string>} 翻译结果 */ async translate(text) { const translator = this.translators[this.currentTranslator]; return await translator.translate(text); } /** * 批量翻译文本 * @param {string[]} texts - 要翻译的中文文本数组 * @returns {Promise<Object>} 翻译结果映射 */ async batchTranslate(texts) { const results = {}; const translator = this.translators[this.currentTranslator]; // 如果是百度翻译,使用其原生批量翻译能力 if (this.currentTranslator === 'baidu' && translator.batchTranslate) { const batchSize = 20; // 百度API支持一次翻译20个文本 for (let i = 0; i < texts.length; i += batchSize) { const batch = texts.slice(i, i + batchSize); console.log(`📡 正在翻译第 ${Math.floor(i/batchSize) + 1} 批,共 ${Math.ceil(texts.length/batchSize)} 批(${batch.length} 个文本)`); try { const translations = await translator.batchTranslate(batch); // 合并结果 for (let j = 0; j < batch.length; j++) { const text = batch[j]; const translation = translations[j]; results[text] = translation; console.log(`✅ ${text} -> ${translation}`); } } catch (error) { console.warn(`⚠️ 批量翻译失败: ${error.message}`); // 批量翻译失败,回退到单个翻译 for (const text of batch) { try { const translation = await translator.translate(text); results[text] = translation; console.log(`✅ 单个翻译: ${text} -> ${translation}`); await new Promise(resolve => setTimeout(resolve, 500)); } catch (singleError) { console.warn(`⚠️ 单个翻译失败: ${text} - ${singleError.message}`); results[text] = null; await new Promise(resolve => setTimeout(resolve, 1000)); } } } // 批次之间的延迟 if (i + batchSize < texts.length) { console.log('🕰️ 等待 5 秒后继续下一批...'); await new Promise(resolve => setTimeout(resolve, 5000)); } } } else { // 其他翻译服务使用原有的串行处理逻辑 const batchSize = 3; for (let i = 0; i < texts.length; i += batchSize) { const batch = texts.slice(i, i + batchSize); console.log(`📡 正在翻译第 ${Math.floor(i/batchSize) + 1} 批,共 ${Math.ceil(texts.length/batchSize)} 批(${batch.length} 个文本)`); for (const text of batch) { try { const translation = await this.translate(text); results[text] = translation; console.log(`✅ ${text} -> ${translation}`); await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { console.warn(`⚠️ 翻译失败: ${text} - ${error.message}`); results[text] = null; await new Promise(resolve => setTimeout(resolve, 1000)); } } if (i + batchSize < texts.length) { console.log('🕰️ 等待 3 秒后继续下一批...'); await new Promise(resolve => setTimeout(resolve, 3000)); } } } return results; } } /** * 百度翻译 API */ class BaiduTranslator { constructor() { this.appId = process.env.BAIDU_TRANSLATE_APP_ID; this.secretKey = process.env.BAIDU_TRANSLATE_SECRET_KEY; this.apiUrl = 'https://fanyi-api.baidu.com/api/trans/vip/translate'; } /** * 翻译单个文本 * @param {string} text - 中文文本 * @returns {Promise<string>} 英文翻译 */ async translate(text) { const results = await this.batchTranslate([text]); return results[0]; } /** * 批量翻译文本(百度 API 原生支持) * @param {string[]} texts - 中文文本数组(最多20个) * @param {number} retryCount - 重试次数 * @returns {Promise<string[]>} 英文翻译数组 */ async batchTranslate(texts, retryCount = 0) { if (!this.appId || !this.secretKey) { throw new Error('百度翻译API配置缺失,请设置 BAIDU_TRANSLATE_APP_ID 和 BAIDU_TRANSLATE_SECRET_KEY 环境变量'); } if (texts.length > 20) { throw new Error('百度翻译API单次最多支持 20 个文本'); } // 使用换行符分隔多个文本 const query = texts.join('\n'); const salt = Date.now(); const sign = this.generateSign(query, salt); const params = { q: query, from: 'zh', to: 'en', appid: this.appId, salt: salt, sign: sign }; try { const response = await axios.get(this.apiUrl, { params }); if (response.data.error_code) { const errorMsg = response.data.error_msg; // 如果是限流错误且还有重试次数,则重试 if (errorMsg.includes('Invalid Access Limit') && retryCount < 3) { const waitTime = (retryCount + 1) * 5000; // 递增等待时间:5s, 10s, 15s console.log(`⏳ API限流,${waitTime/1000}秒后重试第${retryCount + 1}次...`); await new Promise(resolve => setTimeout(resolve, waitTime)); return await this.batchTranslate(texts, retryCount + 1); } throw new Error(`百度翻译API错误: ${errorMsg}`); } if (response.data.trans_result && response.data.trans_result.length > 0) { // 返回的结果数量应该与输入文本数量一致 return response.data.trans_result.map(result => result.dst); } throw new Error('百度翻译API返回结果为空'); } catch (error) { // 网络错误也可以重试 if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') { if (retryCount < 2) { const waitTime = (retryCount + 1) * 3000; console.log(`🔄 网络错误,${waitTime/1000}秒后重试...`); await new Promise(resolve => setTimeout(resolve, waitTime)); return await this.batchTranslate(texts, retryCount + 1); } } throw new Error(`百度翻译请求失败: ${error.message}`); } } /** * 生成百度翻译API签名 * @param {string} query - 查询文本 * @param {number} salt - 随机数 * @returns {string} 签名 */ generateSign(query, salt) { const str = this.appId + query + salt + this.secretKey; return crypto.createHash('md5').update(str).digest('hex'); } } /** * 有道翻译 API */ class YoudaoTranslator { constructor() { this.appKey = process.env.YOUDAO_TRANSLATE_APP_KEY; this.appSecret = process.env.YOUDAO_TRANSLATE_APP_SECRET; this.apiUrl = 'https://openapi.youdao.com/api'; } /** * 翻译文本 * @param {string} text - 中文文本 * @returns {Promise<string>} 英文翻译 */ async translate(text) { if (!this.appKey || !this.appSecret) { throw new Error('有道翻译API配置缺失,请设置 YOUDAO_TRANSLATE_APP_KEY 和 YOUDAO_TRANSLATE_APP_SECRET 环境变量'); } const salt = Date.now(); const curtime = Math.round(Date.now() / 1000); const sign = this.generateSign(text, salt, curtime); const params = { q: text, from: 'zh-CHS', to: 'en', appKey: this.appKey, salt: salt, sign: sign, signType: 'v3', curtime: curtime }; try { const response = await axios.post(this.apiUrl, querystring.stringify(params), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (response.data.errorCode !== '0') { throw new Error(`有道翻译API错误: ${response.data.errorCode}`); } if (response.data.translation && response.data.translation.length > 0) { return response.data.translation[0]; } throw new Error('有道翻译API返回结果为空'); } catch (error) { throw new Error(`有道翻译请求失败: ${error.message}`); } } /** * 生成有道翻译API签名 * @param {string} query - 查询文本 * @param {number} salt - 随机数 * @param {number} curtime - 当前时间戳 * @returns {string} 签名 */ generateSign(query, salt, curtime) { const input = query.length <= 20 ? query : query.substring(0, 10) + query.length + query.substring(query.length - 10); const str = this.appKey + input + salt + curtime + this.appSecret; return crypto.createHash('sha256').update(str).digest('hex'); } } /** * Google 翻译 API */ class GoogleTranslator { constructor() { this.apiKey = process.env.GOOGLE_TRANSLATE_API_KEY; this.apiUrl = 'https://translation.googleapis.com/language/translate/v2'; } /** * 翻译文本 * @param {string} text - 中文文本 * @returns {Promise<string>} 英文翻译 */ async translate(text) { if (!this.apiKey) { throw new Error('Google翻译API配置缺失,请设置 GOOGLE_TRANSLATE_API_KEY 环境变量'); } const params = { key: this.apiKey, q: text, source: 'zh', target: 'en', format: 'text' }; try { const response = await axios.post(this.apiUrl, querystring.stringify(params), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (response.data.error) { throw new Error(`Google翻译API错误: ${response.data.error.message}`); } if (response.data.data && response.data.data.translations && response.data.data.translations.length > 0) { return response.data.data.translations[0].translatedText; } throw new Error('Google翻译API返回结果为空'); } catch (error) { throw new Error(`Google翻译请求失败: ${error.message}`); } } } module.exports = { TranslationManager, BaiduTranslator, YoudaoTranslator, GoogleTranslator };