UNPKG

i18n-xy

Version:

自动提取React项目中的中文字符串并进行国际化的CLI工具

1 lines 211 kB
{"version":3,"sources":["../src/config/default.config.ts","../src/utils/fs.ts","../src/utils/logger.ts","../src/config/index.ts","../src/translation/providers/baidu.ts","../src/translation/queue.ts","../src/translation/cache.ts","../src/translation/manager.ts","../src/translation/cli.ts","../src/cli.ts","../src/ast/index.ts","../src/gen-key-value.ts","../src/utils/import-manager.ts","../src/utils/pattern.ts","../package.json","../src/utils/config-validator.ts"],"sourcesContent":["import { I18nConfig } from './type';\n\n// 默认配置\nexport const defaultConfig: I18nConfig = {\n // 基础语言配置\n locale: 'zh-CN',\n fallbackLocale: 'en-US',\n outputDir: 'locales',\n // tempDir: undefined, // 可选,不设置则直接修改源文件\n\n // 文件处理配置\n include: [\n 'src/**/*.{js,jsx,ts,tsx}',\n 'pages/**/*.{js,jsx,ts,tsx}',\n 'components/**/*.{js,jsx,ts,tsx}',\n ],\n\n exclude: [\n 'node_modules/**',\n 'dist/**',\n 'build/**',\n '**/*.test.{js,jsx,ts,tsx}',\n '**/*.spec.{js,jsx,ts,tsx}',\n ],\n\n // Key 生成配置\n keyGeneration: {\n maxChineseLength: 10, // 最大汉字长度\n hashLength: 6, // 哈希长度\n maxRetryCount: 5, // 最大重试次数\n reuseExistingKey: true, // 默认重复使用相同文案的key\n duplicateKeySuffix: 'hash', // 重复key后缀模式,默认hash\n keyPrefix: '', // key前缀,默认为空\n separator: '_', // 连接符,默认下划线\n pinyinOptions: {\n // 拼音转换选项\n toneType: 'none', // 不带声调\n type: 'array', // 返回数组格式\n },\n },\n\n // 输出配置\n output: {\n prettyJson: true, // 格式化 JSON 输出\n localeFileName: '{locale}.json', // 语言文件名格式\n },\n\n // 日志配置\n logging: {\n enabled: true, // 默认启用日志\n level: 'normal', // 默认正常级别\n },\n\n // 替换配置\n replacement: {\n functionName: '$t', // 默认替换函数名\n quoteType: 'single', // 默认使用单引号\n autoImport: {\n enabled: false, // 默认不启用自动引入\n insertPosition: 'afterImports', // 默认在import语句后插入\n imports: {},\n },\n },\n\n // 翻译配置\n translation: {\n enabled: false, // 默认不启用翻译功能\n provider: 'baidu', // 默认使用百度翻译\n defaultSourceLang: 'zh', // 默认源语言:中文\n defaultTargetLang: 'en', // 默认目标语言:英文\n concurrency: 5, // 默认并发数:5 (降低并发数避免API限制)\n retryTimes: 3, // 默认重试次数:3次(不包括第一次)\n retryDelay: 1000, // 默认重试延迟:1秒\n batchDelay: 500, // 默认批次延迟:0.5秒\n baidu: {\n appid: '', // 需要用户配置\n key: '', // 需要用户配置\n },\n },\n};\n","import * as fs from 'fs-extra';\nimport * as path from 'path';\nimport fg, { Options } from 'fast-glob';\n\n/**\n * 读取文件内容\n */\nexport function readFile(filePath: string, encoding: BufferEncoding = 'utf-8'): Promise<string> {\n const absPath = path.resolve(process.cwd(), filePath);\n return fs.readFile(absPath, encoding);\n}\n\n/**\n * 写入文件内容\n */\nexport function writeFile(filePath: string, data: string | Buffer): Promise<void> {\n const absPath = path.resolve(process.cwd(), filePath);\n return fs.outputFile(absPath, data);\n}\n\n/**\n * 写入文件内容,支持 tempDir 配置\n * @param filePath 原始文件路径\n * @param data 文件内容\n * @param tempDir 临时目录,如果配置则写入到临时目录,否则写入原始位置\n */\nexport function writeFileWithTempDir(\n filePath: string,\n data: string | Buffer,\n tempDir?: string\n): Promise<void> {\n let targetPath: string;\n\n if (tempDir) {\n // 如果配置了 tempDir,则在 tempDir 下按照原始路径结构生成文件\n const relativePath = path.relative(process.cwd(), filePath);\n targetPath = path.resolve(process.cwd(), tempDir, relativePath);\n } else {\n // 如果没有配置 tempDir,则直接写入原始文件\n targetPath = path.resolve(process.cwd(), filePath);\n }\n\n return fs.outputFile(targetPath, data);\n}\n\n/**\n * 创建目录\n */\nexport function ensureDir(dirPath: string): Promise<void> {\n const absPath = path.resolve(process.cwd(), dirPath);\n return fs.ensureDir(absPath);\n}\n\n/**\n * 遍历目录下所有文件,支持glob模式和过滤参数\n */\nexport function findFiles(pattern: string | string[], options: Options = {}): Promise<string[]> {\n return fg(pattern, { dot: true, ...options, cwd: process.cwd() });\n}\n\n/**\n * 根据 include/exclude 参数筛选目标文件,所有路径以 CLI 执行目录为根目录\n * @param include 需要包含的文件模式(glob数组或字符串)\n * @param exclude 需要排除的文件模式(glob数组或字符串)\n */\nexport function findTargetFiles(include: string[], exclude: string[] = []): Promise<string[]> {\n // // 处理排除模式,使其能够匹配任意层级的目录\n // const processedExclude = exclude.map((pattern) => {\n // // 如果模式已经以 **/ 开头,则不处理\n // if (pattern.startsWith('**/')) {\n // return pattern;\n // }\n\n // // 处理带有 /** 后缀的模式(如 node_modules/**)\n // // 将其转换为 **/node_modules/** 以匹配任意层级\n // if (pattern.includes('/') && pattern.endsWith('/**')) {\n // const dirName = pattern.slice(0, -3); // 去掉 '/**'\n // return `**/${dirName}/**`;\n // }\n\n // // 如果模式包含完整路径(包含 /),则不处理\n // if (pattern.includes('/')) {\n // return pattern;\n // }\n\n // // 为不带路径分隔符的模式添加 **/ 前缀,使其可以匹配任意层级的目录\n // return `**/${pattern}`;\n // });\n\n return fg(include, { ignore: exclude, dot: true, cwd: process.cwd() });\n}\n\n/**\n * 检查文件是否存在\n * @param filePath 文件路径\n */\nexport function fileExists(filePath: string): boolean {\n const absPath = path.resolve(process.cwd(), filePath);\n return fs.existsSync(absPath);\n}\n\n/**\n * 同步读取 JSON 文件内容\n * @param filePath JSON 文件路径\n */\nexport function readJsonSync(filePath: string): unknown {\n const absPath = path.resolve(process.cwd(), filePath);\n return fs.readJsonSync(absPath);\n}\n\n/**\n * 异步读取 JSON 文件内容\n * @param filePath JSON 文件路径\n * @param defaultValue 文件不存在时的默认值\n */\nexport async function readJson<T = unknown>(filePath: string, defaultValue?: T): Promise<T> {\n try {\n const content = await readFile(filePath, 'utf-8');\n return JSON.parse(content) as T;\n } catch (error) {\n if (defaultValue !== undefined) {\n return defaultValue;\n }\n throw error;\n }\n}\n\n/**\n * 异步写入 JSON 文件内容\n * @param filePath JSON 文件路径\n * @param data 要写入的数据\n * @param pretty 是否格式化输出,默认为 true\n */\nexport function writeJson(filePath: string, data: unknown, pretty: boolean = true): Promise<void> {\n const content = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);\n return writeFile(filePath, content);\n}\n","import { ConfigManager } from '../config';\n\nexport class Logger {\n private static getLogLevel(): string {\n try {\n const config = ConfigManager.get();\n return config.logging?.level ?? 'normal';\n } catch {\n // 如果ConfigManager未初始化,使用默认值\n return 'normal';\n }\n }\n\n private static shouldLog(level: 'minimal' | 'normal' | 'verbose'): boolean {\n const currentLevel = this.getLogLevel();\n\n if (currentLevel === 'minimal') {\n return level === 'minimal';\n } else if (currentLevel === 'normal') {\n return level !== 'verbose';\n } else if (currentLevel === 'verbose') {\n return true;\n }\n\n return false;\n }\n\n static success(message: string, level: 'minimal' | 'normal' | 'verbose' = 'normal'): void {\n if (this.shouldLog(level)) {\n console.log(`✅ ${message}`);\n }\n }\n\n static info(message: string, level: 'minimal' | 'normal' | 'verbose' = 'normal'): void {\n if (this.shouldLog(level)) {\n console.log(`ℹ️ ${message}`);\n }\n }\n\n static warn(message: string, level: 'minimal' | 'normal' | 'verbose' = 'normal'): void {\n if (this.shouldLog(level)) {\n console.warn(`⚠️ ${message}`);\n }\n }\n\n static error(message: string, level: 'minimal' | 'normal' | 'verbose' = 'normal'): void {\n if (this.shouldLog(level)) {\n console.error(`❌ ${message}`);\n }\n }\n\n static verbose(message: string): void {\n this.info(message, 'verbose');\n }\n}\n","import { merge } from 'lodash';\nimport { I18nConfig } from './type';\nimport { defaultConfig } from './default.config';\nimport { fileExists, readJsonSync } from '../utils/fs';\nimport { Logger } from '../utils/logger';\n\n/**\n * 验证配置对象的有效性\n */\nfunction validateConfig(config: I18nConfig): void {\n const errors: string[] = [];\n\n // 验证必需字段\n if (!config.include || !Array.isArray(config.include) || config.include.length === 0) {\n errors.push('include 字段必须是非空数组');\n }\n\n if (!config.outputDir || typeof config.outputDir !== 'string') {\n errors.push('outputDir 字段必须是有效的字符串路径');\n }\n\n if (!config.locale || typeof config.locale !== 'string') {\n errors.push('locale 字段必须是有效的语言代码');\n }\n\n // 验证翻译配置\n if (config.translation?.enabled) {\n if (!config.translation.provider) {\n errors.push('启用翻译时必须指定 translation.provider');\n }\n\n if (config.translation.provider === 'baidu') {\n if (!config.translation.baidu?.appid || !config.translation.baidu?.key) {\n errors.push('使用百度翻译时必须配置 translation.baidu.appid 和 translation.baidu.key');\n }\n }\n }\n\n // 验证日志级别\n if (config.logging?.level && !['minimal', 'normal', 'verbose'].includes(config.logging.level)) {\n errors.push('logging.level 必须是 minimal、normal 或 verbose 之一');\n }\n\n if (errors.length > 0) {\n Logger.error('配置验证失败:', 'minimal');\n errors.forEach(error => Logger.error(` - ${error}`, 'minimal'));\n throw new Error(`配置验证失败: ${errors.join('; ')}`);\n }\n}\n\nexport function loadConfig(customConfigPath?: string): I18nConfig {\n let userConfig: Partial<I18nConfig> = {};\n\n if (customConfigPath) {\n if (fileExists(customConfigPath)) {\n Logger.verbose(`加载配置文件: ${customConfigPath}`);\n try {\n userConfig = readJsonSync(customConfigPath) as Partial<I18nConfig>;\n Logger.verbose('配置文件加载成功');\n } catch (error) {\n throw new Error(`配置文件解析失败: ${error}`);\n }\n } else {\n Logger.warn(`配置文件不存在: ${customConfigPath},使用默认配置`);\n }\n }\n\n const finalConfig = merge({}, defaultConfig, userConfig) as I18nConfig;\n\n // 验证最终配置\n validateConfig(finalConfig);\n\n Logger.verbose('配置验证通过');\n return finalConfig;\n}\n\n// ConfigManager 单例\nclass ConfigManagerClass {\n private config: I18nConfig | null = null;\n\n init(config: I18nConfig): void {\n this.config = config;\n }\n\n get(): I18nConfig {\n if (!this.config) {\n throw new Error('ConfigManager: config not initialized!');\n }\n return this.config;\n }\n}\n\nexport const ConfigManager = new ConfigManagerClass();\n","import { TranslationProvider, TranslationResult } from '../index';\nimport { Logger } from '../../utils/logger';\nimport baiduTranslateService from 'baidu-translate-service';\n\ninterface BaiduTranslateConfig {\n appid: string;\n key: string;\n}\n\ninterface BaiduTranslateResponse {\n from: string;\n to: string;\n trans_result: Array<{\n src: string;\n dst: string;\n }>;\n error_code?: string;\n error_msg?: string;\n}\n\nexport class BaiduTranslationProvider implements TranslationProvider {\n public readonly name = 'baidu';\n private config: BaiduTranslateConfig;\n\n constructor(config: BaiduTranslateConfig) {\n this.config = config;\n Logger.verbose(`百度翻译提供者初始化完成,appid: ${config.appid ? '已配置' : '未配置'}`);\n }\n\n async translate(\n text: string,\n from: string = 'auto',\n to: string = 'en'\n ): Promise<TranslationResult> {\n if (!this.isConfigured()) {\n throw new Error('百度翻译服务未配置,请设置 appid 和 key');\n }\n\n try {\n const mappedFrom = this.mapLanguageCode(from);\n const mappedTo = this.mapLanguageCode(to);\n Logger.verbose(`发起百度翻译请求: ${mappedFrom} -> ${mappedTo}, 文本长度: ${text.length}`);\n\n const response: BaiduTranslateResponse = await baiduTranslateService({\n appid: this.config.appid,\n key: this.config.key,\n q: text,\n from: mappedFrom,\n to: mappedTo,\n });\n\n if (response.error_code) {\n throw new Error(`百度翻译API错误: ${response.error_code} - ${response.error_msg}`);\n }\n\n if (!response.trans_result || response.trans_result.length === 0) {\n throw new Error('翻译结果为空');\n }\n\n const translatedText = response.trans_result.map((item) => item.dst).join('\\n');\n\n Logger.verbose(`翻译成功: \"${text}\" -> \"${translatedText}\"`);\n\n return {\n originalText: text,\n translatedText,\n sourceLanguage: response.from,\n targetLanguage: response.to,\n provider: this.name,\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n Logger.error(`百度翻译失败: ${errorMessage}`);\n throw error;\n }\n }\n\n isConfigured(): boolean {\n return !!(this.config.appid && this.config.key);\n }\n\n getSupportedLanguages(): string[] {\n return [\n 'auto',\n 'zh',\n 'en',\n 'yue',\n 'wyw',\n 'jp',\n 'kor',\n 'fra',\n 'spa',\n 'th',\n 'ara',\n 'ru',\n 'pt',\n 'de',\n 'it',\n 'el',\n 'nl',\n 'pl',\n 'bul',\n 'est',\n 'dan',\n 'fin',\n 'cs',\n 'rom',\n 'slo',\n 'swe',\n 'hu',\n 'cht',\n 'vie',\n ];\n }\n\n /**\n * 映射语言代码到百度翻译API支持的格式\n */\n private mapLanguageCode(lang: string): string {\n const langMap: Record<string, string> = {\n 'zh-CN': 'zh',\n 'zh-TW': 'cht',\n 'en-US': 'en',\n ja: 'jp',\n ko: 'kor',\n fr: 'fra',\n es: 'spa',\n ar: 'ara',\n ru: 'ru',\n pt: 'pt',\n de: 'de',\n it: 'it',\n nl: 'nl',\n pl: 'pl',\n sv: 'swe',\n vi: 'vie',\n };\n\n return langMap[lang] ?? lang;\n }\n}\n","import { Logger } from '../utils/logger';\n\nexport interface QueueTask<T> {\n id: string;\n execute: () => Promise<T>;\n maxRetries: number;\n retryDelay: number;\n currentRetry?: number;\n}\n\nexport interface QueueStats {\n total: number;\n pending: number;\n completed: number;\n failed: number;\n inProgress: number;\n}\n\n/**\n * 翻译队列,用于控制并发和重试逻辑\n */\nexport class TranslationQueue {\n private readonly concurrency: number;\n private tasks: QueueTask<unknown>[] = [];\n private completedResults: Map<string, unknown> = new Map();\n private failedTasks: Map<string, Error> = new Map();\n private stats: QueueStats = {\n total: 0,\n pending: 0,\n completed: 0,\n failed: 0,\n inProgress: 0,\n };\n private batchDelay: number = 0; // 批次间延迟(毫秒)\n private isApiLimited: boolean = false; // API限制状态标志\n private apiLimitResetTime: number = 0; // API限制重置时间\n\n constructor(concurrency: number = 10) {\n this.concurrency = concurrency;\n }\n\n /**\n * 添加任务到队列\n */\n addTask<T>(task: QueueTask<T>): void {\n task.currentRetry = 0;\n this.tasks.push(task as QueueTask<unknown>);\n this.stats.total++;\n this.stats.pending++;\n }\n\n /**\n * 设置批次间延迟时间(毫秒)\n */\n setBatchDelay(delay: number): void {\n this.batchDelay = delay;\n }\n\n /**\n * 执行所有任务,返回结果映射\n */\n async executeAll(): Promise<Map<string, unknown>> {\n Logger.info(`开始执行翻译队列,并发数: ${this.concurrency},任务总数: ${this.stats.total}`);\n\n this.completedResults.clear();\n this.failedTasks.clear();\n\n const workers: Promise<void>[] = [];\n\n // 启动固定数量的worker\n for (let i = 0; i < Math.min(this.concurrency, this.tasks.length); i++) {\n workers.push(this.worker());\n }\n\n // 等待所有worker完成\n await Promise.all(workers);\n\n return this.completedResults;\n }\n\n /**\n * 工作线程,负责执行任务队列中的任务\n */\n private async worker(): Promise<void> {\n while (this.tasks.length > 0) {\n // 检查API限制状态\n if (this.isApiLimited) {\n const now = Date.now();\n if (now < this.apiLimitResetTime) {\n // API处于限制状态,等待重置\n const waitTime = this.apiLimitResetTime - now;\n Logger.warn(`API速率限制中,等待 ${Math.ceil(waitTime / 1000)} 秒后继续...`);\n await this.delay(waitTime);\n this.isApiLimited = false;\n }\n }\n\n const task = this.tasks.shift();\n if (!task) continue;\n\n this.stats.pending--;\n this.stats.inProgress++;\n\n try {\n // 执行任务\n const result = await task.execute();\n\n // 记录成功结果\n this.completedResults.set(task.id, result);\n this.stats.completed++;\n this.stats.inProgress--;\n\n Logger.verbose(`任务 ${task.id} 执行成功`);\n\n // 添加批次间延迟,避免API限制\n if (this.batchDelay > 0) {\n await this.delay(this.batchDelay);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // 检测API限制错误\n if (this.isApiLimitError(errorMessage)) {\n this.handleApiLimitError();\n\n // 放回队列头部,等待API限制解除后重试\n this.tasks.unshift(task);\n this.stats.pending++;\n } else {\n Logger.error(`任务 ${task.id} 执行失败: ${errorMessage}`);\n\n // 检查是否还可以重试\n if ((task.currentRetry ?? 0) < task.maxRetries) {\n task.currentRetry = (task.currentRetry ?? 0) + 1;\n Logger.warn(\n `任务 ${task.id} 执行失败,准备第 ${task.currentRetry} 次重试: ${errorMessage}`\n );\n\n // 等待重试延迟时间\n await this.delay(task.retryDelay);\n\n // 放回队列末尾,等待重试\n this.tasks.push(task);\n this.stats.pending++;\n } else {\n // 重试次数用尽,记录最终失败\n const finalError = error instanceof Error ? error : new Error(String(error));\n this.failedTasks.set(task.id, finalError);\n this.stats.failed++;\n }\n }\n\n this.stats.inProgress--;\n }\n }\n }\n\n /**\n * 检测是否为API限制错误\n */\n private isApiLimitError(errorMessage: string): boolean {\n // 根据错误消息判断是否为API限制错误\n return (\n errorMessage.includes('API错误: 54003') ||\n errorMessage.includes('Invalid Access Limit') ||\n errorMessage.includes('rate limit') ||\n errorMessage.includes('too many requests')\n );\n }\n\n /**\n * 处理API限制错误\n */\n private handleApiLimitError(): void {\n // API限制,设置标志并计算恢复时间\n this.isApiLimited = true;\n // 设置60秒的API限制冷却时间\n const cooldownTime = 60 * 1000;\n this.apiLimitResetTime = Date.now() + cooldownTime;\n\n Logger.warn(`检测到API速率限制,将暂停 ${cooldownTime / 1000} 秒后继续`);\n }\n\n /**\n * 获取已完成任务的结果\n */\n getCompletedResults(): Map<string, unknown> {\n return this.completedResults;\n }\n\n /**\n * 获取失败任务的错误信息\n */\n getFailedTasks(): Map<string, Error> {\n return this.failedTasks;\n }\n\n /**\n * 获取队列统计信息\n */\n getStats(): QueueStats {\n return { ...this.stats };\n }\n\n /**\n * Promise延迟等待\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","import { Logger } from '../utils/logger';\n\nexport interface TranslationCacheItem {\n originalText: string;\n translatedText: string;\n sourceLanguage: string;\n targetLanguage: string;\n provider: string;\n timestamp: number;\n}\n\n/**\n * 翻译缓存管理器\n * 简单的内存缓存,用于存储和检索已翻译过的文本,避免重复调用翻译API\n */\nexport class TranslationCache {\n private static instance: TranslationCache;\n private cache: Map<string, TranslationCacheItem> = new Map();\n\n private constructor() {\n // 单例模式,私有构造函数\n }\n\n /**\n * 获取翻译缓存管理器实例\n */\n public static getInstance(): TranslationCache {\n if (!TranslationCache.instance) {\n TranslationCache.instance = new TranslationCache();\n Logger.verbose('初始化翻译内存缓存');\n }\n return TranslationCache.instance;\n }\n\n /**\n * 从缓存中获取翻译结果\n */\n public get(\n text: string,\n from: string,\n to: string,\n provider: string\n ): TranslationCacheItem | null {\n const key = this.generateCacheKey(text, from, to, provider);\n const cached = this.cache.get(key);\n\n if (cached) {\n Logger.verbose(`缓存命中: \"${text}\" (${from} -> ${to})`);\n return cached;\n }\n\n return null;\n }\n\n /**\n * 将翻译结果存入缓存\n */\n public set(\n text: string,\n translatedText: string,\n from: string,\n to: string,\n provider: string\n ): void {\n const key = this.generateCacheKey(text, from, to, provider);\n\n this.cache.set(key, {\n originalText: text,\n translatedText,\n sourceLanguage: from,\n targetLanguage: to,\n provider,\n timestamp: Date.now(),\n });\n }\n\n /**\n * 清空缓存\n */\n public clear(): void {\n this.cache.clear();\n Logger.verbose('翻译缓存已清空');\n }\n\n /**\n * 获取缓存大小\n */\n public size(): number {\n return this.cache.size;\n }\n\n /**\n * 生成缓存键\n */\n private generateCacheKey(text: string, from: string, to: string, provider: string): string {\n // 使用源文本、源语言、目标语言和提供者生成唯一键\n return `${provider}:${from}:${to}:${text}`;\n }\n}\n","import { TranslationProvider, TranslationResult, TranslationConfig } from './index';\nimport { BaiduTranslationProvider } from './providers/baidu';\nimport { Logger } from '../utils/logger';\nimport { TranslationQueue } from './queue';\nimport { readJson, writeJson, fileExists } from '../utils/fs';\nimport { TranslationCache } from './cache';\nimport * as path from 'path';\n\nexport class TranslationManager {\n private providers: Map<string, TranslationProvider> = new Map();\n private config: TranslationConfig;\n private cache: TranslationCache;\n\n constructor(config: TranslationConfig) {\n this.config = config;\n Logger.verbose(`初始化翻译管理器,提供者: ${config.provider},并发数: ${config.concurrency}`);\n\n // 初始化缓存\n this.cache = TranslationCache.getInstance();\n\n this.initializeProviders();\n }\n\n private initializeProviders(): void {\n // 初始化百度翻译服务\n if (this.config.baidu?.appid && this.config.baidu?.key) {\n const baiduProvider = new BaiduTranslationProvider({\n appid: this.config.baidu.appid,\n key: this.config.baidu.key,\n });\n this.providers.set('baidu', baiduProvider);\n Logger.verbose('百度翻译服务已初始化');\n }\n\n // TODO: 可以在这里添加其他翻译服务提供者\n // if (this.config.custom) {\n // const customProvider = new CustomTranslationProvider(this.config.custom);\n // this.providers.set('custom', customProvider);\n // }\n }\n\n /**\n * 翻译文本\n */\n async translate(\n text: string,\n from: string = this.config.defaultSourceLang ?? 'auto',\n to: string = this.config.defaultTargetLang ?? 'en'\n ): Promise<TranslationResult> {\n if (!this.config.enabled) {\n throw new Error('翻译服务未启用');\n }\n\n const provider = this.providers.get(this.config.provider);\n if (!provider) {\n throw new Error(`翻译服务提供者 '${this.config.provider}' 未找到或未配置`);\n }\n\n if (!provider.isConfigured()) {\n throw new Error(`翻译服务提供者 '${this.config.provider}' 配置不完整`);\n }\n\n const providerName = provider.name ?? this.config.provider;\n\n // 先检查缓存是否存在\n const cachedResult = this.cache.get(text, from, to, providerName);\n if (cachedResult) {\n Logger.info(`[缓存] 翻译: \"${text}\" -> \"${cachedResult.translatedText}\"`);\n return {\n originalText: cachedResult.originalText,\n translatedText: cachedResult.translatedText,\n sourceLanguage: cachedResult.sourceLanguage,\n targetLanguage: cachedResult.targetLanguage,\n provider: cachedResult.provider,\n };\n }\n\n Logger.verbose(`使用 ${providerName} 翻译: \"${text}\" (${from} -> ${to})`);\n\n try {\n const result = await provider.translate(text, from, to);\n Logger.info(`翻译完成: \"${text}\" -> \"${result.translatedText}\"`);\n\n // 翻译完成后存入缓存\n this.cache.set(text, result.translatedText, from, to, providerName);\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n Logger.error(`翻译失败: ${errorMessage}`);\n throw error;\n }\n }\n\n /**\n * 批量翻译文本数组\n */\n async translateBatch(\n texts: string[],\n from: string = this.config.defaultSourceLang ?? 'auto',\n to: string = this.config.defaultTargetLang ?? 'en'\n ): Promise<TranslationResult[]> {\n if (!this.config.enabled) {\n throw new Error('翻译服务未启用');\n }\n\n // 优化API调用参数\n const concurrency = Math.min(this.config.concurrency ?? 5, 5); // 限制最大并发为5\n const retryTimes = this.config.retryTimes ?? 3;\n const retryDelay = Math.max(this.config.retryDelay ?? 1000, 1000); // 至少1秒的重试延迟\n const batchDelay = Math.max(this.config.batchDelay ?? 500, 500); // 至少0.5秒的批次延迟\n\n Logger.info(\n `开始批量翻译,文本数量: ${texts.length},并发数: ${concurrency},重试次数: ${retryTimes}`,\n 'normal'\n );\n Logger.verbose(`翻译方向: ${from} -> ${to}`);\n\n // 检查哪些文本已经在缓存中\n const providerName = this.config.provider;\n const textsToTranslate: string[] = [];\n const cachedResults: Map<string, TranslationResult> = new Map();\n\n for (let i = 0; i < texts.length; i++) {\n const text = texts[i];\n if (!text) continue;\n\n const cachedResult = this.cache.get(text, from || 'auto', to || 'en', providerName);\n\n if (cachedResult) {\n // 使用缓存结果\n Logger.verbose(`[缓存] 文本 ${i + 1}/${texts.length}: \"${text}\"`);\n cachedResults.set(`translate_${i}`, {\n originalText: cachedResult.originalText,\n translatedText: cachedResult.translatedText,\n sourceLanguage: cachedResult.sourceLanguage,\n targetLanguage: cachedResult.targetLanguage,\n provider: cachedResult.provider,\n });\n } else {\n // 需要翻译\n textsToTranslate.push(text);\n }\n }\n\n // 创建翻译队列\n const queue = new TranslationQueue(concurrency);\n queue.setBatchDelay(batchDelay);\n\n // 添加需要翻译的文本到队列\n for (let i = 0; i < texts.length; i++) {\n if (!cachedResults.has(`translate_${i}`)) {\n const text = texts[i];\n if (!text) continue;\n\n queue.addTask({\n id: `translate_${i}`,\n execute: () => this.translate(text, from || 'auto', to || 'en'),\n maxRetries: retryTimes,\n retryDelay: retryDelay,\n });\n }\n }\n\n Logger.info(`需要翻译的文本: ${textsToTranslate.length},从缓存加载: ${cachedResults.size}`);\n\n // 如果所有文本都在缓存中,则直接返回\n if (textsToTranslate.length === 0) {\n Logger.info(`所有文本都从缓存加载,跳过API调用`);\n const results: TranslationResult[] = [];\n for (let i = 0; i < texts.length; i++) {\n const result = cachedResults.get(`translate_${i}`);\n if (result) {\n results.push(result);\n }\n }\n return results;\n }\n\n // 执行所有任务\n const completedResults = await queue.executeAll();\n\n // 整理结果\n const results: TranslationResult[] = [];\n const failedTasks = queue.getFailedTasks();\n\n for (let i = 0; i < texts.length; i++) {\n const taskId = `translate_${i}`;\n\n // 如果是缓存结果\n if (cachedResults.has(taskId)) {\n const result = cachedResults.get(taskId);\n if (result) {\n results.push(result);\n }\n continue;\n }\n\n // 如果是API请求结果\n const result = completedResults.get(taskId) as TranslationResult | undefined;\n\n if (result) {\n results.push(result);\n } else {\n const error = failedTasks.get(taskId);\n const errorMessage = error?.message ?? '未知错误';\n const currentText = texts[i];\n if (currentText) {\n Logger.error(`文本 \"${currentText}\" 翻译失败: ${errorMessage}`);\n // 为失败的翻译创建一个占位结果\n results.push({\n originalText: currentText,\n translatedText: currentText, // 失败时返回原文\n sourceLanguage: from || 'auto',\n targetLanguage: to || 'en',\n provider: this.config.provider,\n });\n }\n }\n }\n\n const stats = queue.getStats();\n Logger.info(\n `批量翻译完成,成功: ${stats.completed},失败: ${stats.failed},总计: ${texts.length},使用缓存: ${cachedResults.size}`\n );\n\n return results;\n }\n\n /**\n * 翻译语言文件(JSON)\n * 此方法整合了原来的translateJsonFile和translateLanguageFiles\n * 支持增量翻译和实时写入结果\n */\n async translateLanguageFile(\n sourcePath: string,\n targetLocale: string,\n from: string = this.config.defaultSourceLang ?? 'auto',\n to: string = this.config.defaultTargetLang ?? 'en',\n incrementalMode: boolean = true // 是否增量翻译模式\n ): Promise<{\n outputPath: string;\n totalCount: number;\n successCount: number;\n skippedCount: number;\n }> {\n if (!fileExists(sourcePath)) {\n throw new Error(`源语言文件不存在: ${sourcePath}`);\n }\n\n Logger.info(`📖 读取JSON文件: ${sourcePath}`);\n\n // 解析源文件\n const sourceContent = await readJson<Record<string, unknown>>(sourcePath);\n const totalSourceItems = Object.keys(sourceContent).length;\n\n // 生成正确的目标文件名,使用targetLocale,如\"en-US.json\"而不是\"zh-CN.en.json\"\n const outputPath = path.join(path.dirname(sourcePath), `${targetLocale}.json`);\n\n // 如果是增量翻译模式,先尝试读取已有的翻译文件\n let existingTranslations: Record<string, unknown> = {};\n let skippedCount = 0;\n if (incrementalMode && fileExists(outputPath)) {\n Logger.info(`增量翻译模式: 加载已有的翻译文件 ${outputPath}`);\n try {\n existingTranslations = await readJson<Record<string, unknown>>(outputPath);\n Logger.info(`已加载 ${Object.keys(existingTranslations).length} 个已翻译的项`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n Logger.warn(`读取已有翻译文件失败: ${errorMessage},将创建新文件`);\n existingTranslations = {};\n }\n }\n\n // 提取需要翻译的文本\n const keysToTranslate: string[] = [];\n const textsToTranslate: string[] = [];\n const finalTranslations: Record<string, unknown> = { ...existingTranslations };\n\n // 分析源文件,确定哪些需要翻译\n for (const key in sourceContent) {\n const value = sourceContent[key];\n // 只翻译字符串类型的值\n if (typeof value === 'string') {\n if (incrementalMode && key in existingTranslations && existingTranslations[key]) {\n // 如果已经翻译过,并且是增量模式,则跳过\n Logger.verbose(`跳过已翻译的键: ${key}`);\n skippedCount++;\n continue;\n }\n keysToTranslate.push(key);\n textsToTranslate.push(value);\n } else {\n // 非字符串值直接复制\n finalTranslations[key] = value;\n }\n }\n\n // 所有需要翻译的新条目数\n const needTranslateCount = textsToTranslate.length;\n\n if (needTranslateCount === 0) {\n Logger.info(`没有需要翻译的新文本,保留所有已翻译内容`);\n // 依然写入文件,以确保输出文件存在\n await writeJson(outputPath, finalTranslations, true);\n return {\n outputPath,\n totalCount: totalSourceItems,\n successCount: 0, // 没有新翻译,成功数为0\n skippedCount: skippedCount, // 返回跳过的数量\n };\n }\n\n Logger.info(`🔄 开始翻译 ${needTranslateCount} 个文本条目...`);\n\n // 批量翻译文本\n const results = await this.translateBatch(textsToTranslate, from, to);\n\n // 处理翻译结果\n let successCount = 0;\n const realTimeWriteInterval = 50; // 每翻译50个条目写入一次文件\n\n for (let i = 0; i < results.length; i++) {\n const key = keysToTranslate[i];\n const result = results[i];\n\n if (\n key &&\n typeof key === 'string' &&\n result &&\n result.translatedText !== result.originalText\n ) {\n finalTranslations[key] = result.translatedText;\n successCount++;\n } else if (key && typeof key === 'string') {\n // 翻译失败或未变化,保留原文\n finalTranslations[key] = sourceContent[key];\n }\n\n // 实时写入文件,防止在大量翻译时因错误丢失已翻译内容\n if ((i + 1) % realTimeWriteInterval === 0 || i === results.length - 1) {\n try {\n await writeJson(outputPath, finalTranslations, true);\n Logger.verbose(`实时保存翻译结果到 ${outputPath},已处理 ${i + 1}/${results.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n Logger.warn(`实时保存翻译结果失败: ${errorMessage}`);\n }\n }\n }\n\n // 最终保存结果\n await writeJson(outputPath, finalTranslations, true);\n\n // 移除冗余日志输出,由CLI层负责统一输出\n Logger.verbose(`翻译完成,结果保存到: ${outputPath}`);\n\n return {\n outputPath,\n totalCount: totalSourceItems,\n successCount,\n skippedCount,\n };\n }\n\n /**\n * 检查服务是否可用\n */\n isAvailable(): boolean {\n if (!this.config.enabled) {\n return false;\n }\n\n const provider = this.providers.get(this.config.provider);\n return provider ? provider.isConfigured() : false;\n }\n\n /**\n * 获取当前提供者支持的语言\n */\n getSupportedLanguages(): string[] {\n const provider = this.providers.get(this.config.provider);\n return provider ? provider.getSupportedLanguages() : [];\n }\n\n /**\n * 获取可用的翻译服务提供者列表\n */\n getAvailableProviders(): string[] {\n return Array.from(this.providers.keys()).filter((name) => {\n const provider = this.providers.get(name);\n return provider?.isConfigured() ?? false;\n });\n }\n\n /**\n * 获取缓存状态\n */\n getCacheStatus(): { size: number } {\n return {\n size: this.cache.size(),\n };\n }\n\n /**\n * 清除缓存\n */\n clearCache(): void {\n this.cache.clear();\n Logger.info('翻译缓存已清空');\n }\n}\n","import { ConfigManager, loadConfig } from '../config';\nimport { TranslationManager } from './manager';\nimport { readFile, fileExists } from '../utils/fs';\nimport { Logger } from '../utils/logger';\nimport * as path from 'path';\n\ninterface TranslateOptions {\n config: string;\n from?: string;\n to?: string;\n input?: string;\n json?: string;\n batch?: boolean;\n test?: boolean;\n incremental?: boolean; // 增量翻译选项,默认启用\n}\n\nexport async function translateCommand(options: TranslateOptions): Promise<void> {\n // 加载配置\n const config = loadConfig(options.config);\n ConfigManager.init(config);\n\n if (!config.translation?.enabled) {\n Logger.info('翻译功能未启用,请在配置文件中设置 translation.enabled = true', 'normal');\n return;\n }\n\n // 初始化翻译管理器\n const translationManager = new TranslationManager({\n enabled: config.translation.enabled,\n provider: config.translation.provider ?? 'baidu',\n defaultSourceLang: config.translation.defaultSourceLang ?? 'zh',\n defaultTargetLang: config.translation.defaultTargetLang ?? 'en',\n concurrency: config.translation.concurrency ?? 5,\n retryTimes: config.translation.retryTimes ?? 3,\n retryDelay: config.translation.retryDelay ?? 1000,\n batchDelay: config.translation.batchDelay ?? 500,\n baidu:\n config.translation.baidu?.appid && config.translation.baidu?.key\n ? {\n appid: config.translation.baidu.appid,\n key: config.translation.baidu.key,\n }\n : undefined,\n custom:\n config.translation.custom?.endpoint && config.translation.custom?.apiKey\n ? {\n endpoint: config.translation.custom.endpoint,\n apiKey: config.translation.custom.apiKey,\n }\n : undefined,\n });\n\n if (!translationManager.isAvailable()) {\n Logger.error('翻译服务不可用,请检查配置', 'minimal');\n Logger.info('百度翻译需要配置 appid 和 key', 'normal');\n return;\n }\n\n // 确定翻译方向\n const defaultSourceLang = config.translation.defaultSourceLang ?? 'zh';\n const defaultTargetLang = config.translation.defaultTargetLang ?? config.fallbackLocale ?? 'en';\n const from = options.from ?? defaultSourceLang;\n const to = options.to ?? defaultTargetLang;\n\n // 增量翻译模式,默认启用\n const incrementalMode = options.incremental !== false;\n\n try {\n // 根据不同的选项执行相应的翻译操作\n if (options.test && options.input) {\n await handleTranslateTest(translationManager, options.input, from, to);\n } else if (options.json) {\n await handleTranslateJsonFile(translationManager, options.json, from, to, incrementalMode);\n } else if (options.batch) {\n await handleTranslateBatchFiles(translationManager, from, to, incrementalMode);\n } else if (options.input) {\n await handleTranslateInput(translationManager, options.input, from, to);\n } else {\n // 没有传递任何参数时,使用默认行为:读取配置的outputDir下的默认语言文件\n const outputDir = config.outputDir ?? 'locales';\n const sourceLocale = config.locale ?? 'zh-CN';\n const targetLocale = config.fallbackLocale ?? 'en-US';\n\n Logger.info(\n `未指定翻译内容,将从 ${outputDir} 目录读取 ${sourceLocale}.json 文件并翻译成 ${targetLocale}`,\n 'normal'\n );\n\n const sourceLang = sourceLocale.split('-')[0] || 'zh'; // 提取语言代码,如zh-CN -> zh\n const targetLang = targetLocale.split('-')[0] || 'en'; // 提取语言代码,如en-US -> en\n\n try {\n const sourcePath = path.join(outputDir, `${sourceLocale}.json`);\n if (!fileExists(sourcePath)) {\n throw new Error(`源语言文件不存在: ${sourcePath},请先运行生成命令创建源语言文件`);\n }\n\n Logger.info(`📖 从源语言文件读取: ${sourcePath}`);\n\n const { outputPath, totalCount, successCount, skippedCount } =\n await translationManager.translateLanguageFile(\n sourcePath,\n targetLocale,\n sourceLang,\n targetLang,\n incrementalMode\n );\n\n Logger.success(`✅ 翻译完成,结果保存到: ${outputPath}`, 'normal');\n if (skippedCount > 0) {\n if (successCount > 0) {\n Logger.info(\n `📊 成功翻译: ${successCount}项新内容,跳过已翻译项: ${skippedCount}项,共${totalCount}项`,\n 'normal'\n );\n } else {\n Logger.info(\n `📊 无新内容需要翻译,已有翻译项: ${skippedCount}项,共${totalCount}项`,\n 'normal'\n );\n }\n } else {\n Logger.info(`📊 成功翻译: ${successCount}/${totalCount}项`, 'normal');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n Logger.error(`翻译失败: ${errorMessage}`, 'minimal');\n showUsageHelp();\n }\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n Logger.error(`翻译失败: ${errorMessage}`, 'minimal');\n process.exit(1);\n }\n}\n\n/**\n * 测试翻译单个文本\n */\nasync function handleTranslateTest(\n manager: TranslationManager,\n text: string,\n from: string,\n to: string\n): Promise<void> {\n Logger.info(`测试翻译模式 (${from} -> ${to})`, 'normal');\n Logger.info(`原文: ${text}`, 'normal');\n\n const result = await manager.translate(text, from, to);\n Logger.success(`译文: ${result.translatedText}`, 'normal');\n Logger.info(`提供者: ${result.provider}`, 'normal');\n}\n\n/**\n * 翻译指定的JSON文件\n */\nasync function handleTranslateJsonFile(\n manager: TranslationManager,\n jsonPath: string,\n from: string,\n to: string,\n incremental: boolean = true\n): Promise<void> {\n // 推导目标语言的Locale\n const targetLocale = ConfigManager.get().fallbackLocale || 'en-US';\n\n const { outputPath, totalCount, successCount, skippedCount } =\n await manager.translateLanguageFile(jsonPath, targetLocale, from, to, incremental);\n\n Logger.success(`✅ 翻译完成,结果保存到: ${outputPath}`, 'normal');\n if (skippedCount > 0) {\n if (successCount > 0) {\n Logger.info(\n `📊 成功翻译: ${successCount}项新内容,跳过已翻译项: ${skippedCount}项,共${totalCount}项`,\n 'normal'\n );\n } else {\n Logger.info(\n `📊 无新内容需要翻译,已有翻译项: ${skippedCount}项,共${totalCount}项`,\n 'normal'\n );\n }\n } else {\n Logger.info(`📊 成功翻译: ${successCount}/${totalCount}项`, 'normal');\n }\n}\n\n/**\n * 批量翻译语言文件\n */\nasync function handleTranslateBatchFiles(\n manager: TranslationManager,\n from: string,\n to: string,\n incremental: boolean = true\n): Promise<void> {\n const config = ConfigManager.get();\n const outputDir = config.outputDir ?? './locales';\n const sourceLocale = config.locale ?? from;\n const targetLocale = config.fallbackLocale ?? 'en-US';\n\n const sourcePath = path.join(outputDir, `${sourceLocale}.json`);\n if (!fileExists(sourcePath)) {\n throw new Error(`源语言文件不存在: ${sourcePath},请先运行生成命令创建源语言文件`);\n }\n\n Logger.info(`📖 从源语言文件读取: ${sourcePath}`);\n\n const { outputPath, totalCount, successCount, skippedCount } =\n await manager.translateLanguageFile(sourcePath, targetLocale, from, to, incremental);\n\n Logger.success(`✅ 批量翻译完成,结果保存到: ${outputPath}`, 'normal');\n if (skippedCount > 0) {\n if (successCount > 0) {\n Logger.info(\n `📊 成功翻译: ${successCount}项新内容,跳过已翻译项: ${skippedCount}项,共${totalCount}项`,\n 'normal'\n );\n } else {\n Logger.info(\n `📊 无新内容需要翻译,已有翻译项: ${skippedCount}项,共${totalCount}项`,\n 'normal'\n );\n }\n } else {\n Logger.info(`📊 成功翻译: ${successCount}/${totalCount}项`, 'normal');\n }\n}\n\n/**\n * 翻译输入的文本或文件\n */\nasync function handleTranslateInput(\n manager: TranslationManager,\n input: string,\n from: string,\n to: string\n): Promise<void> {\n let text = input;\n\n // 检查是否是文件路径\n if (fileExists(input)) {\n try {\n text = await readFile(input, 'utf-8');\n Logger.info(`从文件读取内容: ${input}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n throw new Error(`读取文件失败: ${errorMessage}`);\n }\n }\n\n Logger.info(`正在翻译 (${from} -> ${to})...`, 'normal');\n const result = await manager.translate(text, from, to);\n\n Logger.info('翻译结果:', 'normal');\n Logger.info(`原文 (${result.sourceLanguage}): ${result.originalText}`, 'normal');\n Logger.success(`译文 (${result.targetLanguage}): ${result.translatedText}`, 'normal');\n Logger.info(`提供者: ${result.provider}`, 'verbose');\n}\n\n/**\n * 显示使用帮助\n */\nfunction showUsageHelp(): void {\n Logger.error('请指定翻译内容:', 'minimal');\n Logger.info(' 使用 -i 指定文本或文件路径', 'normal');\n Logger.info(' 使用 -j 指定JSON文件路径', 'normal');\n Logger.info(' 使用 --batch 批量翻译语言文件', 'normal');\n Logger.info(' 使用 --test -i \"文本\" 测试翻译', 'normal');\n Logger.info(' 使用 --incremental=false 禁用增量翻译模式', 'normal');\n}\n","import { Command } from 'commander';\n// 处理CommonJS兼容性\nconst inquirer = require('inquirer').default ?? require('inquirer');\nimport { loadConfig, ConfigManager } from './config';\nimport { scanAndReplaceAll } from './ast';\nimport { writeJson } from './utils/fs';\nimport { defaultConfig } from './config/default.config';\nimport { version } from '../package.json';\nimport { Logger } from './utils/logger';\nimport { ConfigValidator } from './utils/config-validator';\n\nconst program = new Command();\n\nprogram\n .name('i18n-xy')\n .description('自动提取React项目中的中文字符串并国际化')\n .version(version);\n\nprogram\n .command('init')\n .description('初始化i18n配置')\n .action(async () => {\n const answers = await inquirer.prompt([\n {\n type: 'input',\n name: 'outputDir',\n message: '请输入国际化文件输出目录:',\n default: 'locales',\n },\n {\n type: 'input',\n name: 'configPath',\n message: '请输入配置文件保存路径:',\n default: './i18n.config.json',\n },\n ]);\n\n const config = {\n ...defaultConfig,\n outputDir: answers.outputDir,\n };\n\n try {\n await writeJson(answers.configPath, config, true);\n Logger.success(`配置文件已生成: ${answers.configPath}`, 'minimal');\n Logger.info('你可以根据项目需求修改配置文件。', 'normal');\n } catch (error) {\n Logger.error(`配置文件生成失败: ${error}`, 'minimal');\n process.exit(1);\n }\n });\n\nprogram\n .command('extract')\n .description('提取中文并生成i18n key-value')\n .option('-c, --config <path>', '指定配置文件路径', './i18n.config.json')\n .action(async (options) => {\n try {\n Logger.info(`开始加载配置文件: ${options.config}`, 'verbose');\n const config = loadConfig(options.config);\n ConfigManager.init(config);\n\n // 执行配置验证\n const validation = ConfigValidator.validateConfigUsage();\n if (!validation.isValid) {\n Logger.error('配置验证失败,无法继续执行', 'minimal');\n process.exit(1);\n }\n\n ConfigValidator.checkConfigConsistency();\n Logger.info('配置加载完成,开始执行提取与替换流程...', 'normal');\n await scanAndReplaceAll();\n Logger.success('提取与替换流程已完成', 'minimal');\n } catch (error) {\n Logger.error(`提取过程中发生错误: ${error}`, 'minimal');\n process.exit(1);\n }\n });\n\nprogram\n .command('translate')\n .description('翻译中文字符串到其他语言')\n .option('-c, --config <path>', '指定配置文件路径', './i18n.config.json')\n .option('-f, --from <from>', '源语言代码(如:zh, en, auto)')\n .option('-t, --to <to>', '目标语言代码(如:en, zh, ja, ko)')\n .option('-i, --input <input>', '要翻译的文本或文件路径')\n .option('-j, --json <json>', '指定要翻译的JSON文件路径')\n .option('--batch', '批量翻译语言文件(从配置的源语言文件翻译)')\n .option('--test', '测试模式:翻译单个文本')\n .action(async (options) => {\n try {\n Logger.info('开始执行翻译命令...', 'verbose');\n const { translateCommand } = await import('./translation/cli');\n await translateCommand(options);\n } catch (error) {\n Logger.error(`翻译失败: ${error}`, 'minimal');\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import { parse } from '@babel/parser';\nimport * as t from '@babel/types';\nimport { findTargetFiles, readFile, writeFileWithTempDir } from '../utils/fs';\nimport { ConfigManager } from '../config';\nimport { createI18nKey, initI18nCache, flushI18nCache } from '../gen-key-value';\nimport { Logger } from '../utils/logger';\nimport { ImportManager } from '../utils/import-manager';\n\n// 简单的require导入,只针对有兼容性问题的模块\nconst traverse = require('@babel/traverse').default;\nconst generate = require('@babel/generator').default;\n\n// 中文字符正则\nconst CHINESE_RE = /[\\u4e00-\\u9fa5]/;\n\n/**\n * 检查节点是否在不应该替换的位置\n * 包括:TypeScript 类型位置、对象属性键、import/export 语句等\n */\nfunction isInTypePosition(path: any): boolean {\n let current = path;\n\n while (current) {\n const parent = current.parent;\n const parentPath = current.parentPath;\n\n if (!parent || !parentPath) break;\n\n // 检查是否是对象属性的键\n if (t.isObjectProperty(parent) && parent.key === current.node) {\n return true;\n }\n\n // 检查是否是对象方法的键\n if (t.isObjectMethod(parent) && parent.key === current.node) {\n return true;\n }\n\n // 检查是否在 import/export 语句中\n if (\n t.isImportDeclaration(parent) ||\n t.isExportDeclaration(parent) ||\n t.isImportSpecifier(parent) ||\n t.isExportSpecifier(parent)\n ) {\n return true;\n }\n\n // 检查各种 TypeScript 类型上下文\n if (\n t.isTSTypeAnnotation(parent) || // : string\n t.isTSLiteralType(parent) || // type T = \"literal\"\n t.isTSUnionType(parent) || // \"a\" | \"b\"\n t.isTSIntersectionType(parent) || // A & B\n t.isTSTypeReference(parent) || // SomeType<T>\n t.isTSTypeLiteral(parent) || // { key: \"value\" }\n t.isTSInterfaceDeclaration(parent) || // interface I {}\n t.isTSTypeAliasDeclaration(parent) || // type T = ...\n t.isTSEnumDeclaration(parent) || // enum E {}\n t.isTSEnumMember(parent) || // A = \"value\" -- 但需要特殊处理枚举成员的值\n t.isTSPropertySignature(parent) || // { prop: \"type\" }\n t.isTSMethodSignature(parent) || // { method(): \"return\" }\n t.isTSCallSignatureDeclaration(parent) || // { (): \"return\" }\n t.isTSConstructSignatureDeclaration(parent) || // { new(): \"type\" }\n t.isTSIndexSignature(parent) || // { [key: string]: \"value\" }\n t.isTSMappedType(parent) || // { [K in keyof T]: \"value\" }\n t.isTSConditionalType(parent) || // T extends \"literal\" ? A : B\n t.isTSInferType(parent) || // infer R\n t.isTSTypeParameter(parent) || // <T extends \"literal\">\n t.isTSTypeParameterDeclaration(parent) // <T = \"default\">\n ) {\n // 特殊处理: 枚举成员值需要进行替换,但枚举成员名称不需要\n if (t.isTSEnumMember(parent) && parent.initializer === current.node) {\n return false; // 枚举成员的值需要替换,所以不在类型位置\n }\n return true;\n }\n\n // 检查是否在泛型参数中\n if (t.isTSTypeParameterInstantiation(parent)) {\n return true;\n }\n\n // 检查是否在类属性的键位置\n if (t.isClassProperty(parent) && parent.key === current.node) {\n return true;\n }\n\n // 检查是否在成员表达式的属性位置 (obj.property)\n if (t.isMemberExpression(parent) && parent.property === current.node && !parent.computed) {\n return true;\n }\n\n // 向上遍历\n current = parentPath;\n }\n\n return false;\n}\n\n/**\n * 检查字符串是否包含中文\n */\nfunction containsChinese(str: string): boolean {\n return CHINESE_RE.test(str);\n}\n\n/**\n * 创建国际化替换表达式\n */\nfunction createI18nCallExpression(\n functionName: string,\n key: string,\n quoteType: 'single' | 'double'\n): t.CallExpression {\n const keyNode = t.stringLiteral(key);\n\n // 当需要双引号时,确保节点具有正确的引号类型\n if (quoteType === 'double' && keyNode.extra) {\n keyNode.extra = { ...keyNode.extra, raw: `\"${key}\"`, rawValue: key };\n }\n\n return t.callExpression(t.identifier(functionName), [keyNode]);\n}\n\n/**\n * 处理字符串字面量\n */\nfunction handleStringLiteral(\n path: any,\n functionName: string,\n quoteType: 'single' | 'double'\n): boolean {\n // 跳过类型定义位置的字符串字面量\n if (isInTypePosition(path)) {\n return false;\n }\n\n const stringValue = path.node.value;\n if (stringValue && typeof stringValue === 'string' && containsChinese(stringValue)) {\n const key = createI18nKey(stringValue);\n\n // 根据配置创建带有适当引号类型的函数调用\n const callExpression = createI18nCallExpression(functionName, key, quoteType);\n path.replaceWith(callExpression);\n\n return true;\n }\n\n return false;\n}\n\nexport async function scanAndReplaceAll(): Promise<void> {\n const config = ConfigManager.get();\n\n Logger.info('开始扫描和替换中文字符串...', 'normal');\n Logger.verbose(`配置信息: \n - include: ${JSON.stringify(config.include)}\n - exclude: ${JSON.stringify(config.exclude)}\n - outputDir: ${config.outputDir}\n - locale: ${config.locale}\n - tempDir: ${config.tempDir || '无'}`);\n\n Logger.info('初始化i18n缓存...', 'verbose');\n await initI18nCache();\n\n Logger.info('搜索目标文件...', 'verbose');\n const files = await findTargetFiles(config.include, config.exclude);\n\n if (files.length === 0) {\n Logger.warn('没有找到匹配的文件', 'normal');\n return;\n }\n\n Logger.info(`找到 ${files.length} 个文件需要处理`, 'normal');\n Logger.verbose(`文件列表: ${files.join(', ')}`);\n\n // 获取替换配置\n const functionName = config.replac