i18n-xy
Version:
自动提取React项目中的中文字符串并进行国际化的CLI工具
1 lines • 316 kB
Source Map (JSON)
{"version":3,"sources":["../main/cli/index.ts","../package.json","../main/utils/fs.ts","../main/config/index.ts","../main/config/defaults.ts","../main/config/manager.ts","../main/config/validator.ts","../main/utils/logger.ts","../main/cli/commands/init.ts","../main/cli/wrapper.ts","../main/core/ast/index.ts","../main/core/key-generator/index.ts","../main/utils/import-manager.ts","../main/utils/pattern.ts","../main/cli/commands/extract.ts","../main/core/translation/providers/baidu.ts","../main/core/translation/queue.ts","../main/core/translation/cache.ts","../main/utils/progress-display.ts","../main/utils/write-queue.ts","../main/core/translation/manager.ts","../main/utils/config-validator.ts","../main/core/translation/translation-checker.ts","../main/core/translation/reports.ts","../main/core/translation/cli.ts","../main/cli/commands/translate.ts","../main/core/check/cli.ts","../main/cli/commands/check.ts","../main/cli/commands/rpkey.ts","../main/cli/commands/check-translation.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { version } from '../../package.json';\n\n// 导入各个命令\nimport { initCommand } from './commands/init';\nimport { extractCommand } from './commands/extract';\nimport { translateCommand } from './commands/translate';\nimport { checkCommand } from './commands/check';\nimport { rpkeyCommand } from './commands/rpkey';\nimport { checkTranslationCommand } from './commands/check-translation';\n\nconst program = new Command();\n\nprogram.name('i18n-xy').description('自动提取React项目中的中文字符串并国际化').version(version);\n\nprogram\n .command('init')\n .alias('i')\n .description('初始化i18n配置')\n .option('-o, --outputDir <dir>', '国际化文件输出目录', '.test-output/locales')\n .option('-t, --tempDir <dir>', '临时文件目录', '.test-output/temp')\n .option('-c, --configPath <path>', '配置文件保存路径', './i18n.config.json')\n .option('-l, --locale <locale>', '源语言代码', 'zh-CN')\n .option('-f, --functionName <name>', 'i18n函数名', '$t')\n .action(initCommand);\n\nprogram\n .command('extract')\n .alias('e')\n .description('提取中文并生成i18n key-value')\n .option('-c, --config <path>', '指定配置文件路径', './i18n.config.json')\n .action(extractCommand);\n\nprogram\n .command('translate')\n .alias('t')\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(translateCommand);\n\nprogram\n .command('check')\n .alias('ck')\n .description('检查还有哪些文件存在没有被t函数包裹的中文')\n .option('-c, --config <path>', '指定配置文件路径', './i18n.config.json')\n .option('-o, --output <path>', '自定义输出文件路径(默认生成i18n-check-report.md)')\n .option('-s, --summary', '使用简略输出格式,只显示文件路径和中文文案(默认启用)')\n .option('--detailed', '使用详细输出格式,显示完整信息')\n .option('--no-file', '不生成文件,仅在控制台输出摘要')\n .option('--include <types>', '指定要包含的内容类型:need(需要处理但未处理), ignored(已忽略), all(全部)', 'need')\n .action(checkCommand);\n\nprogram\n .command('rpkey')\n .alias('rk')\n .description('根据配置批量替换 $t(key) 为 $t(value)')\n .option('-c, --config <path>', '指定配置文件路径', './i18n.config.json')\n .action(rpkeyCommand);\n\nprogram\n .command('check-translation')\n .alias('ct')\n .description('检查语言文件的翻译完整性')\n .option('-c, --config <path>', '指定配置文件路径', './i18n.config.json')\n .option('-l, --languages <languages>', '指定要检查的目标语言,用逗号分隔(未指定时自动从outputDir发现)')\n .option('-o, --output [path]', '指定报告输出路径(Markdown格式),不指定路径时使用默认文件名')\n .option('-s, --summary', '简略模式:仅显示前20个缺失的键和未翻译条目(默认启用)')\n .option('--detailed', '详细模式:显示完整的缺失键和未翻译条目列表')\n .action(checkTranslationCommand);\n\nprogram.parse();","{\n \"name\": \"i18n-xy\",\n \"version\": \"0.0.30\",\n \"description\": \"自动提取React项目中的中文字符串并进行国际化的CLI工具\",\n \"main\": \"dist/index.js\",\n \"types\": \"dist/index.d.ts\",\n \"type\": \"module\",\n \"bin\": {\n \"i18n-xy\": \"dist/cli.cjs\",\n \"i18nx\": \"dist/cli.cjs\"\n },\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"engines\": {\n \"node\": \">=16.0.0\"\n },\n \"scripts\": {\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint main --ext .ts,.tsx --fix\",\n \"lint:check\": \"eslint main --ext .ts,.tsx\",\n \"format\": \"prettier --write \\\"main/**/*.{ts,tsx,js,jsx,json,md}\\\"\",\n \"format:check\": \"prettier --check \\\"main/**/*.{ts,tsx,js,jsx,json,md}\\\"\",\n \"format:fix\": \"pnpm run format && pnpm run lint\",\n \"type-check\": \"tsc --noEmit\",\n \"check-all\": \"pnpm run type-check && pnpm run lint:check && pnpm run format:check\",\n \"prepublishOnly\": \"pnpm run build\",\n \"postpack\": \"echo '✅ 打包完成,可以使用 npm publish 发布'\",\n \"clean-test\": \"rm -rf test-output dist\",\n \"extract\": \"pnpm build && node dist/cli.cjs extract\",\n \"check\": \"pnpm build && node dist/cli.cjs check --no-file\",\n \"translate\": \"pnpm build && node dist/cli.cjs translate --batch\",\n \"rpkey\": \"pnpm build && node dist/cli.cjs rpkey\",\n \"test:full\": \"pnpm clean-test && pnpm build && pnpm extract && pnpm check && pnpm translate && pnpm rpkey && echo '✅ 所有CLI测试完成'\"\n },\n \"keywords\": [\n \"i18n\",\n \"internationalization\",\n \"localization\",\n \"l10n\",\n \"translation\",\n \"react\",\n \"vue\",\n \"angular\",\n \"frontend\",\n \"cli\",\n \"command-line-tool\",\n \"development-tool\",\n \"build-tool\",\n \"typescript\",\n \"javascript\",\n \"jsx\",\n \"tsx\",\n \"babel\",\n \"ast\",\n \"abstract-syntax-tree\",\n \"code-transformation\",\n \"string-extraction\",\n \"extraction\",\n \"automation\",\n \"chinese\",\n \"chinese-characters\",\n \"pinyin\",\n \"chinese-to-pinyin\",\n \"multilingual\",\n \"language-files\",\n \"json-generation\",\n \"locale\",\n \"text-processing\",\n \"parser\",\n \"generator\",\n \"codemod\",\n \"refactoring\"\n ],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"\"\n },\n \"devDependencies\": {\n \"@babel/cli\": \"^7.27.2\",\n \"@babel/core\": \"^7.27.4\",\n \"@types/babel__generator\": \"^7.27.0\",\n \"@types/babel__traverse\": \"^7.20.7\",\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/inquirer\": \"^9.0.8\",\n \"@types/lodash\": \"^4.17.18\",\n \"@types/micromatch\": \"^4.0.9\",\n \"@types/node\": \"^24.0.3\",\n \"@types/string-hash\": \"^1.1.3\",\n \"@typescript-eslint/eslint-plugin\": \"^8.13.0\",\n \"@typescript-eslint/parser\": \"^8.13.0\",\n \"eslint\": \"^9.15.0\",\n \"eslint-config-prettier\": \"^9.1.0\",\n \"eslint-plugin-prettier\": \"^5.2.1\",\n \"prettier\": \"^3.3.3\",\n \"tsup\": \"^8.5.0\",\n \"typescript\": \"^5.8.3\"\n },\n \"dependencies\": {\n \"@babel/generator\": \"^7.27.5\",\n \"@babel/parser\": \"^7.27.5\",\n \"@babel/traverse\": \"^7.27.4\",\n \"@babel/types\": \"^7.27.6\",\n \"baidu-translate-service\": \"^2.2.4\",\n \"cli-progress\": \"^3.12.0\",\n \"commander\": \"^14.0.0\",\n \"fast-glob\": \"^3.3.3\",\n \"fs-extra\": \"^11.3.0\",\n \"inquirer\": \"^12.6.3\",\n \"lodash\": \"^4.17.21\",\n \"micromatch\": \"^4.0.8\",\n \"pinyin-pro\": \"^3.26.0\",\n \"string-hash\": \"^1.1.3\"\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 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}","import { merge } from 'lodash';\nimport { I18nConfig } from './types';\nimport { defaultConfig } from './defaults';\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// 导出其他相关模块\nexport { ConfigManager } from './manager';\nexport { ConfigValidator } from './validator';\nexport type { I18nConfig } from './types';","import { I18nConfig } from './types';\n\n// 默认配置\nexport const defaultConfig: I18nConfig = {\n // 基础语言配置\n locale: 'zh-CN',\n displayLanguage: 'en-US',\n outputDir: 'locales',\n // tempDir: undefined, // 可选,不设置则直接修改源文件\n\n // 文件处理配置\n include: [\n 'src/**/*.{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 useOriginalTextAsKey: false, // 默认使用生成的key\n templateString: {\n enabled: true, // 默认启用模板字符串智能处理\n preserveExpressions: true, // 默认保留原始表达式\n splitStrategy: 'smart', // 默认智能拆分策略\n },\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};","import { I18nConfig } from './types';\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 reset(): void {\n this.config = null;\n }\n\n isInitialized(): boolean {\n return this.config !== null;\n }\n}\n\nexport const ConfigManager = new ConfigManagerClass();","import { ConfigManager } from './manager';\nimport { Logger } from '../utils/logger';\n\n/**\n * 配置一致性检查工具\n * 用于验证所有配置项是否正确使用,没有硬编码值\n */\nexport class ConfigValidator {\n /**\n * 检查配置的一致性和完整性\n */\n static validateConfigUsage(): {\n isValid: boolean;\n errors: string[];\n warnings: string[];\n } {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n try {\n const config = ConfigManager.get();\n\n // 检查必需配置\n if (!config.include || config.include.length === 0) {\n errors.push('配置项 include 不能为空');\n }\n\n if (!config.outputDir) {\n errors.push('配置项 outputDir 不能为空');\n }\n\n if (!config.locale) {\n errors.push('配置项 locale 不能为空');\n }\n\n // 检查翻译配置一致性\n if (config.translation?.enabled) {\n if (!config.translation.provider) {\n errors.push('启用翻译时必须指定 provider');\n }\n\n if (config.translation.provider === 'baidu') {\n if (!config.translation.baidu?.appid || !config.translation.baidu?.key) {\n warnings.push('百度翻译服务缺少 appid 或 key 配置');\n }\n }\n }\n\n // 检查key生成配置\n if (config.keyGeneration) {\n if (config.keyGeneration.maxChineseLength && config.keyGeneration.maxChineseLength <= 0) {\n warnings.push('maxChineseLength 应该大于0');\n }\n\n if (config.keyGeneration.hashLength && config.keyGeneration.hashLength <= 0) {\n warnings.push('hashLength 应该大于0');\n }\n }\n\n // 检查日志配置\n if (\n config.logging?.level &&\n !['minimal', 'normal', 'verbose'].includes(config.logging.level)\n ) {\n errors.push('logging.level 必须是 minimal、normal 或 verbose 之一');\n }\n\n // 检查输出配置\n if (config.output?.localeFileName && !config.output.localeFileName.includes('{locale}')) {\n warnings.push('localeFileName 建议包含 {locale} 占位符');\n }\n } catch (error) {\n errors.push(`配置检查时发生错误: ${error}`);\n }\n\n const isValid = errors.length === 0;\n\n if (!isValid) {\n Logger.error('配置验证失败:', 'minimal');\n errors.forEach((error) => Logger.error(` - ${error}`, 'minimal'));\n }\n\n if (warnings.length > 0) {\n Logger.warn('配置警告:', 'normal');\n warnings.forEach((warning) => Logger.warn(` - ${warning}`, 'normal'));\n }\n\n // 移除\"配置验证通过,无警告\"输出以减少日志噪音\n\n return { isValid, errors, warnings };\n }\n\n /**\n * 检查是否存在配置与代码不一致的问题\n */\n static checkConfigConsistency(): void {\n const config = ConfigManager.get();\n\n Logger.verbose('检查配置一致性...');\n\n // 检查文件名生成是否使用了配置\n const expectedFileName = (config.output?.localeFileName ?? '{locale}.json').replace(\n '{locale}',\n config.locale\n );\n Logger.verbose(`预期的语言文件名: ${expectedFileName}`);\n\n // 检查替换函数名是否一致\n const functionName = config.replacement?.functionName ?? '$t';\n Logger.verbose(`预期的替换函数名: ${functionName}`);\n\n // 检查目录配置\n Logger.verbose(`输出目录: ${config.outputDir}`);\n if (config.tempDir) {\n Logger.verbose(`临时目录: ${config.tempDir}`);\n }\n\n Logger.verbose('配置一致性检查完成');\n }\n}","import { ConfigManager } from '../config';\n\nexport class Logger {\n private static temporaryLevel: string | null = null; // 临时日志级别\n \n private static getLogLevel(): string {\n try {\n // 优先使用临时级别\n if (this.temporaryLevel) {\n return this.temporaryLevel;\n }\n const config = ConfigManager.get();\n return config.logging?.level ?? 'normal';\n } catch {\n // 如果ConfigManager未初始化,使用默认值\n return 'normal';\n }\n }\n\n /**\n * 临时设置日志级别(用于进度显示期间)\n */\n static setTemporaryLevel(level: string | null): void {\n this.temporaryLevel = level;\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}","import { writeJson } from '../../utils/fs';\nimport { Logger } from '../../utils/logger';\nimport { defaultConfig } from '../../config/defaults';\nimport type { I18nConfig } from '../../config';\n\ninterface InitOptions {\n outputDir?: string;\n tempDir?: string;\n configPath?: string;\n locale?: string;\n functionName?: string;\n}\n\nasync function initHandler(options: InitOptions): Promise<void> {\n // 使用传入的参数或默认值\n const outputDir = options.outputDir || '.test-output/locales';\n const tempDir = options.tempDir || '.test-output/temp';\n const configPath = options.configPath || './i18n.config.json';\n const locale = options.locale || defaultConfig.locale;\n const functionName = options.functionName || defaultConfig.replacement?.functionName;\n\n const config: I18nConfig = {\n ...defaultConfig,\n outputDir,\n tempDir,\n locale,\n replacement: {\n ...defaultConfig.replacement,\n functionName\n }\n };\n\n try {\n await writeJson(configPath, config, true);\n Logger.success(`配置文件已生成: ${configPath}`, 'minimal');\n Logger.info('你可以根据项目需求修改配置文件。', 'normal');\n } catch (error) {\n Logger.error(`配置文件生成失败: ${error}`, 'minimal');\n process.exit(1);\n }\n}\n\n// init命令不需要wrapper,直接导出\nexport const initCommand = initHandler;","import { loadConfig, ConfigManager, ConfigValidator } from '../config';\nimport { Logger } from '../utils/logger';\nimport type { I18nConfig } from '../config';\n\ninterface CLIWrapperOptions<T> {\n configRequired?: boolean;\n validator?: (config: I18nConfig) => void;\n preProcess?: (options: T) => T;\n}\n\n/**\n * CLI统一包裹函数\n * 统一处理配置加载、验证、错误处理等逻辑\n */\nexport function withCLI<T extends Record<string, any>>(\n handler: (options: T, config: I18nConfig) => Promise<void>,\n wrapperOptions: CLIWrapperOptions<T> = {}\n) {\n return async (options: T) => {\n try {\n // 1. 参数预处理\n const processedOptions = wrapperOptions.preProcess?.(options) ?? options;\n \n // 2. 配置加载(统一在这里处理)\n let config: I18nConfig | null = null;\n if (wrapperOptions.configRequired !== false) {\n // 默认使用根目录下的配置文件\n const configPath = (processedOptions as any).config ?? './i18n.config.json';\n config = loadConfig(configPath);\n ConfigManager.init(config);\n \n // 3. 配置验证(统一在这里处理)\n const validation = ConfigValidator.validateConfigUsage();\n if (!validation.isValid) {\n Logger.error('配置验证失败,无法继续执行', 'minimal');\n process.exit(1);\n }\n ConfigValidator.checkConfigConsistency();\n }\n \n // 4. 执行自定义验证器(如果有)\n if (config && wrapperOptions.validator) {\n wrapperOptions.validator(config);\n }\n \n // 5. 执行业务逻辑\n await handler(processedOptions, config!);\n } catch (error) {\n Logger.error(`命令执行失败: ${error}`, 'minimal');\n process.exit(1);\n }\n };\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 '../key-generator';\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 */\nexport type ContextType = 'code' | 'comment' | 'ts-definition' | 'object-key' | 'enum-value' | 'import-export' | 'type-annotation';\n\n/**\n * 上下文分析结果\n */\nexport interface ContextAnalysis {\n shouldProcess: boolean;\n contextType: ContextType;\n reason?: string;\n}\n\n/**\n * 检查节点是否在注释中\n */\nfunction isInComment(path: any, codeLines: string[]): boolean {\n const loc = path.node.loc;\n if (!loc || !codeLines) return false;\n\n const lineIndex = loc.start.line - 1;\n if (lineIndex >= 0 && lineIndex < codeLines.length) {\n const line = codeLines[lineIndex];\n const beforeText = line.substring(0, loc.start.column);\n const afterText = line.substring(loc.end.column);\n \n // 检查单行注释 //\n if (beforeText.includes('//')) {\n return true;\n }\n \n // 检查多行注释 /* */\n if (beforeText.includes('/*') && !beforeText.includes('*/')) {\n return true;\n }\n \n // 检查是否在多行注释中间\n for (let i = lineIndex - 1; i >= 0; i--) {\n const prevLine = codeLines[i];\n if (prevLine.includes('*/')) {\n break; // 找到注释结束,不在注释中\n }\n if (prevLine.includes('/*')) {\n return true; // 找到注释开始,在注释中\n }\n }\n \n // 检查JSDoc注释 /** */\n if (beforeText.includes('/**') || beforeText.includes('*')) {\n return true;\n }\n }\n \n return false;\n}\n\n/**\n * 检查节点是否在不应该替换的位置并返回详细的上下文信息\n * 包括:TypeScript 类型位置、对象属性键、import/export 语句、注释等\n */\nfunction analyzeContext(path: any, codeLines?: string[]): ContextAnalysis {\n // 首先检查是否在注释中\n if (codeLines && isInComment(path, codeLines)) {\n return {\n shouldProcess: false,\n contextType: 'comment',\n reason: '注释中的文本'\n };\n }\n\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 {\n shouldProcess: false,\n contextType: 'object-key',\n reason: '对象属性键名'\n };\n }\n\n // 检查是否是对象方法的键\n if (t.isObjectMethod(parent) && parent.key === current.node) {\n return {\n shouldProcess: false,\n contextType: 'object-key',\n reason: '对象方法名'\n };\n }\n\n // 检查是否在 import/export 语句中\n // 注意:只检查直接的import/export声明,不包括export function内部的内容\n if (\n t.isImportDeclaration(parent) ||\n t.isImportSpecifier(parent) ||\n t.isExportSpecifier(parent) ||\n // 只有当字符串直接在export声明中时才忽略(如 export { \"key\" as alias })\n // 而不是在export function/class内部\n (t.isExportDeclaration(parent) && (\n t.isExportAllDeclaration(parent) ||\n t.isExportDefaultDeclaration(parent) && current.node === parent.declaration ||\n t.isExportNamedDeclaration(parent) && parent.specifiers?.some(spec => spec === current.node)\n ))\n ) {\n return {\n shouldProcess: false,\n contextType: 'import-export',\n reason: 'import/export语句'\n };\n }\n\n // 检查各种 TypeScript 类型上下文\n if (\n t.isTSTypeAnnotation(parent) || // : string\n t.isTSLiteralType(parent) || // type T = \"literal\"\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.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 as any).initializer === current.node) {\n return {\n shouldProcess: true,\n contextType: 'enum-value',\n reason: '枚举值'\n };\n }\n return {\n shouldProcess: false,\n contextType: 'ts-definition',\n reason: 'TypeScript类型定义'\n };\n }\n\n // 增强的联合类型检测\n if (t.isTSUnionType(parent)) {\n // 检查当前节点是否是联合类型的成员\n if (parent.types && parent.types.includes(current.node)) {\n return {\n shouldProcess: false,\n contextType: 'type-annotation',\n reason: '联合类型成员'\n };\n }\n // 检查是否在联合类型的字面量类型中\n if (t.isTSLiteralType(current.parent) && parent.types.includes(current.parent)) {\n return {\n shouldProcess: false,\n contextType: 'type-annotation',\n reason: '联合类型字面量'\n };\n }\n }\n\n // 检查是否在接口属性的类型注解中\n if (t.isTSPropertySignature(parent)) {\n if (parent.typeAnnotation && parent.typeAnnotation.typeAnnotation === current.node) {\n return {\n shouldProcess: false,\n contextType: 'type-annotation',\n reason: '接口属性类型注解'\n };\n }\n // 检查接口属性的类型注解内部\n if (t.isTSTypeAnnotation(current.parent) && parent.typeAnnotation === current.parent) {\n return {\n shouldProcess: false,\n contextType: 'type-annotation',\n reason: '接口属性类型注解'\n };\n }\n }\n\n // 检查是否在类型别名定义中\n if (t.isTSTypeAliasDeclaration(parent)) {\n if (parent.typeAnnotation === current.node) {\n return {\n shouldProcess: false,\n contextType: 'ts-definition',\n reason: '类型别名定义'\n };\n }\n }\n\n // 检查是否在泛型参数中\n if (t.isTSTypeParameterInstantiation(parent)) {\n return {\n shouldProcess: false,\n contextType: 'type-annotation',\n reason: '泛型参数'\n };\n }\n\n // 检查是否在类属性的键位置\n if (t.isClassProperty(parent) && parent.key === current.node) {\n return {\n shouldProcess: false,\n contextType: 'object-key',\n reason: '类属性名'\n };\n }\n\n // 检查是否在成员表达式的属性位置 (obj.property)\n if (t.isMemberExpression(parent) && parent.property === current.node && !parent.computed) {\n return {\n shouldProcess: false,\n contextType: 'object-key',\n reason: '成员表达式属性名'\n };\n }\n\n // 增强的函数参数类型检测\n if (\n t.isFunctionDeclaration(parent) ||\n t.isFunctionExpression(parent) ||\n t.isArrowFunctionExpression(parent)\n ) {\n // 检查是否在函数参数的类型注解中\n if (parent.params) {\n for (const param of parent.params) {\n if (t.isIdentifier(param) && param.typeAnnotation) {\n const typeNode = (param.typeAnnotation as any).typeAnnotation;\n if (isNodeInTypeAnnotation(current.node, typeNode)) {\n return {\n shouldProcess: false,\n contextType: 'type-annotation',\n reason: '函数参数类型注解'\n };\n }\n }\n }\n }\n }\n\n // 向上遍历\n current = parentPath;\n }\n\n // 默认情况,应该处理\n return {\n shouldProcess: true,\n contextType: 'code',\n reason: '代码中的字符串'\n };\n}\n\n/**\n * 检查节点是否在不应该替换的位置(向后兼容)\n */\nfunction isInTypePosition(path: any): boolean {\n return !analyzeContext(path).shouldProcess;\n}\n\n/**\n * 辅助函数:检查节点是否在类型注解内部\n */\nfunction isNodeInTypeAnnotation(targetNode: any, typeAnnotation: any): boolean {\n if (!typeAnnotation) return false;\n\n if (typeAnnotation === targetNode) return true;\n\n if (t.isTSUnionType(typeAnnotation)) {\n return typeAnnotation.types.some((type: any) => isNodeInTypeAnnotation(targetNode, type));\n }\n\n if (t.isTSLiteralType(typeAnnotation)) {\n return typeAnnotation.literal === targetNode;\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 // 获取配置,判断是否使用原始中文文本作为key\n\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 * 检查节点是否已经是国际化函数调用格式(如$t(key))\n */\nfunction isI18nFunctionCall(path: any, functionName: string): boolean {\n const node = path.node;\n\n // 检查是否是函数调用\n if (!t.isCallExpression(node)) {\n return false;\n }\n\n // 检查函数名是否匹配\n if (!t.isIdentifier(node.callee) || node.callee.name !== functionName) {\n return false;\n }\n\n // 检查参数数量\n if (node.arguments.length !== 1) {\n return false;\n }\n\n // 检查参数是否为字符串字面量\n if (!t.isStringLiteral(node.arguments[0])) {\n return false;\n }\n\n return true;\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 // 检查父节点是否已经是国际化函数调用\n const parentPath = path.parentPath;\n if (parentPath && isI18nFunctionCall(parentPath, functionName)) {\n Logger.verbose(`跳过已经国际化的函数调用: ${functionName}(${path.node.value})`);\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\n/**\n * 检查结果接口\n */\nexport interface CheckResult {\n file: string;\n issues: Array<{\n line: number;\n column: number;\n text: string;\n type: 'string' | 'template' | 'jsx-text' | 'jsx-attribute';\n context?: string;\n shouldProcess: boolean;\n contextType: 'code' | 'comment' | 'ts-definition' | 'object-key' | 'enum-value' | 'import-export' | 'type-annotation';\n reason?: string; // 不应该处理的原因\n }>;\n}\n\n/**\n * 检查文件中未国际化的中文字符串\n */\nexport async function checkUnwrappedChinese(): Promise<CheckResult[]> {\n const config = ConfigManager.get();\n const functionName = config.replacement?.functionName ?? '$t';\n\n Logger.info('开始检查未国际化的中文字符串...', 'normal');\n Logger.verbose(`配置信息: \n - include: ${JSON.stringify(config.include)}\n - exclude: ${JSON.stringify(config.exclude)}\n - tempDir: ${config.tempDir || '无'}\n - functionName: ${functionName}`);\n\n Logger.info('搜索目标文件...', 'verbose');\n \n let files: string[];\n let sourceDescription: string;\n \n // 优先检查tempDir,如果存在且有文件则使用;否则使用include配置\n if (config.tempDir) {\n try {\n // 检查tempDir是否存在文件\n const tempFiles = await findTargetFiles([`${config.tempDir}/**/*.{js,jsx,ts,tsx}`], config.exclude);\n if (tempFiles.length > 0) {\n files = tempFiles;\n sourceDescription = `临时目录 (${config.tempDir})`;\n Logger.info(`使用临时目录文件进行检查: ${config.tempDir}`, 'verbose');\n } else {\n // tempDir存在但无文件,使用原始文件\n files = await findTargetFiles(config.include, config.exclude);\n sourceDescription = '原始文件';\n Logger.info('临时目录无文件,使用原始文件进行检查', 'verbose');\n }\n } catch (error) {\n // tempDir访问失败,使用原始文件\n files = await findTargetFiles(config.include, config.exclude);\n sourceDescription = '原始文件';\n Logger.verbose(`临时目录访问失败: ${error}, 使用原始文件进行检查`);\n }\n } else {\n // 没有tempDir配置,使用include配置\n files = await findTargetFiles(config.include, config.exclude);\n sourceDescription = '原始文件';\n }\n\n if (files.length === 0) {\n Logger.warn(`在${sourceDescription}中没有找到匹配的文件`, 'normal');\n return [];\n }\n\n Logger.info(`找到 ${files.length} 个文件需要检查 (来源: ${sourceDescription})`, 'normal');\n\n const results: CheckResult[] = [];\n let processedCount = 0;\n let totalIssues = 0;\n\n for (const file of files) {\n processedCount++;\n Logger.info(`[${processedCount}/${files.length}] 检查文件: ${file}`, 'verbose');\n\n const code = await readFile(file, 'utf-8');\n const fileResult: CheckResult = { file, issues: [] };\n\n let ast;\n try {\n ast = parse(code, {\n sourceType: 'unambiguous',\n plugins: ['jsx', 'typescript', 'classProperties', 'decorators-legacy', 'dynamicImport'],\n });\n } catch (error) {\n Logger.warn(`解析文件失败: ${file}`, 'minimal');\n Logger.verbose(`错误详情: ${error}`);\n continue;\n }\n\n // 获取代码行信息用于定位\n const codeLines = code.split('\\n');\n\n traverse(ast, {\n // 检查字符串字面量\n StringLiteral(path: any) {\n const contextAnalysis = analyzeContext(path, codeLines);\n\n const parentPath = path.parentPath;\n if (parentPath && isI18nFunctionCall(parentPath, functionName)) return;\n\n const stringValue = path.node.value;\n if (stringValue && typeof stringValue === 'string' && containsChinese(stringValue)) {\n const loc = path.node.loc;\n if (loc) {\n fileResult.issues.push({\n line: loc.start.line,\n column: loc.start.column,\n text: stringValue,\n type: 'string',\n context: getContextInfo(path, codeLines),\n shouldProcess: contextAnalysis.shouldProcess,\n contextType: contextAnalysis.contextType,\n reason: contextAnalysis.reason,\n });\n }\n }\n },\n\n // 检查模板字符串\n TemplateLiteral(path: any) {\n const contextAnalysis = analyzeContext(path, codeLines);\n\n const parentPath = path.parentPath;\n if (parentPath && isI18nFunctionCall(parentPath, functionName)) return;\n\n // 检查模板字符串的静态部分\n path.node.quasis.forEach((quasi: any) => {\n const raw = quasi.value?.raw;\n if (raw && typeof raw === 'string' && containsChinese(raw)) {\n const loc = path.node.loc;\n if (loc) {\n fileResult.issues.push({\n line: loc.start.line,\n column: loc.start.column,\n text: raw,\n type: 'template',\n context: getContextInfo(path, codeLines),\n shouldProcess: contextAnalysis.shouldProcess,\n contextType: contextAnalysis.contextType,\n reason: contextAnalysis.reason,\n });\n }\n }\n });\n\n // 检查模板字符串的表达式部分\n path.node.expressions.forEach((expr: any) => {\n if (t.isStringLiteral(expr) && containsChinese(expr.value)) {\n const loc = expr.loc;\n if (loc) {\n fileResult.issues.push({\n line: loc.start.line,\n column: loc.start.column,\n text: expr.value,\n type: 'template',\n context: getContextInfo(path, codeLines),\n shouldProcess: contextAnalysis.shouldProcess,\n contextType: contextAnalysis.contextType,\n reason: contextAnalysis.reason,\n });\n }\n }\n });\n },\n\n // 检查JSX文本\n JSXText(path: any) {\n const textValue = path.node.value;\n if (textValue && typeof textValue === 'string' && containsChinese(textValue)) {\n const contextAnalysis = analyzeContext(path, codeLines);\n\n const parentPath = path.parentPath;\n if (\n parentPath?.isJSXExpressionContainer() &&\n parentPath.node.expression &&\n isI18nFunctionCall({ node: parentPath.node.expression }, functionName)\n ) {\n return;\n }\n\n const trimmedValue = textValue.trim();\n if (trimmedValue) {\n const loc = path.node.loc;\n if (loc) {\n fileResult.issues.push({\n line: loc.start.line,\n column: loc.start.column,\n text: trimmedValue,\n type: 'jsx-text',\n context: getContextInfo(path, codeLines),\n shouldProcess: contextAnalysis.shouldProcess,\n contextType: contextAnalysis.contextType,\n reason: contextAnalysis.reason,\n });\n }\n }\n }\n },\n\n // 检查JSX属性\n JSXAttribute(path: any) {\n if (\n t.isStringLiteral(path.node.value) &&\n path.node.value.value &&\n typeof path.node.value.value === 'string' &&\n containsChinese(path.node.value.value)\n ) {\n const contextAnalysis = analyzeContext(path, codeLines);\n\n if (\n t.isJSXExpressionContainer(path.node.value) &&\n path.node.value.expression &&\n isI18nFunctionCall({ node: path.node.value.expression }, functionName)\n ) {\n return;\n }\n\n const attributeValue = path.node.value.value;\n const loc = path.node.loc;\n if (loc) {\n fileResult.issues.push({\n line: loc.start.line,\n column: loc.start.column,\n text: attributeValue,\n type: 'jsx-attribute',\n context: getContextInfo(path, codeLines),\n shouldProcess: contextAnalysis.shouldProcess,\n contextType: contextAnalysis.contextType,\n reason: contextAnalysis.reason,\n });\n }\n }\n },\n });\n\n if (fileResult.issues.length > 0) {\n results.push(fileResult);\n totalIssues += fileResult.issues.length;\n Logger.info(`文件 ${file} 发现 ${fileResult.issues.length} 个未国际化的中文字符串`, 'normal');\n } else {\n Logger.verbose(`文件 ${file} 无未国际化的中文字符串`);\n }\n }\n\n Logger.success(`检查完成!`, 'minimal');\n Logger.info(`统计信息:`, 'normal');\n Logger.info(` - 检查文件总数: ${processedCount}`, 'normal');\n Logger.info(` - 有问题的文件数: ${results.length}`, 'normal');\n Logger.info(` - 未国际化字符串总数: ${totalIssues}`, 'normal');\n\n return results;\n}\n\n/**\n * 获取上下文信息\n */\nfunction getContextInfo(path: any, codeLines: string[]): string {\n const loc = path.node.loc;\n if (!loc || !codeLines) return '';\n\n const lineIndex = loc.start.line - 1;\n if (lineIndex >= 0 && lineIndex < codeLines.length) {\n return codeLines[lineIndex]?.trim() ?? '';\n }\n return '';\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.replacement?.functionName ?? '$t';\n const quoteType = config.replacement?.quoteType ?? 'single';\n const autoImportEnabled = config.replacement?.autoImport?.enabled ?? false;\n const insertPosition = config.replacement?.autoImport?.insertPosition ?? 'afterImports';\n const imports = config.replacement?.autoImport?.imports ?? {};\n\n // 获取模板字符串配置\n const templateConfig = config.replacement?.templateString ?? {\n enabled: true,\n preserveExpressions: true,\n splitStrategy: 'smart',\n };\n\n Logger.verbose(`替换配置:\n - 函数名: ${functionName}\n - 引号类型: ${quoteType}\n - 自动导入: ${autoImportEnabled ? '启用' : '禁用'}\n - 插入位置: ${insertPosition}\n - 模板字符串处理: ${templateConfig.enabled ? '启用' : '禁用'}\n - 拆分策略: ${templateConfig.splitStrategy}`);\n\n // 统计变量\n let processedCount = 0;\n let modifiedCount = 0;\n let totalReplacements = 0;\n\n // 创建import管理器实例\n const importManager = new ImportManager();\n\n for (const file of files) {\n processedCount++;\n Logger.info(`[${processedCount}/${files.length}] 处理文件: ${file}`, 'verbose');\n Logger.verbose(`正在处理文件: ${file}`);\n const code = await readFile(file, 'utf-8');\n\n let ast;\n try {\n ast = parse(code, {\n sourceType: 'unambiguous',\n plugins: ['jsx', 'typescript', 'classProperties', 'decorators-legacy', 'dynamicImport'],\n });\n } catch (error) {\n Logger.warn(`解析文件失败: ${file}`, 'minimal');\n Logger.verbose(`错误详情: ${error}`);\n continue;\n }\n\n // 重置import管理器状态\n importManager.reset();\n\n // 跟踪是否发生了替换\n let hasReplacement = false;\n let fileReplacements = 0;\n\n traverse(ast, {\n // 处理普通字符串字面量\n StringLiteral(path: any) {\n if (handleStringLiteral(path, functionName, quoteType)) {\n hasReplacement = true;\n fileReplacements++;\n const stringValue = path.node.value;\n Logger.verbose(\n `替换字符串: \"${stringValue}\" -> ${functionName}(${quoteType === 'single' ? \"'\" : '\"'}${stringValue}${quoteType === 'single' ? \"'\" : '\"'})`\n );\n }\n },\n\n // 处理模板字符串元素\n TemplateElement(path: any) {\n // 检查是否启用模板字符串处理\n if (!templateConfig.enabled) {\n return;\n }\n\n // 跳过类型定义位置的模板字符串\n if (isInTypePosition(path)) {\n return;\n }\n\n // 检查父节点是否已经是国际化函数调用的一部分\n const parentPath = path.parentPath;\n if (parentPath?.parentPath && isI18nFunctionCall(parentPath.parentPath, functionName)) {\n Logger.verbose(`跳过已经国际化的模板字符串元素`);\n return;\n }\n\n const raw = path.node.value?.raw;\n if (raw && typeof raw === 'string' && containsChinese(raw)) {\n // 检查中文片段\n const chineseMatches = raw.match(/[\\u4e00-\\u9fa5]+/g);\n if (chineseMatches && chineseMatches.length > 0) {\n // 应用不同的拆分策略\n if (templateConfig.splitStrategy === 'conservative') {\n // 保守策略:只替换独立的中文片段\n const result = raw.replace(/[\\u4e00-\\u9fa5]+/g, (match: string) => {\n const key = createI18nKey(match);\n const quoteChar = quoteType === 'single' ? \"'\" : '\"';\n return '${' + functionName + '(' + quoteChar + key + quoteChar + ')}';\n });\n path.node.value.raw = result;\n path.node.value.cooked = result;\n hasReplacement = true;\n fileReplacements++;\n Logger.verbose(`保守策略替换模板字符串中的中文片段`);\n } else {\n // 智能策略或激进策略:使用原有逻辑\n const SEPARATOR = '\\u{1F4D6}'; // 使用书本emoji作为分隔符,避免控制字符问题\n const replaced = raw.replace(\n /[\\u4e00-\\u9fa5]+/g,\n (match: string) => `${SEPARATOR}${match}${SEPARATOR}`\n );\n const parts = replaced\n .split(new RegExp(`${SEPARATOR}([\\\\u4e00-\\\\u9fa5]+)${SEPARATOR}`, 'g'))\n .filter(Boolean);\n let result = '';\n\n // 根据配置选择引号类型\n const quoteChar = quoteType === 'single' ? \"'\" : '\"';\n\n for (const part of parts) {\n if (/^[\\u4e00-\\u9fa5]+$/.test(part)) {\n const key = createI18nKey(part);\n result += '${' + functionName + '(' + quoteChar + key + quoteChar + ')}';\n } else {\n result += part;\n }\n }\n path.node.value.raw = result;\n path.node.value.cooked = result;\n hasReplacement = true;\n fileReplacements++;\n Logger.verbose(`${templateConfig.splitStrategy}策略替换模板字符串中的中文片段`);\n }\n }\n }\n },\n\n // 处理模板字符串\n TemplateLiteral(path: any) {\n // 检查父节点是否已经是国际化函数调用\n const parentPath = path.parentPath;\n if (parentPath && isI18nFunctionCall(parentPath, functionName)) {\n Logger.verbose(`跳过已经国际化的模板字符串`);\n return;\n }\n\n // 跳过类型定义位置的模板字符串\n if (isInTypePosition(path)) {\n return;\n }\n\n // 检查是否启用模板字符串处理\n if (!templateConfig.enabled) {\n return;\n }\n\n // 处理模板字符串:保持模板字符串格式,只替换其中的中文部分\n let hasModified = false;\n\n // 处理 quasis(模板字符串的静态部分)\n path.node.quasis.forEach((quasi: any) => {\n if (quasi?.value?.raw && containsChinese(quasi.value.raw)) {\n const raw = quasi.value.raw;\n // 检查中文片段\n const chineseMatches = raw.match(/[\\u4e00-\\u9fa5]+/g);\n\n if (chineseMatches && chineseMatches.length > 0) {\n // 在模板字符串内部替换中文为 ${$t('key')} 格式\n const newRaw = raw.replace(/[\\u4e00-\\u9fa5]+/g, (match: string) => {\n const key = createI18nKey(match);\n const quoteChar = quoteType === 'single' ? \"'\" : '\"';\n return '${' + functionName + '(' + quoteChar + key + quoteChar + ')}';\n });\n\n if (newRaw !== raw) {\n quasi.value.raw = newRaw;\n quasi.value.cooked = newRaw;\n hasModified = true;\n