vue-translate-auto
Version:
Vue 国际化自动翻译工具 - 自动提取 $t 包裹的字符串并生成多语言文件,支持增量翻译和多种翻译API
321 lines (275 loc) • 12.4 kB
JavaScript
const https = require('https');
const querystring = require('querystring');
const crypto = require('crypto');
class Translator {
constructor(config) {
this.config = config;
this.cache = new Map(); // 翻译缓存
}
// 翻译单个文本
async translateText(text, targetLang, sourceLang = 'auto') {
const cacheKey = `${text}_${sourceLang}_${targetLang}`;
// 检查缓存
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// 在线翻译
try {
let result;
switch (this.config.api.provider) {
case 'google':
result = await this.translateWithGoogle(text, targetLang, sourceLang);
break;
case 'baidu':
result = await this.translateWithBaidu(text, targetLang, sourceLang);
break;
case 'youdao':
result = await this.translateWithYoudao(text, targetLang, sourceLang);
break;
default:
// 使用免费的Google翻译API
result = await this.translateWithGoogle(text, targetLang, sourceLang);
}
// 缓存结果
this.cache.set(cacheKey, result);
return result;
} catch (error) {
// 不在这里打印错误,由上层重试逻辑处理
throw error; // 重新抛出错误,让调用方处理
}
}
// Google翻译(免费版本)
async translateWithGoogle(text, targetLang, sourceLang = 'auto') {
return new Promise((resolve, reject) => {
const url = 'https://translate.googleapis.com/translate_a/single';
const params = {
client: 'gtx',
sl: sourceLang,
tl: targetLang,
dt: 't',
q: text
};
const fullUrl = `${url}?${querystring.stringify(params)}`;
https.get(fullUrl, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const result = JSON.parse(data);
if (result && result[0] && result[0][0]) {
resolve(result[0][0][0]);
} else {
reject(new Error('翻译结果格式错误'));
}
} catch (error) {
reject(error);
}
});
}).on('error', reject);
});
}
// 百度翻译
async translateWithBaidu(text, targetLang, sourceLang = 'auto') {
// 需要百度翻译API密钥
if (!this.config.api.apiKey || !this.config.api.apiSecret) {
throw new Error('百度翻译需要API密钥');
}
console.log('暂不支持百度翻译')
// 这里实现百度翻译API调用
// 由于需要API密钥,暂时返回原文
return text;
}
// 有道翻译
async translateWithYoudao(text, targetLang, sourceLang = 'auto') {
// 需要有道翻译API密钥
if (!this.config.api.apiKey || !this.config.api.apiSecret) {
throw new Error('有道翻译需要API密钥');
}
return new Promise((resolve, reject) => {
try {
// 有道翻译API参数
const appKey = this.config.api.apiKey;
const appSecret = this.config.api.apiSecret;
const salt = Date.now().toString();
const curtime = Math.round(Date.now() / 1000).toString();
// 计算签名
const signStr = appKey + this.truncateQuery(text) + salt + curtime + appSecret;
const sign = crypto.createHash('sha256').update(signStr).digest('hex');
// 语言代码转换
const fromLang = this.convertToYoudaoLangCode(sourceLang);
const toLang = this.convertToYoudaoLangCode(targetLang);
// 请求参数
const params = {
q: text,
from: fromLang,
to: toLang,
appKey: appKey,
salt: salt,
sign: sign,
signType: 'v3',
curtime: curtime
};
const postData = querystring.stringify(params);
const options = {
hostname: 'openapi.youdao.com',
port: 443,
path: '/api',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
},
timeout: 10000
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const result = JSON.parse(data);
if (result.errorCode === '0' && result.translation && result.translation.length > 0) {
// 清理翻译结果中的特殊字符
let translation = result.translation[0];
// 移除开头的引号和特殊字符
translation = translation.replace(/^["'"']/, '').replace(/["'"']$/, '');
resolve(translation);
} else {
// 不在这里打印错误,由上层重试逻辑处理
reject(new Error(`有道翻译失败: ${result.errorCode}`));
}
} catch (error) {
reject(error);
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('有道翻译请求超时'));
});
req.write(postData);
req.end();
} catch (error) {
reject(error);
}
});
}
// 截断查询字符串(有道翻译要求)
truncateQuery(q) {
const len = q.length;
if (len <= 20) {
return q;
} else {
return q.substring(0, 10) + len + q.substring(len - 10, len);
}
}
// 转换为有道翻译的语言代码
convertToYoudaoLangCode(lang) {
return lang;
}
// 创建进度条
createProgressBar(current, total, barWidth = 30) {
const progress = current / total;
const filledWidth = Math.round(barWidth * progress);
const emptyWidth = barWidth - filledWidth;
const filledBar = '█'.repeat(filledWidth);
const emptyBar = '░'.repeat(emptyWidth);
const percentage = (progress * 100).toFixed(1);
return `[${filledBar}${emptyBar}] ${percentage}% (${current}/${total})`;
}
// 批量翻译(确保100%在线翻译成功)
async translateBatch(texts, targetLang, sourceLang = 'auto') {
const results = {};
const totalTexts = Object.keys(texts).length;
let currentIndex = 0;
let successCount = 0;
let totalRetries = 0;
const startTime = Date.now(); // 记录开始时间
console.log(`\n🚀 开始翻译到 ${targetLang} 语言`);
console.log(`📊 总计需要翻译: ${totalTexts} 个词汇`);
console.log('─'.repeat(60));
for (const [key, text] of Object.entries(texts)) {
currentIndex++;
// 更新进度条(在同一行实时更新)
const progressBar = this.createProgressBar(currentIndex, totalTexts);
process.stdout.write(`\r${progressBar} 正在翻译: ${text}`);
// 尝试翻译,失败时重试(增加重试次数和延迟)
let translatedText = text;
let currentRetryCount = 0;
const maxRetries = 8; // 增加重试次数到8次
while (currentRetryCount <= maxRetries) {
try {
translatedText = await this.translateText(text, targetLang, sourceLang);
if (translatedText !== text) {
successCount++;
}
break; // 成功则跳出循环
} catch (error) {
currentRetryCount++;
totalRetries++;
if (currentRetryCount > maxRetries) {
// 只有在重试次数用完后才打印错误信息
console.error(`\n❌ 翻译最终失败: ${text} -> ${targetLang}, 错误: ${error.message}`);
translatedText = text;
} else {
// 指数退避策略,延迟时间逐渐增加
const baseDelay = 3000; // 基础延迟3秒
const delayTime = Math.min(baseDelay * Math.pow(2, currentRetryCount - 1), 30000); // 最大30秒
// 在重试时更新进度条状态,不打印错误
process.stdout.write(`\r${progressBar} ⚠️ 重试中 (${currentRetryCount}/${maxRetries}): ${text}`);
await new Promise(resolve => setTimeout(resolve, delayTime));
// 重试后恢复正常显示
process.stdout.write(`\r${progressBar} 正在翻译: ${text}`);
}
}
}
results[key] = translatedText;
// 显示翻译完成状态(在同一行更新)
const status = translatedText !== text ? '✓ 完成' : '❌ 失败';
process.stdout.write(`\r${progressBar} ${status}: ${text}`);
// 增加延迟避免API限制
const delay = 500; // 0.5秒间隔
await new Promise(resolve => setTimeout(resolve, delay));
}
const endTime = Date.now(); // 记录结束时间
const totalTime = ((endTime - startTime) / 1000).toFixed(1); // 计算总时间
// 完成时换行
console.log('\n');
// 显示最终统计
console.log('─'.repeat(60));
console.log(`🎉 ${targetLang} 语言翻译完成!`);
console.log(`📊 最终统计:`);
console.log(` • 总词汇数: ${totalTexts}`);
console.log(` • 成功翻译: ${successCount}`);
console.log(` • 失败词汇: ${totalTexts - successCount}`);
console.log(` • 成功率: ${((successCount / totalTexts) * 100).toFixed(1)}%`);
console.log(` • 总重试次数: ${totalRetries}`);
console.log(` • 总耗时: ${totalTime}秒`);
console.log('─'.repeat(60));
return { results, totalTime }; // 返回结果和耗时
}
// 获取语言代码映射
getLanguageCode(lang) {
const langMap = {
'en': 'en',
'zh-CN': 'zh-CN',
'zh': 'zh-CN',
'ja': 'ja',
'japanese': 'ja',
'ko': 'ko',
'korean': 'ko',
'fr': 'fr',
'french': 'fr',
'de': 'de',
'german': 'de',
'es': 'es',
'spanish': 'es',
'ru': 'ru',
'russian': 'ru'
};
return langMap[lang] || lang;
}
}
module.exports = Translator;