@alauda-fe/i18n-tools
Version:
基于 Azure OpenAI 的 JSON i18n 文件翻译和英文语法检查工具集
454 lines (393 loc) • 15.7 kB
JavaScript
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 '未找到';
}
}