UNPKG

@alauda-fe/i18n-tools

Version:

基于 Azure OpenAI 的 JSON i18n 文件翻译和英文语法检查工具集

454 lines (393 loc) 15.7 kB
import fs from "node:fs/promises"; import path from "node:path"; import chalk from "chalk"; // ====================== 统一日志工具 ====================== export class Logger { constructor(name) { this.name = name; } // 格式化时间戳 timestamp() { return chalk.gray(`[${new Date().toLocaleTimeString()}]`); } // 成功消息 - 绿色 success(message) { console.log(`${chalk.green('✅')} ${chalk.green(message)}`); } // 信息消息 - 蓝色 info(message) { console.log(`${chalk.blue('ℹ️ ')} ${chalk.blue(message)}`); } // 警告消息 - 黄色 warn(message) { console.log(`${chalk.yellow('⚠️ ')} ${chalk.yellow(message)}`); } // 错误消息 - 红色 error(message) { console.log(`${chalk.red('❌')} ${chalk.red(message)}`); } // 进度消息 - 紫色 progress(message) { console.log(`${chalk.magenta('🔷')} ${chalk.magenta(message)}`); } // 处理结果 - 橙色 result(message) { console.log(`${chalk.hex('#FFA500')('🔶')} ${chalk.hex('#FFA500')(message)}`); } // 开始处理 - 青色 start(message) { console.log(`${chalk.cyan('🚀')} ${chalk.cyan(message)}`); } // 完成处理 - 绿色粗体 finish(message) { console.log(`${chalk.green('🎉')} ${chalk.green.bold(message)}`); } // 跳过处理 - 灰色 skip(message) { console.log(`${chalk.gray('⏩')} ${chalk.gray(message)}`); } // 下载相关 - 蓝色 download(message) { console.log(`${chalk.blue('⬇️ ')} ${chalk.blue(message)}`); } // 清理相关 - 黄色 clean(message) { console.log(`${chalk.yellow('🧹')} ${chalk.yellow(message)}`); } // 同步相关 - 紫色 sync(message) { console.log(`${chalk.magenta('🔄')} ${chalk.magenta(message)}`); } // 文件操作 file(action, filePath) { const fileName = path.basename(filePath); const colorMap = { 'read': { emoji: '📖', color: chalk.blue }, 'write': { emoji: '💾', color: chalk.green }, 'backup': { emoji: '🔄', color: chalk.yellow }, 'check': { emoji: '🔍', color: chalk.cyan }, 'clean': { emoji: '🧹', color: chalk.yellow } }; const config = colorMap[action] || { emoji: '📁', color: chalk.gray }; console.log(`${config.color(config.emoji)} ${config.color(`${action}: ${fileName}`)}`); } // 统计信息 - 带框格式 stats(title, stats) { console.log(`\n${chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`); console.log(`${chalk.cyan('�')} ${chalk.cyan.bold(title)}`); console.log(`${chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`); Object.entries(stats).forEach(([key, value]) => { // 根据不同的值类型使用不同颜色 let coloredValue = value; if (typeof value === 'number') { if (key.includes('失败') || key.includes('错误')) { coloredValue = value > 0 ? chalk.red(value) : chalk.green(value); } else if (key.includes('成功') || key.includes('完成')) { coloredValue = chalk.green(value); } else { coloredValue = chalk.cyan(value); } } else if (typeof value === 'string' && value.includes('%')) { const percentage = parseFloat(value); if (percentage >= 90) { coloredValue = chalk.green(value); } else if (percentage >= 70) { coloredValue = chalk.yellow(value); } else { coloredValue = chalk.red(value); } } else { coloredValue = chalk.white(value); } console.log(`${chalk.cyan('│')} ${chalk.white(key.padEnd(12))}: ${coloredValue}`); }); console.log(`${chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`); } // API 请求日志 apiRequest(batch, type = 'API') { const count = typeof batch === 'object' ? Object.keys(batch).length : batch; console.log(`${chalk.blue('🔷')} ${chalk.blue(`${type} 请求: ${count} 条目`)}`); } // API 响应日志(开发模式) apiResponse(content, verbose = false) { if (verbose && process.env.NODE_ENV === 'development') { console.log(`${chalk.hex('#FFA500')('🔶')} ${chalk.hex('#FFA500')('API 响应:')}`, content); } } // 仓库信息 - 带标题格式 repoHeader(repoKey, repoConfig, branch) { console.log(`\n${chalk.cyan('────────────────────────────────────────')}`); console.log(`${chalk.cyan('📦')} ${chalk.cyan.bold(`正在处理仓库: ${repoKey}`)}`); console.log(`${chalk.cyan('│')} ${chalk.gray(`URL: ${repoConfig.repository}`)}`); console.log(`${chalk.cyan('│')} ${chalk.gray(`分支: ${branch}`)}`); console.log(`${chalk.cyan('│')} ${chalk.gray(`文件数: ${repoConfig.files.length}`)}`); console.log(`${chalk.cyan('────────────────────────────────────────')}`); } // 仓库结果统计 repoResult(repoKey, success, total) { const successRate = ((success / total) * 100).toFixed(1); const status = success === total ? chalk.green('✅ 全部成功') : success > 0 ? chalk.yellow('⚠️ 部分成功') : chalk.red('❌ 全部失败'); console.log(`\n${status} ${chalk.bold(repoKey)}: ${chalk.cyan(success)}/${chalk.cyan(total)} 文件 (${successRate >= 100 ? chalk.green(successRate + '%') : chalk.yellow(successRate + '%')})`); } // 最终总结 summary(totalSuccess, totalFiles, outputDir) { const successRate = ((totalSuccess / totalFiles) * 100).toFixed(1); const isFullSuccess = totalSuccess === totalFiles; console.log(`\n${chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`); console.log(`${isFullSuccess ? chalk.green('🎉') : chalk.yellow('⚠️ ')} ${chalk.bold('同步完成统计')}`); console.log(`${chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`); console.log(`${chalk.cyan('│')} ${'总文件数'.padEnd(12)}: ${chalk.cyan(totalFiles)}`); console.log(`${chalk.cyan('│')} ${'成功下载'.padEnd(12)}: ${chalk.green(totalSuccess)}`); console.log(`${chalk.cyan('│')} ${'失败数量'.padEnd(12)}: ${totalFiles - totalSuccess > 0 ? chalk.red(totalFiles - totalSuccess) : chalk.green(0)}`); console.log(`${chalk.cyan('│')} ${'成功率'.padEnd(12)}: ${successRate >= 100 ? chalk.green(successRate + '%') : successRate >= 90 ? chalk.yellow(successRate + '%') : chalk.red(successRate + '%')}`); console.log(`${chalk.cyan('│')} ${'保存位置'.padEnd(12)}: ${chalk.gray(outputDir)}`); console.log(`${chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`); if (isFullSuccess) { console.log(`\n${chalk.green('🎊 所有文件同步成功!')}`); } else { console.log(`\n${chalk.yellow('⚠️ 部分文件同步失败,请检查上面的错误信息')}`); } } } // ====================== 参数验证工具 ====================== export class Validator { static async validateFile(filePath, required = true) { if (!filePath) { if (required) throw new Error('文件路径为必填参数'); return false; } const resolvedPath = path.resolve(filePath); try { await fs.access(resolvedPath); const stats = await fs.stat(resolvedPath); if (!stats.isFile()) { throw new Error(`路径不是文件: ${resolvedPath}`); } return resolvedPath; } catch (err) { throw new Error(`文件不存在或无法访问: ${resolvedPath}`); } } static async validateDirectory(dirPath, required = true) { if (!dirPath) { if (required) throw new Error('目录路径为必填参数'); return false; } const resolvedPath = path.resolve(dirPath); try { await fs.access(resolvedPath); const stats = await fs.stat(resolvedPath); if (!stats.isDirectory()) { throw new Error(`路径不是目录: ${resolvedPath}`); } return resolvedPath; } catch (err) { throw new Error(`目录不存在或无法访问: ${resolvedPath}`); } } static validateApiKey(token) { if (!token || typeof token !== 'string' || token.trim().length === 0) { throw new Error('Azure OpenAI API 密钥为必填参数'); } return token.trim(); } static validateLanguage(lang, paramName = '语言') { if (!lang || typeof lang !== 'string' || lang.trim().length === 0) { throw new Error(`${paramName}代码为必填参数`); } return lang.trim(); } static validateParallel(parallel) { const num = parseInt(parallel); if (isNaN(num) || num < 1 || num > 10) { throw new Error('并行数量必须是 1-10 之间的整数'); } return num; } static async validateJsonFile(filePath) { const resolvedPath = await this.validateFile(filePath); if (!resolvedPath.endsWith('.json')) { throw new Error('文件必须是 .json 格式'); } try { const content = await fs.readFile(resolvedPath, 'utf8'); JSON.parse(content); return resolvedPath; } catch (err) { throw new Error(`JSON 文件格式错误: ${err.message}`); } } } // ====================== 错误处理工具 ====================== export class ErrorHandler { constructor(logger, logPath) { this.logger = logger; this.logPath = logPath; } async logError(error, context = {}) { const logEntry = { timestamp: new Date().toISOString(), error: error.message, stack: error.stack, context, response: error.response?.data || null, }; try { // 确保日志文件目录存在 await fs.mkdir(path.dirname(this.logPath), { recursive: true }); await fs.appendFile(this.logPath, JSON.stringify(logEntry) + '\n'); } catch (logErr) { this.logger.error(`无法写入错误日志: ${logErr.message}`); } } async readErrors() { try { const content = await fs.readFile(this.logPath, 'utf8'); return content .trim() .split('\n') .filter(line => line.trim()) .map(line => JSON.parse(line)); } catch (err) { if (err.code === 'ENOENT') return []; throw err; } } async writeErrors(errors) { try { const content = errors.map(e => JSON.stringify(e)).join('\n') + '\n'; await fs.writeFile(this.logPath, content, 'utf8'); } catch (err) { this.logger.error(`无法更新错误日志: ${err.message}`); } } async clear() { try { await fs.writeFile(this.logPath, '', 'utf8'); } catch (err) { this.logger.error(`无法清空错误日志: ${err.message}`); } } handleApiError(error, retryCallback = null) { if (error.response?.status === 429) { const waitMs = parseInt(error.response.headers['retry-after'] || '60', 10) * 1000; this.logger.warn(`API 限速,等待 ${waitMs / 1000}s 后重试`); return { shouldRetry: true, waitMs, retryCallback }; } if (error.response?.status === 401) { this.logger.error('API 密钥无效,请检查您的 Azure OpenAI API 密钥'); return { shouldRetry: false }; } if (error.response?.status >= 500) { this.logger.error('服务器错误,请稍后重试'); return { shouldRetry: true, waitMs: 5000 }; } this.logger.error(`API 请求失败: ${error.message}`); return { shouldRetry: false }; } } // ====================== 进度追踪工具 ====================== export class ProgressTracker { constructor(logger, total) { this.logger = logger; this.total = total; this.current = 0; this.failed = 0; this.startTime = Date.now(); } update(success = true) { this.current++; if (!success) this.failed++; const progress = ((this.current / this.total) * 100).toFixed(1); const elapsed = Math.round((Date.now() - this.startTime) / 1000); const eta = this.current > 0 ? Math.round((elapsed / this.current) * (this.total - this.current)) : 0; this.logger.progress( `进度: ${this.current}/${this.total} (${progress}%) | ` + `已用时: ${elapsed}s | 预计剩余: ${eta}s` ); } finish() { const totalTime = Math.round((Date.now() - this.startTime) / 1000); this.logger.stats('处理完成统计', { '总条目数': this.total, '成功处理': this.current - this.failed, '处理失败': this.failed, '成功率': `${(((this.current - this.failed) / this.total) * 100).toFixed(1)}%`, '总耗时': `${totalTime}s` }); } } // ====================== 配置管理 ====================== export const CONFIG = { ENDPOINT: "https://apt-docs-openai.openai.azure.com", API_VERSION: "2025-03-01-preview", TRANSLATE_BATCH_SIZE: 50, GRAMMAR_BATCH_SIZE: 30, REQUEST_INTERVAL: 300, DOT_ESC: "__DOT__", // 日志文件路径 LOGS: { TRANSLATE: "translate_errors.log", GRAMMAR_CHECK: "grammar_check_errors.log" } }; // ====================== Token 环境变量处理工具 ====================== export class TokenResolver { /** * 解析 API token,优先使用命令行参数,然后检查环境变量 * @param {string|undefined} cmdToken 命令行传入的 token * @param {string} envVarName 环境变量名称,默认为 'OPENAI_API_KEY' * @returns {string} 解析到的 token * @throws {Error} 如果没有找到有效的 token */ static resolveToken(cmdToken, envVarName = 'OPENAI_API_KEY') { // 优先使用命令行参数 if (cmdToken && cmdToken.trim()) { return cmdToken.trim(); } // 检查环境变量 const envToken = process.env[envVarName]; if (envToken && envToken.trim()) { return envToken.trim(); } // 如果都没有找到,抛出错误 throw new Error( `未找到 API Token。请使用以下方式之一提供:\n` + `1. 命令行参数:--token <your_token>\n` + `2. 环境变量:export ${envVarName}=<your_token>` ); } /** * 检查是否存在 token(不抛出错误,用于警告提示) * @param {string|undefined} cmdToken 命令行传入的 token * @param {string} envVarName 环境变量名称,默认为 'OPENAI_API_KEY' * @returns {boolean} 是否存在有效的 token */ static hasToken(cmdToken, envVarName = 'OPENAI_API_KEY') { try { this.resolveToken(cmdToken, envVarName); return true; } catch { return false; } } /** * 获取 token 来源描述(用于日志) * @param {string|undefined} cmdToken 命令行传入的 token * @param {string} envVarName 环境变量名称,默认为 'OPENAI_API_KEY' * @returns {string} token 来源描述 */ static getTokenSource(cmdToken, envVarName = 'OPENAI_API_KEY') { if (cmdToken && cmdToken.trim()) { return '命令行参数'; } if (process.env[envVarName] && process.env[envVarName].trim()) { return `环境变量 ${envVarName}`; } return '未找到'; } }