mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-13 18:10:53 +08:00
【新增】插件git同步模块,用于同步项目内容,加速项目开发
【调整】前端暗色问题
This commit is contained in:
182
frontend/plugin/vite-plugin-i18n/README.md
Normal file
182
frontend/plugin/vite-plugin-i18n/README.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# vite-plugin-i18n-ai-translate
|
||||
|
||||
一个基于Vite的i18n自动化翻译插件,支持智谱AI等多种翻译服务。
|
||||
|
||||
## 特性
|
||||
|
||||
- 支持智谱AI和传统API多种翻译服务
|
||||
- 自动扫描并提取vue-i18n的$t模板变量中的中文内容
|
||||
- 并发翻译处理,提高效率
|
||||
- 智能缓存机制,避免重复翻译
|
||||
- 完善的错误处理和重试机制
|
||||
- 支持文件变更监听,实时翻译
|
||||
- 可扩展的翻译适配器设计
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install vite-plugin-i18n-ai-translate
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
在vite.config.js中配置插件:
|
||||
|
||||
```javascript
|
||||
import i18nAiTranslate from 'vite-plugin-i18n-ai-translate'
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
i18nAiTranslate({
|
||||
apiKey: {
|
||||
zhipuAI: 'your-zhipu-api-key',
|
||||
api1: 'your-api1-key',
|
||||
},
|
||||
languages: ['zhCN', 'zhTW', 'enUS', 'jaJP', 'koKR'],
|
||||
translateMethod: 'zhipuAI',
|
||||
// 其他配置项...
|
||||
}),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| --------------- | -------- | ---------------------------------------- | ------------------- |
|
||||
| projectPath | string | './src' | 项目扫描路径 |
|
||||
| outputPath | string | './locales' | 翻译文件输出路径 |
|
||||
| cachePath | string | './cache/translation_cache.json' | 缓存文件路径 |
|
||||
| logPath | string | './logs' | 日志文件路径 |
|
||||
| apiKey | object | {} | 各翻译服务的API密钥 |
|
||||
| languages | string[] | ['zhCN', 'zhTW', 'enUS', 'jaJP', 'koKR'] | 目标语言列表 |
|
||||
| concurrency | number | 100 | 并发翻译数量 |
|
||||
| templateRegex | string | '\$t\\(["\']([\u4e00-\u9fa5]+)["\']\\)' | 模板变量正则表达式 |
|
||||
| fileExtensions | string[] | ['.vue', '.js', '.ts'] | 扫描的文件类型 |
|
||||
| interval | number | 5000 | 文件监听间隔(ms) |
|
||||
| requestInterval | number | 100 | 请求间隔时间(ms) |
|
||||
| maxRetries | number | 3 | 最大重试次数 |
|
||||
| translateMethod | string | 'zhipuAI' | 使用的翻译服务 |
|
||||
| cacheLifetime | number | 7 | 缓存保留天数 |
|
||||
| logRetention | number | 30 | 日志保留天数 |
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. 扫描项目文件,提取需要翻译的中文文本
|
||||
2. 检查翻译缓存,跳过已翻译内容
|
||||
3. 使用配置的翻译服务进行并发翻译
|
||||
4. 更新翻译缓存
|
||||
5. 生成翻译文件
|
||||
6. 监听文件变更,触发实时翻译
|
||||
|
||||
## 支持的翻译服务
|
||||
|
||||
- 智谱AI翻译服务
|
||||
|
||||
- 支持多语言批量翻译
|
||||
- 基于GLM大语言模型
|
||||
- 高质量翻译结果
|
||||
|
||||
- 传统API翻译服务
|
||||
- 可扩展的适配器设计
|
||||
- 支持添加自定义翻译服务
|
||||
|
||||
## API 参考
|
||||
|
||||
### 核心类
|
||||
|
||||
#### TranslationAdapter
|
||||
|
||||
翻译适配器基类,定义统一的翻译接口。
|
||||
|
||||
方法:
|
||||
|
||||
- translate(text, apiKey, languages, maxRetries)
|
||||
- validateApiKey(apiKey)
|
||||
- getSupportedLanguages()
|
||||
- isLanguageSupported(language)
|
||||
|
||||
#### CacheManager
|
||||
|
||||
缓存管理类,处理翻译结果的缓存。
|
||||
|
||||
方法:
|
||||
|
||||
- initCache()
|
||||
- getCachedTranslations(texts, languages)
|
||||
- updateCache(texts, translations, languages)
|
||||
- cleanCache(validTexts)
|
||||
|
||||
#### LogManager
|
||||
|
||||
日志管理类,处理系统日志。
|
||||
|
||||
方法:
|
||||
|
||||
- init()
|
||||
- logError(error)
|
||||
- logInfo(message)
|
||||
- cleanLogs(days)
|
||||
- getLogs(logType, lines)
|
||||
|
||||
## 错误处理
|
||||
|
||||
插件包含完善的错误处理机制:
|
||||
|
||||
- 翻译失败自动重试
|
||||
- 详细的错误日志记录
|
||||
- 可配置的最大重试次数
|
||||
- 翻译服务异常处理
|
||||
- API密钥验证
|
||||
|
||||
## 开发扩展
|
||||
|
||||
### 添加新的翻译服务
|
||||
|
||||
1. 在 src/translation/traditional 或 src/translation/ai 目录下创建新的翻译服务模块
|
||||
2. 实现必要的翻译接口
|
||||
3. 创建对应的适配器类
|
||||
4. 在配置中添加新的翻译方法
|
||||
|
||||
### 自定义适配器示例
|
||||
|
||||
```javascript
|
||||
const TranslationAdapter = require('./index')
|
||||
|
||||
class CustomAdapter extends TranslationAdapter {
|
||||
async translate(text, apiKey, languages, maxRetries) {
|
||||
// 实现翻译逻辑
|
||||
}
|
||||
|
||||
async validateApiKey(apiKey) {
|
||||
// 实现密钥验证
|
||||
}
|
||||
|
||||
getSupportedLanguages() {
|
||||
// 返回支持的语言列表
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. 翻译服务不可用
|
||||
|
||||
- 检查API密钥是否正确
|
||||
- 确认网络连接正常
|
||||
- 查看错误日志获取详细信息
|
||||
|
||||
2. 翻译缓存问题
|
||||
|
||||
- 检查缓存文件权限
|
||||
- 适当调整缓存保留时间
|
||||
- 可以手动清理缓存目录
|
||||
|
||||
3. 文件监听不生效
|
||||
- 确认配置的文件扩展名正确
|
||||
- 检查监听间隔设置
|
||||
- 验证文件路径配置
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
22
frontend/plugin/vite-plugin-i18n/eslint.config.js
Normal file
22
frontend/plugin/vite-plugin-i18n/eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import vueConfig from '@baota/eslint/vue'
|
||||
import baseConfig from '@baota/eslint'
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
const config = [
|
||||
// Vue 相关配置,包含 TypeScript 支持
|
||||
...vueConfig,
|
||||
|
||||
// 基础配置,用于通用的 JavaScript/TypeScript 规则
|
||||
...baseConfig,
|
||||
|
||||
// 项目特定的配置覆盖
|
||||
{
|
||||
files: ['**/*.{js,ts,tsx,jsx,vue}'],
|
||||
rules: {
|
||||
// 在此处添加项目特定的规则覆盖
|
||||
'vue/multi-word-component-names': 'off', // 关闭组件名称必须由多个单词组成的规则
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default config
|
||||
303
frontend/plugin/vite-plugin-i18n/index.md
Normal file
303
frontend/plugin/vite-plugin-i18n/index.md
Normal file
@@ -0,0 +1,303 @@
|
||||
一、概述
|
||||
本项目是一个 Vite 插件,主要用于实现 i18n(国际化)自动化翻译功能。它会自动扫描项目文件,精准抓取 vue - i18n 的 $t 模板变量里的中文内容,借助不同的翻译服务(包括传统 API 翻译和 AI 批量翻译)进行翻译,并生成对应的翻译文件。同时,插件具备缓存机制,能有效加速对重复内容的处理,还可清理缓存文件中的无效数据。翻译过程采用并发模式,并发上限为 200,以提高处理效率。在扫描文件前,会同步缓存的状态配置,确保数据的一致性。各模块采用正交性设计,保证了系统的可维护性和扩展性。
|
||||
二、技术选型
|
||||
项目类型:Vite 插件
|
||||
Vite 版本:Vite 4.x 及以上,以充分利用其快速构建和热更新特性。
|
||||
Vue 版本:Vue 3.x,借助其响应式系统和组合式 API 提升开发效率。
|
||||
vue - i18n 版本:vue - i18n 9.x,适配 Vue 3 实现国际化支持。
|
||||
翻译服务:支持智谱 AI 等多种 AI 翻译服务以及传统 API 翻译服务,提供多样化的翻译选择。
|
||||
Lodash:用于数组处理等操作,简化复杂的数据处理逻辑。
|
||||
FS Promises:用于文件操作的异步处理,避免阻塞主线程。
|
||||
Axios:用于发送 HTTP 请求,与翻译服务进行通信。
|
||||
三、整体架构
|
||||
目录架构图
|
||||
plaintext
|
||||
vite-plugin-i18n-ai-translate/
|
||||
├── src/
|
||||
│ ├── fileOperation/ # 文件操作模块
|
||||
│ │ ├── index.js
|
||||
│ │ └── ...
|
||||
│ ├── translation/ # 翻译模块
|
||||
│ │ ├── index.js
|
||||
│ │ ├── adapter/ # 翻译适配器模块
|
||||
│ │ │ ├── index.js
|
||||
│ │ │ ├── traditionalApiAdapter.js # 传统 API 翻译适配器
|
||||
│ │ │ ├── aiBatchAdapter.js # AI 批量翻译适配器
|
||||
│ │ │ └── ...
|
||||
│ │ ├── ai/ # AI 翻译模块集合
|
||||
│ │ │ ├── zhipuAI.js # 智谱 AI 翻译模块
|
||||
│ │ │ ├── otherAI.js # 其他 AI 翻译模块示例
|
||||
│ │ │ └── ...
|
||||
│ │ └── traditional/ # 传统 API 翻译模块集合
|
||||
│ │ ├── api1.js # 传统 API 1 翻译模块
|
||||
│ │ ├── api2.js # 传统 API 2 翻译模块
|
||||
│ │ └── ...
|
||||
│ ├── stateManagement/ # 状态管理模块
|
||||
│ │ ├── index.js
|
||||
│ │ └── ...
|
||||
│ ├── cache/ # 缓存模块
|
||||
│ │ ├── index.js
|
||||
│ │ └── ...
|
||||
│ ├── logManagement/ # 日志管理模块
|
||||
│ │ ├── index.js
|
||||
│ │ └── ...
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── index.js
|
||||
│ │ └── ...
|
||||
├── config/
|
||||
│ └── config.json # 独立的配置文件
|
||||
├── cache/ # 缓存文件目录
|
||||
├── logs/ # 日志文件目录
|
||||
└── package.json # 项目依赖和脚本配置
|
||||
功能流程图
|
||||
plaintext
|
||||
开始
|
||||
|
|
||||
|-- 同步缓存状态配置
|
||||
|
|
||||
|-- 扫描文件
|
||||
| |
|
||||
| |-- 捕获 $t 模板变量中的中文内容
|
||||
| |
|
||||
| |-- 清理缓存中的无效数据
|
||||
| |
|
||||
| |-- 过滤重复内容(通过缓存)
|
||||
|
|
||||
|-- 并发调用翻译服务进行翻译(上限 200)
|
||||
| |
|
||||
| |-- 根据 translateMethod 选择对应翻译模块
|
||||
| | |
|
||||
| | |-- 通过适配器转换请求和响应格式
|
||||
| | | |
|
||||
| | | |-- 支持多个目标语言(JSON 字符串数组)
|
||||
| | | |-- 错误处理和重试机制
|
||||
| | | |-- 并发请求精细控制(请求间隔、失败重试)
|
||||
|
|
||||
|-- 生成翻译文件(动态追加)
|
||||
| |
|
||||
| |-- 文件操作模块创建、修改、复制、读取文件
|
||||
|
|
||||
|-- 更新缓存
|
||||
|
|
||||
|-- 状态管理模块更新状态
|
||||
|
|
||||
|-- 日志管理模块记录日志
|
||||
|
|
||||
结束
|
||||
功能清单
|
||||
文件操作模块:负责文件和目录的创建、修改、复制、读取等操作,为翻译文件的生成和管理提供支持。
|
||||
翻译模块:根据配置选择合适的翻译服务,通过适配器统一请求和响应格式,对中文内容进行并发翻译。
|
||||
状态管理模块:集中管理插件的状态信息,确保在扫描文件前同步缓存状态,保证数据一致性。
|
||||
缓存模块:创建和管理缓存文件,加速重复内容的处理,同时清理无效缓存,提高性能。
|
||||
日志管理模块:记录插件运行过程中的关键信息,包括错误信息,方便调试和问题排查。
|
||||
文件扫描模块:按照指定的文件后缀扫描项目文件,捕获 $t 模板变量中的中文内容。
|
||||
文件监听模块:按配置的时间间隔监听文件变化,触发翻译流程,实现实时更新。
|
||||
功能模块划分
|
||||
核心功能模块:翻译模块、文件操作模块,直接实现主要业务逻辑。
|
||||
辅助功能模块:状态管理模块、缓存模块、日志管理模块,为核心功能提供支持和保障。
|
||||
监测模块:文件扫描模块、文件监听模块,负责监测项目文件的变化。
|
||||
四、功能模块详细设计
|
||||
|
||||
1. 文件操作模块
|
||||
函数名:createFile
|
||||
参数:filePath(文件路径),content(文件内容)
|
||||
职责:创建指定路径的文件,并将内容写入文件,同时处理文件创建过程中可能出现的错误。
|
||||
函数名:modifyFile
|
||||
参数:filePath(文件路径),newContent(新的文件内容)
|
||||
职责:修改指定路径文件的内容,处理文件修改过程中的错误。
|
||||
函数名:copyFile
|
||||
参数:sourcePath(源文件路径),destinationPath(目标文件路径)
|
||||
职责:将源文件复制到目标路径,处理文件复制过程中的错误。
|
||||
函数名:readFile
|
||||
参数:filePath(文件路径)
|
||||
职责:读取指定路径文件的内容,处理文件读取过程中的错误。
|
||||
函数名:createDirectory
|
||||
参数:dirPath(目录路径)
|
||||
职责:创建指定路径的目录,处理目录创建过程中的错误。
|
||||
2. 翻译模块
|
||||
函数名:translateTexts
|
||||
参数:
|
||||
texts(待翻译的中文内容列表)
|
||||
apiKey(翻译服务的 API 密钥)
|
||||
languages(翻译的目标语言 JSON 字符串数组)
|
||||
concurrency(并发数量)
|
||||
requestInterval(请求间隔时间)
|
||||
maxRetries(最大重试次数)
|
||||
translateMethod(翻译方式,如 "zhipuAI"、"api1" 等)
|
||||
职责:根据 translateMethod 选择对应翻译模块,通过适配器将请求和响应格式统一,以指定的并发数量调用该模块对中文内容列表进行翻译,控制请求间隔,处理请求失败重试,支持多个目标语言,返回翻译结果列表。
|
||||
3. 翻译适配器模块(translation/adapter)
|
||||
适配器基类(translation/adapter/index.js)
|
||||
javascript
|
||||
class TranslationAdapter {
|
||||
constructor() {
|
||||
if (this.constructor === TranslationAdapter) {
|
||||
throw new Error('Abstract class cannot be instantiated directly.');
|
||||
}
|
||||
}
|
||||
|
||||
translate(text, apiKey, languages, maxRetries) {
|
||||
throw new Error('Method "translate" must be implemented.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = TranslationAdapter;
|
||||
传统 API 翻译适配器(translation/adapter/traditionalApiAdapter.js)
|
||||
javascript
|
||||
const TranslationAdapter = require('./index');
|
||||
const traditionalApiModule = require('../traditional/api1'); // 示例传统 API 模块
|
||||
|
||||
class TraditionalApiAdapter extends TranslationAdapter {
|
||||
async translate(text, apiKey, languages, maxRetries) {
|
||||
// 转换请求格式以适配传统 API
|
||||
const requestData = {
|
||||
text,
|
||||
apiKey,
|
||||
languages,
|
||||
maxRetries
|
||||
};
|
||||
const result = await traditionalApiModule.translate(requestData);
|
||||
// 转换响应格式以统一输出
|
||||
return {
|
||||
text,
|
||||
translations: result.translations
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TraditionalApiAdapter;
|
||||
AI 批量翻译适配器(translation/adapter/aiBatchAdapter.js)
|
||||
javascript
|
||||
const TranslationAdapter = require('./index');
|
||||
const aiModule = require('../ai/zhipuAI'); // 示例 AI 模块
|
||||
|
||||
class AIBatchAdapter extends TranslationAdapter {
|
||||
async translate(text, apiKey, languages, maxRetries) {
|
||||
// 转换请求格式以适配 AI 批量翻译
|
||||
const requestData = {
|
||||
text,
|
||||
apiKey,
|
||||
languages,
|
||||
maxRetries
|
||||
};
|
||||
const result = await aiModule.translate(requestData);
|
||||
// 转换响应格式以统一输出
|
||||
return {
|
||||
text,
|
||||
translations: result.translations
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AIBatchAdapter; 4. AI 翻译模块集合(translation/ai)和传统 API 翻译模块集合(translation/traditional)
|
||||
每个具体的翻译模块(如 zhipuAI.js、api1.js 等)负责与对应的翻译服务进行交互,接收适配器转换后的请求数据,返回翻译结果。5. 状态管理模块
|
||||
函数名:syncCacheState
|
||||
参数:无
|
||||
职责:在扫描文件前同步缓存的状态配置。
|
||||
函数名:updateState
|
||||
参数:newState(新的状态信息)
|
||||
职责:更新插件的状态信息。
|
||||
函数名:getState
|
||||
参数:无
|
||||
职责:获取插件的当前状态信息。6. 缓存模块
|
||||
函数名:getCachedTranslations
|
||||
参数:texts(待检查的中文内容列表),languages(翻译的目标语言 JSON 字符串数组),cachePath(缓存文件存放地址)
|
||||
职责:检查指定路径的缓存文件,返回已缓存的翻译结果,同时返回未缓存的中文内容列表。
|
||||
函数名:updateCache
|
||||
参数:texts(中文内容列表),translations(对应的翻译结果列表,符合翻译模块返回值固定格式),languages(翻译的目标语言 JSON 字符串数组),cachePath(缓存文件存放地址)
|
||||
职责:将新的翻译结果更新到指定路径的缓存文件中。
|
||||
函数名:cleanCache
|
||||
参数:validTexts(有效的中文内容列表),languages(翻译的目标语言 JSON 字符串数组),cachePath(缓存文件存放地址)
|
||||
职责:清理指定路径缓存文件中无效的翻译结果。7. 日志管理模块
|
||||
函数名:logInfo
|
||||
参数:message(日志信息)
|
||||
职责:记录普通日志信息。
|
||||
函数名:logError
|
||||
参数:error(错误信息)
|
||||
职责:记录错误日志信息。8. 文件扫描模块
|
||||
函数名:scanFiles
|
||||
参数:projectPath(项目路径),templateRegex(检索模板变量的正则表达式),fileExtensions(支持的扫描文件后缀)
|
||||
职责:扫描指定项目路径下符合后缀要求的文件,依据给定的正则表达式提取 $t 模板变量中的中文内容,并返回中文内容列表。
|
||||
9. 文件监听模块
|
||||
函数名:watchFiles
|
||||
参数:projectPath(项目路径),interval(监听文件间隔时间),callback(文件变化时的回调函数)
|
||||
职责:按指定的时间间隔监听项目路径下的文件变化,触发回调函数。
|
||||
五、业务 / 系统流程
|
||||
项目流程
|
||||
启动 Vite 项目,插件开始工作。
|
||||
状态管理模块同步缓存的状态配置,确保数据一致性。
|
||||
文件扫描模块按配置的文件后缀扫描项目文件,依据配置的正则表达式提取 $t 模板变量中的中文内容。
|
||||
缓存模块清理缓存文件中的无效数据,提高缓存的有效性。
|
||||
检查缓存,过滤掉已缓存的中文内容,减少不必要的翻译请求。
|
||||
翻译模块根据 translateMethod 选择对应翻译模块,通过适配器将请求和响应格式统一,以配置的并发数量对未缓存的中文内容进行翻译,控制请求间隔,处理请求失败重试,支持多个目标语言。
|
||||
文件操作模块根据翻译结果和指定的语言列表生成翻译文件,若文件已存在则动态追加内容,处理文件操作过程中的错误。
|
||||
缓存模块更新缓存文件,将新的翻译结果加入缓存,加速后续翻译。
|
||||
状态管理模块更新插件的状态信息,反映当前运行状态。
|
||||
日志管理模块记录插件运行过程中的关键信息,包括错误信息,方便调试和问题排查。
|
||||
文件监听模块按配置的时间间隔监听文件变化,若有变化则重复步骤 2 - 10,实现实时更新。
|
||||
交互流程
|
||||
插件在 Vite 构建过程中自动运行,无需用户手动干预。
|
||||
用户可以通过修改 config/config.json 文件调整插件的行为,如扫描路径、输出路径、翻译服务的 API 密钥、翻译的目标语言 JSON 字符串数组、并发数量、检索模板变量的正则表达式、缓存文件存放地址、支持的扫描文件后缀、监听文件间隔时间、请求间隔时间、最大重试次数、翻译方式等。
|
||||
相关建议
|
||||
在开发过程中,可提供配置文件的校验机制,确保用户输入的配置信息合法。
|
||||
对于翻译结果的准确性,可提供人工审核和修正的接口,以提高翻译质量。
|
||||
六、配置参数详细说明
|
||||
配置文件(config/config.json)
|
||||
json
|
||||
{
|
||||
"projectPath": "./src",
|
||||
"outputPath": "./locales",
|
||||
"apiKey": {
|
||||
"zhipuAI": "your_zhipuAI_api_key",
|
||||
"api1": "your_api1_api_key"
|
||||
},
|
||||
"cachePath": "./cache/translation_cache.json",
|
||||
"languages": ["zhCN", "zhTW", "enUS", "jaJP", "koKR", "ruRU", "ptBR", "frFR", "esAR", "arDZ"],
|
||||
"concurrency": 100,
|
||||
"templateRegex": "/\\$t\\(['\"]([\u4e00-\u9fa5]+)['\"]\\)/g",
|
||||
"fileExtensions": [".vue"],
|
||||
"interval": 5000,
|
||||
"requestInterval": 100,
|
||||
"maxRetries": 3,
|
||||
"translateMethod": "zhipuAI"
|
||||
}
|
||||
projectPath:项目扫描路径,插件将从该路径开始扫描文件。
|
||||
outputPath:翻译文件的输出路径,生成的翻译文件将存放在此。
|
||||
apiKey:包含不同翻译服务的 API 密钥,根据 translateMethod 选择使用。
|
||||
cachePath:缓存文件的存放地址,用于存储已翻译的内容。
|
||||
languages:翻译的目标语言列表,使用 JSON 字符串数组表示。
|
||||
concurrency:并发翻译的数量上限,控制并发请求的数量。
|
||||
templateRegex:检索 $t 模板变量中中文内容的正则表达式。
|
||||
fileExtensions:支持扫描的文件后缀列表,只有符合这些后缀的文件才会被扫描。
|
||||
interval:文件监听的时间间隔,单位为毫秒。
|
||||
requestInterval:翻译请求的间隔时间,避免对翻译服务造成过大压力。
|
||||
maxRetries:请求失败后的最大重试次数。
|
||||
translateMethod:选择的翻译方式,如 "zhipuAI"、"api1" 等。
|
||||
七、部署说明
|
||||
项目部署
|
||||
将插件代码复制到项目的 node_modules 目录下,或者使用 npm link 进行本地链接,使项目能够找到插件。
|
||||
在 Vite 配置文件(vite.config.js)中引入插件:
|
||||
javascript
|
||||
const i18nAiTranslatePlugin = require('vite-plugin-i18n-ai-translate');
|
||||
|
||||
module.exports = {
|
||||
plugins: [i18nAiTranslatePlugin()]
|
||||
};
|
||||
环境相关问题
|
||||
确保项目中已安装 Vite、Vue 和 vue - i18n,并且版本符合要求,以保证插件正常运行。
|
||||
确保网络连接正常,以便调用翻译服务的 API,否则翻译请求将失败。
|
||||
若并发数量设置过高,可能会导致网络拥堵或触发翻译服务的限流机制,需根据实际情况调整,避免影响翻译效率。
|
||||
监听文件间隔时间设置过短可能会增加系统开销,需根据项目规模和文件变更频率合理设置,平衡性能和实时性。
|
||||
请求间隔时间和最大重试次数应根据网络状况和翻译服务的稳定性进行调整,提高翻译的成功率。
|
||||
若使用不同的翻译方式,需确保相应的 API 密钥和配置正确,否则无法正常调用翻译服务。
|
||||
八、总结
|
||||
项目情况
|
||||
该 Vite 插件实现了 i18n 自动化翻译功能,通过模块化设计,各个模块采用正交性设计,具备良好的扩展性和可维护性。插件在扫描文件前同步缓存的状态配置,增加了错误处理和重试机制,对并发请求进行更精细的控制。AI 子模块支持多个目标语言,入参和返回值采用固定格式。翻译模块采用适配器模式,兼容传统的 API 翻译和 AI 批量翻译,提高了系统的兼容性和灵活性。同时,支持文件监听,可及时响应文件变化,实现实时更新。
|
||||
开发建议
|
||||
持续优化各个翻译模块和适配器的性能,提高翻译效率和准确性,减少翻译时间。
|
||||
增加更多的翻译服务支持,丰富翻译方式的选择,满足不同用户的需求。
|
||||
完善缓存模块对不同翻译方式和目标语言的缓存管理策略,提高缓存的命中率和清理效率。
|
||||
进一步细化错误处理和重试机制,针对不同翻译服务的错误类型进行更精准的处理,增强系统的稳定性。
|
||||
对并发请求控制进行智能化优化,根据翻译服务的实时状态动态调整并发数量和请求间隔,提高资源利用率。
|
||||
提供更友好的用户配置界面,方便用户调整插件的各项参数,降低使用门槛。
|
||||
加强日志管理,记录更详细的翻译过程信息,便于问题排查和性能分析,提高开发和维护效率。
|
||||
52
frontend/plugin/vite-plugin-i18n/package.json
Normal file
52
frontend/plugin/vite-plugin-i18n/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@baota/vite-plugin-i18n",
|
||||
"version": "1.0.0",
|
||||
"description": "A Vite plugin for automatic i18n translation using AI services",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||
"lint": "eslint src/**/*.js",
|
||||
"format": "prettier --write src/**/*.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.js",
|
||||
"require": "./src/index.js"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"vite-plugin",
|
||||
"i18n",
|
||||
"translation",
|
||||
"ai",
|
||||
"zhipuai"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@baota/eslint": "workspace:*",
|
||||
"@baota/prettier": "workspace:*",
|
||||
"vite": "^4.0.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {},
|
||||
"extensionsToTreatAsEsm": [
|
||||
".js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
frontend/plugin/vite-plugin-i18n/prettier.config.js
Normal file
3
frontend/plugin/vite-plugin-i18n/prettier.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import prettierConfig from '@baota/prettier'
|
||||
|
||||
export default prettierConfig
|
||||
131
frontend/plugin/vite-plugin-i18n/src/cache/index.js
vendored
Normal file
131
frontend/plugin/vite-plugin-i18n/src/cache/index.js
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { Utils } from '../utils/index.js'
|
||||
|
||||
export class CacheManager {
|
||||
constructor(cachePath) {
|
||||
this.cachePath = cachePath
|
||||
this.cache = new Map() // 缓存
|
||||
this.dirty = false // 是否需要保存缓存
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化缓存
|
||||
*/
|
||||
async initCache() {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(this.cachePath), { recursive: true })
|
||||
if (await this.fileExists(this.cachePath)) {
|
||||
const data = await fs.readFile(this.cachePath, 'utf8')
|
||||
const cacheData = JSON.parse(data)
|
||||
this.cache = new Map(Object.entries(cacheData))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化缓存失败:', error)
|
||||
this.cache = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的翻译
|
||||
* @param {string[]} texts - 待检查的中文内容列表
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {Promise<{cached: Object, uncached: string[]}>}
|
||||
*/
|
||||
async getCachedTranslations(texts, languages) {
|
||||
const cached = {}
|
||||
const uncached = []
|
||||
|
||||
texts.forEach((text) => {
|
||||
const cachedItem = this.cache.get(text)
|
||||
// 检查缓存项是否存在且有效
|
||||
if (cachedItem && this.isValidCacheItem(cachedItem, languages)) {
|
||||
cached[text] = cachedItem
|
||||
} else {
|
||||
uncached.push(text)
|
||||
}
|
||||
})
|
||||
|
||||
return { cached, uncached }
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
* @param {string[]} texts - 中文内容列表
|
||||
* @param {Object[]} translations - 翻译结果列表
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
*/
|
||||
async updateCache(texts, translations) {
|
||||
translations.forEach((translation, index) => {
|
||||
const text = texts[index]
|
||||
this.cache.set(text, {
|
||||
text,
|
||||
key: translation.key,
|
||||
translations: Utils.formatTranslations(translation.translations),
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
this.dirty = true
|
||||
await this.saveCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
* @param {string[]} validTexts - 有效的中文内容列表
|
||||
*/
|
||||
async cleanCache(validTexts) {
|
||||
const validTextSet = new Set(validTexts)
|
||||
for (const [text] of this.cache) {
|
||||
if (!validTextSet.has(text)) {
|
||||
this.cache.delete(text)
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dirty) {
|
||||
await this.saveCache()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存缓存
|
||||
*/
|
||||
async saveCache() {
|
||||
if (!this.dirty) return
|
||||
|
||||
try {
|
||||
const cacheData = Object.fromEntries(this.cache)
|
||||
await fs.writeFile(this.cachePath, JSON.stringify(cacheData, null, 2))
|
||||
this.dirty = false
|
||||
} catch (error) {
|
||||
console.error('保存缓存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存项是否有效
|
||||
* @param {Object} cacheItem - 缓存项
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValidCacheItem(cacheItem, languages) {
|
||||
return cacheItem && cacheItem.translations && languages.every((lang) => cacheItem.translations[lang])
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CacheManager
|
||||
@@ -0,0 +1,207 @@
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* 未使用翻译检测器
|
||||
* 负责检测和移除未使用的翻译内容
|
||||
*/
|
||||
export class UnusedTranslationDetector {
|
||||
/**
|
||||
* @param {Object} fileOperation - 文件操作实例
|
||||
* @param {Object} cacheManager - 缓存管理实例
|
||||
*/
|
||||
constructor(fileOperation, cacheManager) {
|
||||
this.fileOperation = fileOperation
|
||||
this.cacheManager = cacheManager
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描项目中实际使用的翻译键
|
||||
* @param {string[]} files - 要扫描的文件列表
|
||||
* @param {RegExp} keyUsageRegex - 匹配翻译键使用的正则表达式
|
||||
* @returns {Promise<Set<string>>} - 项目中使用的翻译键集合
|
||||
*/
|
||||
async scanUsedTranslationKeys(files, keyUsageRegex) {
|
||||
const usedKeys = new Set()
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await this.fileOperation.readFile(file)
|
||||
// 重置正则表达式的lastIndex,确保从头开始匹配
|
||||
keyUsageRegex.lastIndex = 0
|
||||
let match
|
||||
while ((match = keyUsageRegex.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
usedKeys.add(match[1].trim())
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[i18n插件] 扫描文件 ${file} 中使用的翻译键失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return usedKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* 从翻译文件中加载所有翻译键
|
||||
* @param {string} translationDir - 翻译文件目录
|
||||
* @param {string[]} languages - 语言列表
|
||||
* @returns {Promise<Map<string, Object>>} - 键到翻译对象的映射
|
||||
*/
|
||||
async loadAllTranslations(translationDir, languages) {
|
||||
const allTranslations = new Map()
|
||||
|
||||
for (const language of languages) {
|
||||
const filePath = path.join(translationDir, `${language}.json`)
|
||||
|
||||
try {
|
||||
if (await this.fileOperation.fileExists(filePath)) {
|
||||
const content = await this.fileOperation.readFile(filePath)
|
||||
const translations = JSON.parse(content)
|
||||
|
||||
// 将每个键加入到总映射中
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
if (!allTranslations.has(key)) {
|
||||
allTranslations.set(key, { key, translations: {} })
|
||||
}
|
||||
|
||||
const translationObj = allTranslations.get(key)
|
||||
translationObj.translations[language] = value
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[i18n插件] 加载翻译文件 ${filePath} 失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return allTranslations
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测未使用的翻译
|
||||
* @param {Set<string>} usedKeys - 使用的翻译键集合
|
||||
* @param {Map<string, Object>} allTranslations - 所有翻译
|
||||
* @returns {Set<string>} - 未使用的翻译键集合
|
||||
*/
|
||||
detectUnusedTranslations(usedKeys, allTranslations) {
|
||||
const unusedKeys = new Set()
|
||||
|
||||
for (const [key] of allTranslations.entries()) {
|
||||
if (!usedKeys.has(key)) {
|
||||
unusedKeys.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
return unusedKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* 从翻译文件中移除未使用的翻译
|
||||
* @param {Set<string>} unusedKeys - 未使用的翻译键集合
|
||||
* @param {string} translationDir - 翻译文件目录
|
||||
* @param {string[]} languages - 语言列表
|
||||
* @returns {Promise<number>} - 移除的翻译数量
|
||||
*/
|
||||
async removeUnusedTranslations(unusedKeys, translationDir, languages) {
|
||||
let removedCount = 0
|
||||
|
||||
for (const language of languages) {
|
||||
const filePath = path.join(translationDir, `${language}.json`)
|
||||
|
||||
try {
|
||||
if (await this.fileOperation.fileExists(filePath)) {
|
||||
const content = await this.fileOperation.readFile(filePath)
|
||||
const translations = JSON.parse(content)
|
||||
let hasChanges = false
|
||||
|
||||
// 移除未使用的翻译
|
||||
for (const key of unusedKeys) {
|
||||
if (key in translations) {
|
||||
delete translations[key]
|
||||
hasChanges = true
|
||||
|
||||
if (language === languages[0]) {
|
||||
// 只在处理第一种语言时计数,避免重复计数
|
||||
removedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有变更,更新文件
|
||||
if (hasChanges) {
|
||||
await this.fileOperation.modifyFile(filePath, JSON.stringify(translations, null, 2))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[i18n插件] 更新翻译文件 ${filePath} 失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return removedCount
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中移除未使用的翻译
|
||||
* @param {Map<string, Object>} allTranslations - 所有翻译
|
||||
* @param {Set<string>} unusedKeys - 未使用的翻译键集合
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async removeUnusedFromCache(allTranslations, unusedKeys) {
|
||||
// 构建需要保留的中文文本列表
|
||||
const validTexts = []
|
||||
|
||||
for (const [key, translationObj] of allTranslations.entries()) {
|
||||
if (!unusedKeys.has(key)) {
|
||||
// 如果有原始中文文本,添加到有效列表中
|
||||
if (translationObj.text) {
|
||||
validTexts.push(translationObj.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
await this.cacheManager.cleanCache(validTexts)
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行未使用翻译检查和清理
|
||||
* @param {Object} config - 配置对象
|
||||
* @param {string[]} files - 要扫描的文件列表
|
||||
* @returns {Promise<{removedCount: number}>} - 清理结果
|
||||
*/
|
||||
async cleanUnusedTranslations(config, files) {
|
||||
console.log(`[i18n插件] 开始检测未使用的翻译...`)
|
||||
|
||||
// 创建匹配翻译键使用的正则表达式: $t('key') 或 $t("key")
|
||||
const keyUsageRegex = new RegExp(/\$t\(['"](.+?)['"]\)/g)
|
||||
|
||||
// 扫描使用的翻译键
|
||||
const usedKeys = await this.scanUsedTranslationKeys(files, keyUsageRegex)
|
||||
console.log(`[i18n插件] 扫描到 ${usedKeys.size} 个使用中的翻译键`)
|
||||
|
||||
// 加载所有翻译
|
||||
const translationDir = path.join(config.outputPath, 'model')
|
||||
const allTranslations = await this.loadAllTranslations(translationDir, config.languages)
|
||||
console.log(`[i18n插件] 加载了 ${allTranslations.size} 个翻译键`)
|
||||
|
||||
// 检测未使用的翻译
|
||||
const unusedKeys = this.detectUnusedTranslations(usedKeys, allTranslations)
|
||||
console.log(`[i18n插件] 检测到 ${unusedKeys.size} 个未使用的翻译键`)
|
||||
|
||||
if (unusedKeys.size === 0) {
|
||||
console.log(`[i18n插件] 没有发现未使用的翻译,无需清理`)
|
||||
return { removedCount: 0 }
|
||||
}
|
||||
|
||||
// 移除未使用的翻译
|
||||
const removedCount = await this.removeUnusedTranslations(unusedKeys, translationDir, config.languages)
|
||||
|
||||
// 从缓存中移除未使用的翻译
|
||||
await this.removeUnusedFromCache(allTranslations, unusedKeys)
|
||||
|
||||
console.log(`[i18n插件] 已从翻译文件和缓存中移除 ${removedCount} 个未使用的翻译`)
|
||||
|
||||
return { removedCount }
|
||||
}
|
||||
}
|
||||
|
||||
export default UnusedTranslationDetector
|
||||
84
frontend/plugin/vite-plugin-i18n/src/cli/cleanup.js
Normal file
84
frontend/plugin/vite-plugin-i18n/src/cli/cleanup.js
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { vitePluginI18nAiTranslate } from '../index.js'
|
||||
import { FileOperation } from '../fileOperation/index.js'
|
||||
import path from 'path'
|
||||
import minimist from 'minimist'
|
||||
|
||||
/**
|
||||
* CLI工具:清理未使用的翻译
|
||||
*
|
||||
* 使用方法:
|
||||
* node cleanup.js --config=<配置文件路径>
|
||||
*/
|
||||
async function cleanup() {
|
||||
try {
|
||||
// 解析命令行参数
|
||||
const argv = minimist(process.argv.slice(2))
|
||||
|
||||
// 显示帮助信息
|
||||
if (argv.help || argv.h) {
|
||||
console.log(`
|
||||
未使用翻译清理工具
|
||||
|
||||
选项:
|
||||
--config, -c 指定配置文件路径 (默认: ./i18n.config.js)
|
||||
--verbose, -v 显示详细日志
|
||||
--help, -h 显示帮助信息
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// 获取配置文件路径
|
||||
const configPath = argv.config || argv.c || './i18n.config.js'
|
||||
const verbose = argv.verbose || argv.v || false
|
||||
|
||||
console.log(`[i18n清理工具] 正在加载配置文件: ${configPath}`)
|
||||
|
||||
// 动态导入配置文件
|
||||
let config
|
||||
try {
|
||||
const configModule = await import(path.resolve(process.cwd(), configPath))
|
||||
config = configModule.default
|
||||
} catch (error) {
|
||||
console.error(`[i18n清理工具] 加载配置文件失败: ${error.message}`)
|
||||
console.log('[i18n清理工具] 使用默认配置...')
|
||||
// 使用默认配置
|
||||
config = {}
|
||||
}
|
||||
|
||||
console.log('[i18n清理工具] 初始化插件...')
|
||||
const plugin = vitePluginI18nAiTranslate(config)
|
||||
|
||||
// 确保初始化缓存
|
||||
await plugin.configResolved()
|
||||
|
||||
// 获取要扫描的文件
|
||||
const fileOperation = new FileOperation()
|
||||
const globFiles = config.fileExtensions?.map((ext) => `**/*${ext}`) || [
|
||||
'**/*.js',
|
||||
'**/*.jsx',
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
'**/*.vue',
|
||||
]
|
||||
|
||||
console.log(`[i18n清理工具] 扫描文件中...`)
|
||||
const files = await fileOperation.scanFiles(globFiles, config.projectPath || process.cwd())
|
||||
|
||||
if (verbose) {
|
||||
console.log(`[i18n清理工具] 找到 ${files.length} 个文件需要扫描`)
|
||||
}
|
||||
|
||||
console.log('[i18n清理工具] 开始检查和清理未使用的翻译...')
|
||||
const result = await plugin.cleanupUnusedTranslations(files)
|
||||
|
||||
console.log(`[i18n清理工具] 完成! 已移除 ${result.removedCount} 个未使用的翻译`)
|
||||
} catch (error) {
|
||||
console.error(`[i18n清理工具] 发生错误:`, error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行清理
|
||||
cleanup()
|
||||
24
frontend/plugin/vite-plugin-i18n/src/config/config.js
Normal file
24
frontend/plugin/vite-plugin-i18n/src/config/config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
projectPath: './src',
|
||||
outputPath: './src/locales/',
|
||||
logPath: './logs',
|
||||
cachePath: './cache/translation_cache.json',
|
||||
apiKey: {
|
||||
zhipuAI: 'a160afdbea1644e68de5e5b014bea0f7.zZuSidvDSYOD7oJT',
|
||||
qianwenAI: 'sk-1b4f64a523814e33a6221bfccc676be6',
|
||||
api1: '',
|
||||
},
|
||||
languages: ['zhCN', 'zhTW', 'enUS', 'jaJP', 'koKR', 'ruRU', 'ptBR', 'frFR', 'esAR', 'arDZ'],
|
||||
concurrency: 100,
|
||||
exclude: ['node_modules', 'dist', 'build', 'locales', 'cache', 'logs'],
|
||||
templateRegex: '\\$t\\([\\\'"](?!t_)([^\\\'"]+)[\\\'"]',
|
||||
fileExtensions: ['.vue', '.js', '.ts', '.jsx', '.tsx'],
|
||||
interval: 5000,
|
||||
requestInterval: 100,
|
||||
maxRetries: 3,
|
||||
translateMethod: 'qianwenAI',
|
||||
cacheLifetime: 7,
|
||||
logRetention: 30,
|
||||
createFileExt: '.json',
|
||||
createEntryFileExt: '.ts',
|
||||
}
|
||||
128
frontend/plugin/vite-plugin-i18n/src/fileOperation/index.js
Normal file
128
frontend/plugin/vite-plugin-i18n/src/fileOperation/index.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import fastGlob from 'fast-glob'
|
||||
import config from '../config/config.js'
|
||||
|
||||
export class FileOperation {
|
||||
/**
|
||||
* 创建文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {string} content - 文件内容
|
||||
*/
|
||||
async createFile(filePath, content) {
|
||||
try {
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(filePath, content)
|
||||
} catch (error) {
|
||||
throw new Error(`创建文件失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {string} newContent - 新的文件内容
|
||||
*/
|
||||
async modifyFile(filePath, newContent) {
|
||||
try {
|
||||
await fs.writeFile(filePath, newContent)
|
||||
} catch (error) {
|
||||
throw new Error(`修改文件失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件
|
||||
* @param {string} sourcePath - 源文件路径
|
||||
* @param {string} destinationPath - 目标文件路径
|
||||
*/
|
||||
async copyFile(sourcePath, destinationPath) {
|
||||
try {
|
||||
const dir = path.dirname(destinationPath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.copyFile(sourcePath, destinationPath)
|
||||
} catch (error) {
|
||||
throw new Error(`复制文件失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Promise<string>} - 文件内容
|
||||
*/
|
||||
async readFile(filePath) {
|
||||
try {
|
||||
return await fs.readFile(filePath, 'utf8')
|
||||
} catch (error) {
|
||||
throw new Error(`读取文件失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
* @param {string} dirPath - 目录路径
|
||||
*/
|
||||
async createDirectory(dirPath) {
|
||||
try {
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`创建目录失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译文件
|
||||
* @param {string} outputPath - 输出路径
|
||||
* @param {Object} translations - 翻译结果
|
||||
* @param {string} language - 目标语言
|
||||
*/
|
||||
async generateTranslationFile(outputPath, translations, language) {
|
||||
try {
|
||||
const filePath = path.join(outputPath, `${language}${config.createFileExt}`)
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.mkdir(dir, { recursive: true }) // 确保目录存在
|
||||
let content = {}
|
||||
Object.assign(content, translations)
|
||||
content = `${JSON.stringify(content, null, 2)}`
|
||||
await fs.writeFile(filePath, content)
|
||||
} catch (error) {
|
||||
throw new Error(`生成翻译文件失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 glob 模式扫描文件内容
|
||||
* @param {string} pattern - glob 匹配模式
|
||||
* @param {string} basePath - 基础路径
|
||||
* @returns {Promise<Array<{path: string, content: string}>>} - 匹配文件的路径和内容
|
||||
*/
|
||||
async scanFiles(pattern, basePath = process.cwd()) {
|
||||
try {
|
||||
const files = await fastGlob(pattern, { cwd: basePath })
|
||||
const results = files.map((file) => {
|
||||
return path.join(basePath, file)
|
||||
})
|
||||
return results
|
||||
} catch (error) {
|
||||
throw new Error(`扫描文件失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FileOperation
|
||||
416
frontend/plugin/vite-plugin-i18n/src/index.js
Normal file
416
frontend/plugin/vite-plugin-i18n/src/index.js
Normal file
@@ -0,0 +1,416 @@
|
||||
import { CacheManager } from './cache/index.js'
|
||||
import { FileOperation } from './fileOperation/index.js'
|
||||
import { AIBatchAdapter } from './translation/adapter/aiBatchAdapter.js'
|
||||
import { TranslationState } from './stateManagement/index.js'
|
||||
import { Utils } from './utils/index.js'
|
||||
import { UnusedTranslationDetector } from './cleanUp/unusedTranslationDetector.js'
|
||||
import configFile from './config/config.js'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Vite i18n 自动翻译插件
|
||||
* @param {Object} options - 插件配置
|
||||
*/
|
||||
export function vitePluginI18nAiTranslate(options = {}) {
|
||||
const config = {
|
||||
...configFile,
|
||||
...options,
|
||||
templateRegex: new RegExp(configFile.templateRegex, 'g'), // Convert string to RegExp
|
||||
}
|
||||
|
||||
const cacheManager = new CacheManager(config.cachePath) // 缓存管理
|
||||
const fileOperation = new FileOperation() // 文件操作
|
||||
const translator = new AIBatchAdapter() // AI 批量翻译
|
||||
const translationState = new TranslationState() // 翻译状态管理
|
||||
const unusedDetector = new UnusedTranslationDetector(fileOperation, cacheManager) // 未使用翻译检测器
|
||||
|
||||
let watcher = null
|
||||
let outputDirCreated = false // 跟踪输出目录是否已创建
|
||||
let isProcessing = false // 跟踪是否正在进行批量处理
|
||||
|
||||
/**
|
||||
* 处理文件并提取中文文本
|
||||
* @param {string[]} files - 要处理的文件路径列表
|
||||
*/
|
||||
const processFiles = async (files) => {
|
||||
// 如果已经在处理中,则跳过
|
||||
if (isProcessing) {
|
||||
console.log(`[i18n插件] 已有处理正在进行中,跳过本次请求`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置处理标志
|
||||
isProcessing = true
|
||||
|
||||
console.log(`[i18n插件] 开始处理 ${files.length} 个文件...`)
|
||||
|
||||
// 第一步:扫描所有文件并提取中文文本
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await fileOperation.readFile(file) // 读取文件内容
|
||||
const chineseTexts = extractChineseTexts(content) // 提取中文文本
|
||||
// console.log(`[i18n插件] 提取 ${chineseTexts} 个中文文本`)
|
||||
translationState.recordFileProcessed(file, chineseTexts) // 记录处理的文件
|
||||
} catch (error) {
|
||||
console.error(`[i18n插件] 处理文件 ${file} 失败:`, error)
|
||||
}
|
||||
}
|
||||
// 第二步:对比缓存,确定需要翻译的内容
|
||||
await translateAndProcess()
|
||||
} finally {
|
||||
// 无论处理成功还是失败,都重置处理标志
|
||||
isProcessing = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译文本并处理结果
|
||||
*/
|
||||
const translateAndProcess = async () => {
|
||||
// 如果没有需要翻译的文本,直接返回
|
||||
const textsArray = Array.from(translationState.textsToTranslate)
|
||||
|
||||
// 获取缓存的翻译
|
||||
const { cached, uncached } = await cacheManager.getCachedTranslations(textsArray, config.languages)
|
||||
|
||||
// 记录缓存命中情况
|
||||
translationState.recordCacheHit(Object.keys(cached).length)
|
||||
translationState.recordCacheMiss(uncached.length)
|
||||
console.log(`[i18n插件] 缓存命中: ${Object.keys(cached).length} 个, 需要翻译: ${uncached.length} 个`)
|
||||
|
||||
// 所有翻译结果(包括缓存和新翻译)
|
||||
let allTranslations = { ...cached }
|
||||
|
||||
// 如果有未缓存的内容,进行翻译
|
||||
if (uncached.length > 0) {
|
||||
const translations = await translateTexts(uncached)
|
||||
// 更新缓存
|
||||
await cacheManager.updateCache(uncached, translations, config.languages)
|
||||
|
||||
// 合并新翻译结果
|
||||
translations.forEach((translation) => {
|
||||
allTranslations[translation.text] = translation
|
||||
})
|
||||
|
||||
// 记录新翻译的数量
|
||||
translationState.recordTranslated(translations.length)
|
||||
}
|
||||
|
||||
// 如果没有新的翻译内容或缓存,获取完整的缓存内容
|
||||
if (!Object.keys(allTranslations).length) {
|
||||
console.log(`[i18n插件] 没有新的翻译内容,使用完整缓存`)
|
||||
const cacheEntries = Array.from(cacheManager.cache.entries())
|
||||
cacheEntries.forEach(([text, data]) => {
|
||||
allTranslations[text] = {
|
||||
text,
|
||||
key: data.key,
|
||||
translations: data.translations,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 合并历史缓存和当前批次翻译内容
|
||||
const cacheEntries = Array.from(cacheManager.cache.entries())
|
||||
cacheEntries.forEach(([text, data]) => {
|
||||
if (!allTranslations[text]) {
|
||||
allTranslations[text] = {
|
||||
text,
|
||||
key: data.key,
|
||||
translations: data.translations,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 第三步:为每个中文文本生成唯一的键名,并建立映射关系
|
||||
for (const [text, translation] of Object.entries(allTranslations)) {
|
||||
translationState.setTextToKeyMapping(text, translation.key)
|
||||
}
|
||||
|
||||
// 第四步:一次性生成翻译文件(不再每次都检测目录)
|
||||
await generateTranslationFiles(allTranslations)
|
||||
|
||||
// 第五步:替换源文件中的中文文本为翻译键名
|
||||
await replaceSourceTexts()
|
||||
|
||||
// 完成并输出统计信息
|
||||
translationState.complete()
|
||||
outputStatistics()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取中文文本
|
||||
* @param {string} content - 文件内容
|
||||
* @returns {Set<string>} - 中文文本集合
|
||||
*/
|
||||
const extractChineseTexts = (content) => {
|
||||
const texts = new Set()
|
||||
// 重置正则表达式的lastIndex,确保从头开始匹配
|
||||
config.templateRegex.lastIndex = 0
|
||||
let match
|
||||
while ((match = config.templateRegex.exec(content)) !== null) {
|
||||
texts.add(match[1])
|
||||
console.log(`[i18n插件] 提取中文文本: ${match[1]}`)
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译文本
|
||||
* @param {string[]} texts - 待翻译的文本列表
|
||||
* @returns {Promise<Object[]>} - 翻译结果列表
|
||||
*/
|
||||
const translateTexts = async (texts) => {
|
||||
const results = []
|
||||
const chunks = chunkArray(texts, config.concurrency)
|
||||
|
||||
console.log(`[i18n插件] 开始翻译 ${texts.length} 个文本,分为 ${chunks.length} 批处理`)
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i]
|
||||
console.log(`[i18n插件] 正在处理第 ${i + 1}/${chunks.length} 批 (${chunk.length} 个文本)`)
|
||||
|
||||
const promises = chunk.map((text, index) => {
|
||||
return translator.translate(text, config.languages, config.maxRetries, index)
|
||||
})
|
||||
|
||||
const chunkResults = await Promise.all(promises)
|
||||
results.push(...chunkResults)
|
||||
|
||||
// 等待请求间隔
|
||||
if (config.requestInterval > 0 && i < chunks.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, config.requestInterval))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译文件
|
||||
* @param {Object} translations - 翻译结果
|
||||
*/
|
||||
const generateTranslationFiles = async (translations) => {
|
||||
// 确保输出目录存在(仅检查一次)
|
||||
if (!outputDirCreated) {
|
||||
await fileOperation.createDirectory(path.join(config.outputPath, 'model'))
|
||||
outputDirCreated = true
|
||||
}
|
||||
|
||||
console.log(`[i18n插件] 正在生成 ${config.languages.length} 个语言的翻译文件`)
|
||||
|
||||
// 构建每种语言的翻译结构
|
||||
const languageTranslations = {}
|
||||
|
||||
// 初始化每种语言的翻译对象
|
||||
for (const language of config.languages) {
|
||||
languageTranslations[language] = {}
|
||||
}
|
||||
|
||||
console.log(translations, Object.entries(translations).length)
|
||||
// 构建翻译键值对
|
||||
for (const [text, data] of Object.entries(translations)) {
|
||||
// 生成翻译键名
|
||||
const key = translationState.textToKeyMap.get(text) || Utils.renderTranslateName(text)
|
||||
|
||||
console.log(`[i18n插件] 生成翻译键名: ${key} -> ${text}`)
|
||||
// 为每种语言添加翻译
|
||||
for (const language of config.languages) {
|
||||
languageTranslations[language][key] = data.translations[language]
|
||||
}
|
||||
}
|
||||
// console.log(languageTranslations)
|
||||
// 一次性写入每种语言的翻译文件
|
||||
const writePromises = config.languages.map((language) =>
|
||||
fileOperation.generateTranslationFile(
|
||||
path.join(config.outputPath, 'model'),
|
||||
languageTranslations[language],
|
||||
language,
|
||||
),
|
||||
)
|
||||
await Promise.all(writePromises)
|
||||
console.log(`[i18n插件] 翻译文件生成完成`)
|
||||
// 创建入口文件
|
||||
await createI18nEntryFile()
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换源文件中的中文文本为翻译键名
|
||||
*/
|
||||
const replaceSourceTexts = async () => {
|
||||
// 获取所有需要更新的文件
|
||||
const filesToUpdate = translationState.getFilesToUpdate()
|
||||
|
||||
console.log(`[i18n插件] 正在替换 ${filesToUpdate.size} 个文件中的中文文本`)
|
||||
|
||||
// 处理每个需要更新的文件
|
||||
for (const [filePath, replacements] of filesToUpdate.entries()) {
|
||||
try {
|
||||
// 读取文件内容
|
||||
let content = await fileOperation.readFile(filePath)
|
||||
|
||||
// 获取文件相对于项目的命名空间
|
||||
// const namespace = Utils.getNamespace(filePath, config.projectPath);
|
||||
|
||||
// 替换每个中文文本为$t('键名')
|
||||
for (const [text, baseKey] of replacements.entries()) {
|
||||
// 在替换时为每个文件中的键添加命名空间前缀
|
||||
// const key = namespace ? `${namespace}.${baseKey}` : baseKey;
|
||||
// 创建正则表达式,匹配$t('中文文本')或$t("中文文本")
|
||||
const regex = new RegExp(`\\$t\\(['"]${escapeRegExp(text)}['"]`, 'g')
|
||||
content = content.replace(regex, `$t('${baseKey}'`)
|
||||
}
|
||||
|
||||
// 写入更新后的文件内容
|
||||
await fileOperation.modifyFile(filePath, content)
|
||||
} catch (error) {
|
||||
console.error(`[i18n插件] 替换文件 ${filePath} 内容失败:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建i18n入口文件
|
||||
*/
|
||||
const createI18nEntryFile = async () => {
|
||||
try {
|
||||
// 创建i18n入口文件内容
|
||||
const entryFileContent = `// 自动生成的i18n入口文件
|
||||
// 自动生成的i18n入口文件
|
||||
import { useLocale } from '@baota/i18n'
|
||||
import zhCN from './model/zhCN${config.createFileExt}'
|
||||
import enUS from './model/enUS${config.createFileExt}'
|
||||
|
||||
// 使用 i18n 插件
|
||||
export const { i18n, $t, locale, localeOptions } = useLocale(
|
||||
{
|
||||
messages: { zhCN, enUS },
|
||||
locale: 'zhCN',
|
||||
fileExt: 'json'
|
||||
},
|
||||
import.meta.glob([\`./model/*${config.createFileExt}\`], {
|
||||
eager: false,
|
||||
}),
|
||||
)
|
||||
|
||||
`
|
||||
|
||||
// 写入i18n入口文件
|
||||
const entryFilePath = path.join(config.outputPath, `index${config.createEntryFileExt}`)
|
||||
await fileOperation.createFile(entryFilePath, entryFileContent)
|
||||
console.log(`[i18n插件] 已创建i18n入口文件: ${entryFilePath}`)
|
||||
} catch (error) {
|
||||
console.error(`[i18n插件] 创建i18n入口文件失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出翻译统计信息
|
||||
*/
|
||||
const outputStatistics = () => {
|
||||
const summary = translationState.getSummary()
|
||||
console.log('\n======= i18n翻译插件执行统计 =======')
|
||||
console.log(`总耗时: ${summary.duration}`)
|
||||
console.log(`处理文件数: ${summary.filesProcessed}`)
|
||||
console.log(`包含中文文本的文件数: ${summary.filesWithChineseText}`)
|
||||
console.log(`唯一中文文本数: ${summary.uniqueChineseTexts}`)
|
||||
console.log(`命中缓存: ${summary.cacheHits} 条`)
|
||||
console.log(`新翻译: ${summary.translatedTexts} 条`)
|
||||
console.log(`缓存命中率: ${summary.cacheHitRate}`)
|
||||
console.log('===================================\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组分块
|
||||
* @param {Array} array - 待分块的数组
|
||||
* @param {number} size - 块大小
|
||||
* @returns {Array[]} - 分块后的数组
|
||||
*/
|
||||
const chunkArray = (array, size) => {
|
||||
const chunks = []
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunks.push(array.slice(i, i + size))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义正则表达式特殊字符
|
||||
* @param {string} string - 需要转义的字符串
|
||||
* @returns {string} - 转义后的字符串
|
||||
*/
|
||||
const escapeRegExp = (string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理未使用的翻译
|
||||
* @param {string[]} files - 要扫描的文件列表
|
||||
* @returns {Promise<{removedCount: number}>} - 清理结果
|
||||
*/
|
||||
const cleanupUnusedTranslations = async (files) => {
|
||||
if (isProcessing) {
|
||||
console.log(`[i18n插件] 已有处理正在进行中,跳过未使用翻译清理`)
|
||||
return { removedCount: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
isProcessing = true
|
||||
// 执行未使用翻译检查和清理
|
||||
const result = await unusedDetector.cleanUnusedTranslations(config, files)
|
||||
return result
|
||||
} finally {
|
||||
isProcessing = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-i18n-ai-translate',
|
||||
|
||||
// 解析配置时的钩子
|
||||
async configResolved() {
|
||||
// 初始化缓存
|
||||
await cacheManager.initCache()
|
||||
|
||||
// 确保输出目录存在(仅初始化一次)
|
||||
await fileOperation.createDirectory(path.join(config.outputPath, 'model'))
|
||||
outputDirCreated = true
|
||||
},
|
||||
|
||||
// 配置服务器时的钩子
|
||||
async configureServer(server) {
|
||||
// 生成规则
|
||||
const globFiles = config.fileExtensions.map((ext) => `**/*${ext}`)
|
||||
|
||||
// 获取所有文件
|
||||
const files = await fileOperation.scanFiles(globFiles, config.projectPath)
|
||||
|
||||
// 批量处理所有文件
|
||||
await processFiles(files)
|
||||
|
||||
// 设置文件监听
|
||||
// watcher = server.watcher
|
||||
// watcher.on('change', async (file) => {
|
||||
// // 只有在未处理状态且文件扩展名匹配时才处理变更
|
||||
// // 排除指定目录
|
||||
// if (config.exclude.some((item) => file.includes(item))) return
|
||||
// if (!isProcessing && config.fileExtensions.some((ext) => file.endsWith(ext))) {
|
||||
// // console.log(`[i18n插件] 检测到文件变更: ${file}`);
|
||||
// await processFiles([file])
|
||||
// }
|
||||
// })
|
||||
},
|
||||
|
||||
// 关闭打包时的钩子
|
||||
async closeBundle() {
|
||||
if (watcher) {
|
||||
watcher.close()
|
||||
}
|
||||
},
|
||||
|
||||
// 导出额外功能
|
||||
cleanupUnusedTranslations,
|
||||
}
|
||||
}
|
||||
|
||||
export default vitePluginI18nAiTranslate
|
||||
95
frontend/plugin/vite-plugin-i18n/src/logManagement/index.js
Normal file
95
frontend/plugin/vite-plugin-i18n/src/logManagement/index.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { Utils } from '../utils/index.js'
|
||||
|
||||
export class LogManager {
|
||||
constructor(options = {}) {
|
||||
const { logPath = './logs', errorLogFile = 'error.log', infoLogFile = 'info.log' } = options
|
||||
|
||||
this.logPath = logPath
|
||||
this.errorLogFile = path.join(logPath, errorLogFile)
|
||||
this.infoLogFile = path.join(logPath, infoLogFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化日志目录
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
await fs.mkdir(this.logPath, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error('初始化日志目录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
* @param {Error} error - 错误对象
|
||||
*/
|
||||
async logError(error) {
|
||||
try {
|
||||
const formattedError = Utils.formatError(error)
|
||||
const logEntry = `[${formattedError.timestamp}] ERROR: ${formattedError.message}\n${formattedError.stack}\n\n`
|
||||
await fs.appendFile(this.errorLogFile, logEntry)
|
||||
} catch (err) {
|
||||
console.error('写入错误日志失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
* @param {string} message - 日志信息
|
||||
*/
|
||||
async logInfo(message) {
|
||||
try {
|
||||
const timestamp = new Date().toISOString()
|
||||
const logEntry = `[${timestamp}] INFO: ${message}\n`
|
||||
await fs.appendFile(this.infoLogFile, logEntry)
|
||||
} catch (error) {
|
||||
console.error('写入信息日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
* @param {number} days - 保留天数
|
||||
*/
|
||||
async cleanLogs(days) {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const files = await fs.readdir(this.logPath)
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.logPath, file)
|
||||
const stats = await fs.stat(filePath)
|
||||
const age = (now - stats.mtimeMs) / (1000 * 60 * 60 * 24)
|
||||
|
||||
if (age > days) {
|
||||
await fs.unlink(filePath)
|
||||
await this.logInfo(`已删除过期日志文件: ${file}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清理日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志内容
|
||||
* @param {string} logType - 日志类型 ('error' | 'info')
|
||||
* @param {number} lines - 返回的行数
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async getLogs(logType, lines) {
|
||||
try {
|
||||
const logFile = logType === 'error' ? this.errorLogFile : this.infoLogFile
|
||||
const content = await fs.readFile(logFile, 'utf8')
|
||||
return content.split('\n').slice(-lines)
|
||||
} catch (error) {
|
||||
console.error('读取日志失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LogManager
|
||||
317
frontend/plugin/vite-plugin-i18n/src/stateManagement/index.js
Normal file
317
frontend/plugin/vite-plugin-i18n/src/stateManagement/index.js
Normal file
@@ -0,0 +1,317 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export class StateManager {
|
||||
constructor(options = {}) {
|
||||
const { statePath = './state', stateFile = 'plugin-state.json' } = options
|
||||
|
||||
this.statePath = statePath
|
||||
this.stateFile = path.join(statePath, stateFile)
|
||||
this.state = {
|
||||
lastUpdate: null,
|
||||
processedFiles: new Set(),
|
||||
pendingTranslations: new Set(),
|
||||
failedTranslations: new Map(),
|
||||
statistics: {
|
||||
totalProcessed: 0,
|
||||
totalSuccess: 0,
|
||||
totalFailed: 0,
|
||||
cacheHits: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化状态
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
await fs.mkdir(this.statePath, { recursive: true })
|
||||
if (await this.fileExists(this.stateFile)) {
|
||||
const data = await fs.readFile(this.stateFile, 'utf8')
|
||||
const savedState = JSON.parse(data)
|
||||
// 恢复集合和映射
|
||||
this.state = {
|
||||
...savedState,
|
||||
processedFiles: new Set(savedState.processedFiles),
|
||||
pendingTranslations: new Set(savedState.pendingTranslations),
|
||||
failedTranslations: new Map(savedState.failedTranslations),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存状态
|
||||
*/
|
||||
async save() {
|
||||
try {
|
||||
const serializedState = {
|
||||
...this.state,
|
||||
lastUpdate: new Date().toISOString(),
|
||||
processedFiles: Array.from(this.state.processedFiles),
|
||||
pendingTranslations: Array.from(this.state.pendingTranslations),
|
||||
failedTranslations: Array.from(this.state.failedTranslations),
|
||||
}
|
||||
await fs.writeFile(this.stateFile, JSON.stringify(serializedState, null, 2))
|
||||
} catch (error) {
|
||||
console.error('保存状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
* @param {Object} newState - 新的状态
|
||||
*/
|
||||
async updateState(newState) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...newState,
|
||||
lastUpdate: new Date().toISOString(),
|
||||
}
|
||||
await this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加已处理文件
|
||||
* @param {string} filePath - 文件路径
|
||||
*/
|
||||
async addProcessedFile(filePath) {
|
||||
this.state.processedFiles.add(filePath)
|
||||
this.state.statistics.totalProcessed++
|
||||
await this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加待处理翻译
|
||||
* @param {string} text - 待翻译文本
|
||||
*/
|
||||
async addPendingTranslation(text) {
|
||||
this.state.pendingTranslations.add(text)
|
||||
await this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加失败的翻译
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {Error} error - 错误信息
|
||||
*/
|
||||
async addFailedTranslation(text, error) {
|
||||
this.state.failedTranslations.set(text, {
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
this.state.statistics.totalFailed++
|
||||
await this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录翻译成功
|
||||
* @param {string} text - 翻译文本
|
||||
*/
|
||||
async recordTranslationSuccess(text) {
|
||||
this.state.pendingTranslations.delete(text)
|
||||
this.state.failedTranslations.delete(text)
|
||||
this.state.statistics.totalSuccess++
|
||||
await this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存命中
|
||||
*/
|
||||
async recordCacheHit() {
|
||||
this.state.statistics.cacheHits++
|
||||
await this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
* @returns {Object} - 当前状态
|
||||
*/
|
||||
getState() {
|
||||
return this.state
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* @returns {Object} - 统计信息
|
||||
*/
|
||||
getStatistics() {
|
||||
return this.state.statistics
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
async reset() {
|
||||
this.state = {
|
||||
lastUpdate: null,
|
||||
processedFiles: new Set(),
|
||||
pendingTranslations: new Set(),
|
||||
failedTranslations: new Map(),
|
||||
statistics: {
|
||||
totalProcessed: 0,
|
||||
totalSuccess: 0,
|
||||
totalFailed: 0,
|
||||
cacheHits: 0,
|
||||
},
|
||||
}
|
||||
await this.save()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译状态管理
|
||||
* 用于跟踪翻译进度和统计信息
|
||||
*/
|
||||
export class TranslationState {
|
||||
constructor() {
|
||||
// 文件处理统计
|
||||
this.filesProcessed = 0
|
||||
this.filesWithChineseText = 0
|
||||
|
||||
// 翻译统计
|
||||
this.textsToTranslate = new Set() // 所有需要翻译的中文文本
|
||||
this.translatedTexts = 0 // 已翻译的中文文本数量
|
||||
this.cacheHits = 0 // 缓存命中次数
|
||||
this.cacheMisses = 0 // 缓存未命中次数
|
||||
|
||||
// 中文文本到源文件的映射
|
||||
this.textToFiles = new Map() // 记录每个中文文本出现在哪些文件中
|
||||
this.fileTexts = new Map() // 记录每个文件包含哪些中文文本
|
||||
|
||||
// 翻译键名映射
|
||||
this.textToKeyMap = new Map() // 中文文本到翻译键名的映射
|
||||
|
||||
// 待处理的文件队列
|
||||
this.pendingFiles = []
|
||||
|
||||
// 性能指标
|
||||
this.startTime = Date.now()
|
||||
this.endTime = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件处理
|
||||
* @param {string} filePath - 处理的文件路径
|
||||
* @param {Set<string>} chineseTexts - 文件中提取的中文文本
|
||||
*/
|
||||
recordFileProcessed(filePath, chineseTexts) {
|
||||
this.filesProcessed++
|
||||
|
||||
if (chineseTexts.size > 0) {
|
||||
this.filesWithChineseText++
|
||||
this.fileTexts.set(filePath, new Set(chineseTexts))
|
||||
|
||||
// 更新中文文本到文件的映射
|
||||
chineseTexts.forEach((text) => {
|
||||
this.textsToTranslate.add(text)
|
||||
|
||||
if (!this.textToFiles.has(text)) {
|
||||
this.textToFiles.set(text, new Set())
|
||||
}
|
||||
this.textToFiles.get(text).add(filePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存命中
|
||||
* @param {number} hitCount - 命中缓存的数量
|
||||
*/
|
||||
recordCacheHit(hitCount) {
|
||||
this.cacheHits += hitCount
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存未命中
|
||||
* @param {number} missCount - 未命中缓存的数量
|
||||
*/
|
||||
recordCacheMiss(missCount) {
|
||||
this.cacheMisses += missCount
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录翻译完成
|
||||
* @param {number} count - 翻译完成的数量
|
||||
*/
|
||||
recordTranslated(count) {
|
||||
this.translatedTexts += count
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文本到键名的映射
|
||||
* @param {string} text - 中文文本
|
||||
* @param {string} key - 生成的翻译键名
|
||||
*/
|
||||
setTextToKeyMapping(text, key) {
|
||||
this.textToKeyMap.set(text, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成翻译过程
|
||||
*/
|
||||
complete() {
|
||||
this.endTime = Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取翻译状态摘要
|
||||
* @returns {Object} - 翻译状态摘要
|
||||
*/
|
||||
getSummary() {
|
||||
const duration = (this.endTime || Date.now()) - this.startTime
|
||||
|
||||
return {
|
||||
duration: `${(duration / 1000).toFixed(2)}秒`,
|
||||
filesProcessed: this.filesProcessed,
|
||||
filesWithChineseText: this.filesWithChineseText,
|
||||
uniqueChineseTexts: this.textsToTranslate.size,
|
||||
translatedTexts: this.translatedTexts,
|
||||
cacheHits: this.cacheHits,
|
||||
cacheMisses: this.cacheMisses,
|
||||
cacheHitRate:
|
||||
this.textsToTranslate.size > 0 ? `${((this.cacheHits / this.textsToTranslate.size) * 100).toFixed(2)}%` : '0%',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有需要更新的文件及其对应的文本替换映射
|
||||
* @returns {Map<string, Map<string, string>>} - 文件路径到文本替换映射的映射
|
||||
*/
|
||||
getFilesToUpdate() {
|
||||
const filesToUpdate = new Map()
|
||||
|
||||
this.fileTexts.forEach((texts, filePath) => {
|
||||
const fileReplacements = new Map()
|
||||
texts.forEach((text) => {
|
||||
const key = this.textToKeyMap.get(text)
|
||||
if (key) {
|
||||
fileReplacements.set(text, key)
|
||||
}
|
||||
})
|
||||
|
||||
if (fileReplacements.size > 0) {
|
||||
filesToUpdate.set(filePath, fileReplacements)
|
||||
}
|
||||
})
|
||||
|
||||
return filesToUpdate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { TranslationAdapter } from './index.js'
|
||||
import { ZhipuAITranslator } from '../ai/zhipuAI.js'
|
||||
import { QianwenAITranslator } from '../ai/qianwenAI.js'
|
||||
import { DeepSeekAITranslator } from '../ai/deepseekAI.js'
|
||||
import config from '../../config/config.js'
|
||||
|
||||
/**
|
||||
* AI批量翻译适配器 - 用于处理大规模AI翻译服务
|
||||
*/
|
||||
export class AIBatchAdapter extends TranslationAdapter {
|
||||
constructor() {
|
||||
super()
|
||||
this.translator = new DeepSeekAITranslator(config.apiKey[config.translateMethod])
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染翻译名称
|
||||
* @returns {Promise<string>} 生成的唯一翻译名称
|
||||
*/
|
||||
renderTranslateName(index) {
|
||||
const timestamp = Date.now()
|
||||
return `t_${index}_${timestamp}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行AI批量翻译 - 包含错误重试机制
|
||||
* @param {string} text - 待翻译的文本内容
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @param {number} maxRetries - 最大重试次数
|
||||
* @param {number} index - 翻译名称索引
|
||||
* @returns {Promise<{text: string, translations: Record<string, string}>} 翻译结果对象
|
||||
* @throws {Error} 当所有重试都失败时抛出错误
|
||||
*/
|
||||
async translate(text, languages, maxRetries, index) {
|
||||
let lastError = null
|
||||
let retryCount = 0
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
const result = await this.translator.translate({
|
||||
text,
|
||||
languages,
|
||||
})
|
||||
const key = this.renderTranslateName(index)
|
||||
return {
|
||||
text,
|
||||
key,
|
||||
translations: result.translations,
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
retryCount++
|
||||
// 如果还有重试机会,等待一段时间后重试
|
||||
if (retryCount <= maxRetries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount))
|
||||
continue
|
||||
}
|
||||
|
||||
throw new Error(`AI批量翻译失败(已重试${retryCount}次) - ${lastError.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI翻译服务支持的语言列表
|
||||
* @returns {string[]} 支持的语言代码列表
|
||||
*/
|
||||
getSupportedLanguages() {
|
||||
return this.translator.getSupportedLanguages()
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证API密钥是否有效
|
||||
* @param {string} apiKey - 待验证的API密钥
|
||||
* @returns {Promise<boolean>} 密钥是否有效
|
||||
*/
|
||||
async validateApiKey(apiKey) {
|
||||
try {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 翻译适配器基类 - 用于统一不同翻译服务的接口实现
|
||||
*/
|
||||
export class TranslationAdapter {
|
||||
constructor() {
|
||||
if (this.constructor === TranslationAdapter) {
|
||||
throw new Error('翻译适配器:抽象类不能被直接实例化')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行翻译 - 将给定文本翻译为目标语言
|
||||
* @param {string} text - 待翻译的文本内容
|
||||
* @param {string} apiKey - 翻译服务的API密钥
|
||||
* @param {string[]} languages - 目标语言代码列表,如 ['enUS', 'jaJP']
|
||||
* @param {number} maxRetries - 翻译失败时的最大重试次数
|
||||
* @returns {Promise<{text: string, translations: Record<string, string}>} 翻译结果对象
|
||||
* @throws {Error} 当翻译失败且超过重试次数时抛出错误
|
||||
*/
|
||||
async translate(text, apiKey, languages, maxRetries) {
|
||||
throw new Error('翻译适配器:translate 方法必须在子类中实现')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前适配器支持的语言列表
|
||||
* @returns {string[]} 支持的语言代码列表
|
||||
*/
|
||||
getSupportedLanguages() {
|
||||
throw new Error('翻译适配器:getSupportedLanguages 方法必须在子类中实现')
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定语言是否被当前适配器支持
|
||||
* @param {string} language - 需要检查的语言代码
|
||||
* @returns {boolean} 是否支持该语言
|
||||
*/
|
||||
isLanguageSupported(language) {
|
||||
return this.getSupportedLanguages().includes(language)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { TranslationAdapter } from './index.js'
|
||||
import { translate as traditionalApiTranslate } from '../traditional/api1.js'
|
||||
|
||||
/**
|
||||
* 传统API翻译适配器 - 用于适配常规REST API类型的翻译服务
|
||||
*/
|
||||
export class TraditionalApiAdapter extends TranslationAdapter {
|
||||
constructor(apiModule) {
|
||||
super()
|
||||
if (!apiModule?.translate || typeof apiModule.translate !== 'function') {
|
||||
throw new Error('传统API适配器:无效的API模块,必须提供translate方法')
|
||||
}
|
||||
this.apiModule = apiModule
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行翻译请求 - 将数据转换为传统API格式并处理响应
|
||||
* @param {string} text - 待翻译的文本内容
|
||||
* @param {string} apiKey - API密钥
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @param {number} maxRetries - 最大重试次数
|
||||
* @returns {Promise<{text: string, translations: Record<string, string>}>} 标准化的翻译结果
|
||||
* @throws {Error} 当翻译失败或语言不支持时抛出错误
|
||||
*/
|
||||
async translate(text, apiKey, languages, maxRetries) {
|
||||
// 检查所有目标语言是否支持
|
||||
for (const lang of languages) {
|
||||
if (!this.isLanguageSupported(lang)) {
|
||||
throw new Error(`传统API适配器:不支持的目标语言 "${lang}"`)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为API期望的请求格式
|
||||
const requestData = {
|
||||
text,
|
||||
apiKey,
|
||||
targetLanguages: languages,
|
||||
retryCount: maxRetries,
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.apiModule.translate(requestData)
|
||||
return {
|
||||
text,
|
||||
translations: result.translations,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`传统API适配器:翻译失败 - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API支持的语言列表
|
||||
* @returns {string[]} 支持的语言代码数组
|
||||
*/
|
||||
getSupportedLanguages() {
|
||||
return this.apiModule.getSupportedLanguages?.() || []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import axios from 'axios'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { Utils } from '../../utils/index.js'
|
||||
|
||||
export class DeepSeekAITranslator {
|
||||
constructor(apiKey) {
|
||||
this.apiKey = 'sk-cdhgecffemwndfqfiohtzhzkqxkjtstqflnoeoazqxzhfswd'
|
||||
this.baseURL = 'https://api.siliconflow.cn/v1/chat/completions'
|
||||
this.model = 'deepseek-ai/DeepSeek-V3'
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译提示词
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {string}
|
||||
*/
|
||||
generatePrompt(text, languages) {
|
||||
const targetLanguages = languages
|
||||
.map((code) => {
|
||||
const { language, region } = Utils.parseLanguageCode(code)
|
||||
return `${language}${region}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return `你是专业的翻译,根据用户提供的翻译文本,生成不同的翻译结果,请将以下文本翻译成${targetLanguages}多种语言,\r\n
|
||||
如果翻译文本包含{riskNum}包裹的字符,保持{}和包裹的字符,以及翻译文本本身是英文的时候,直接跳过翻译,输出原文按当前格式返回即可,\r\n
|
||||
其他的内容继续翻译,返回JSON格式,注意要严格按照JSON格式返回,返回前先检查是否符合JSON格式,字符串内部不能有换行,输出格式示例:\n{
|
||||
"zhCN": "中文",
|
||||
"enUS": "English"
|
||||
}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用智谱AI进行翻译
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {Promise<{text: string, translations: Object}>}
|
||||
*/
|
||||
async translate({ text, languages }) {
|
||||
try {
|
||||
const translations = {}
|
||||
// 判断当前翻译内容是否为纯英文,如果是,则直接返回原文
|
||||
if (/^[\x00-\x7F]*$/.test(text)) {
|
||||
for (const code of languages) {
|
||||
translations[code] = text
|
||||
}
|
||||
} else {
|
||||
const prompt = this.generatePrompt(text, languages)
|
||||
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
url: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: prompt,
|
||||
},
|
||||
{ role: 'user', content: `翻译文本:${text}` },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.data || !response.data.choices || !response.data.choices[0]) {
|
||||
throw new Error('无效的API响应')
|
||||
}
|
||||
|
||||
// 解析智谱AI翻译结果
|
||||
const rawTranslations = this.parseTranslations(response.data.choices[0].message.content)
|
||||
|
||||
// console.log(rawTranslations, text)
|
||||
// 转换语言代码格式
|
||||
for (const [code, value] of Object.entries(rawTranslations)) {
|
||||
translations[code] = value
|
||||
}
|
||||
}
|
||||
return {
|
||||
text,
|
||||
translations: Utils.formatTranslations(translations),
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`DeepSeek-V3翻译失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析智谱AI翻译结果,转换为标准格式
|
||||
* @param {string} text - 待翻译文本
|
||||
* @returns {Object} - 标准格式的翻译结果
|
||||
*/
|
||||
parseTranslations(text) {
|
||||
text = text.replace('```json\n', '').replace('```', '')
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查API密钥是否有效
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async validateApiKey() {
|
||||
try {
|
||||
await axios.get(`${this.baseURL}/validate`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DeepSeekAITranslator
|
||||
145
frontend/plugin/vite-plugin-i18n/src/translation/ai/qianwenAI.js
Normal file
145
frontend/plugin/vite-plugin-i18n/src/translation/ai/qianwenAI.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import axios from 'axios'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { Utils } from '../../utils/index.js'
|
||||
|
||||
export class QianwenAITranslator {
|
||||
constructor(apiKey) {
|
||||
this.apiKey = apiKey
|
||||
this.baseURL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
|
||||
this.model = 'qwen-max'
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译提示词
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {string}
|
||||
*/
|
||||
generatePrompt(text, languages) {
|
||||
const targetLanguages = languages
|
||||
.map((code) => {
|
||||
const { language, region } = Utils.parseLanguageCode(code)
|
||||
return `${language}${region}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return `你是专业的翻译,根据用户提供的翻译文本,生成不同的翻译结果,请将以下文本翻译成${targetLanguages}多种语言,\r\n
|
||||
如果翻译文本包含{riskNum}包裹的字符,保持{}和包裹的字符,以及翻译文本本身是英文的时候,直接跳过翻译,输出原文按当前格式返回即可,\r\n
|
||||
其他的内容继续翻译,返回JSON格式,注意要严格按照JSON格式返回,返回前先检查是否符合JSON格式,字符串内部不能有换行,输出格式示例:\n{
|
||||
"zhCN": "中文",
|
||||
"enUS": "English"
|
||||
}`
|
||||
}
|
||||
|
||||
// 生成智谱AI API所需的JWT token
|
||||
async getToken() {
|
||||
const [id, secret] = this.apiKey.split('.')
|
||||
const header = { alg: 'HS256', sign_type: 'SIGN' }
|
||||
const payload = {
|
||||
api_key: id,
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
}
|
||||
const headerBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(header))).replace(
|
||||
/=/g,
|
||||
'',
|
||||
)
|
||||
const payloadBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(payload))).replace(
|
||||
/=/g,
|
||||
'',
|
||||
)
|
||||
|
||||
const signature = CryptoJS.enc.Base64.stringify(
|
||||
CryptoJS.HmacSHA256(`${headerBase64}.${payloadBase64}`, secret),
|
||||
).replace(/=/g, '')
|
||||
return `${headerBase64}.${payloadBase64}.${signature}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用智谱AI进行翻译
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {Promise<{text: string, translations: Object}>}
|
||||
*/
|
||||
async translate({ text, languages }) {
|
||||
try {
|
||||
const translations = {}
|
||||
// 判断当前翻译内容是否为纯英文,如果是,则直接返回原文
|
||||
if (/^[\x00-\x7F]*$/.test(text)) {
|
||||
for (const code of languages) {
|
||||
translations[code] = text
|
||||
}
|
||||
} else {
|
||||
const prompt = this.generatePrompt(text, languages)
|
||||
// const token = await this.getToken()
|
||||
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
url: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: prompt,
|
||||
},
|
||||
{ role: 'user', content: `翻译文本:${text}` },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.data || !response.data.choices || !response.data.choices[0]) {
|
||||
throw new Error('无效的API响应')
|
||||
}
|
||||
|
||||
// 解析智谱AI翻译结果
|
||||
const rawTranslations = this.parseTranslations(response.data.choices[0].message.content)
|
||||
|
||||
// console.log(rawTranslations, text)
|
||||
// 转换语言代码格式
|
||||
for (const [code, value] of Object.entries(rawTranslations)) {
|
||||
translations[code] = value
|
||||
}
|
||||
}
|
||||
return {
|
||||
text,
|
||||
translations: Utils.formatTranslations(translations),
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`千问AI翻译失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析智谱AI翻译结果,转换为标准格式
|
||||
* @param {string} text - 待翻译文本
|
||||
* @returns {Object} - 标准格式的翻译结果
|
||||
*/
|
||||
parseTranslations(text) {
|
||||
text = text.replace('```json\n', '').replace('```', '')
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查API密钥是否有效
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async validateApiKey() {
|
||||
try {
|
||||
await axios.get(`${this.baseURL}/validate`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default QianwenAITranslator
|
||||
145
frontend/plugin/vite-plugin-i18n/src/translation/ai/zhipuAI.js
Normal file
145
frontend/plugin/vite-plugin-i18n/src/translation/ai/zhipuAI.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import axios from 'axios'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { Utils } from '../../utils/index.js'
|
||||
|
||||
export class ZhipuAITranslator {
|
||||
constructor(apiKey) {
|
||||
this.apiKey = apiKey
|
||||
this.baseURL = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'
|
||||
this.model = 'glm-4-flash'
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译提示词
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {string}
|
||||
*/
|
||||
generatePrompt(text, languages) {
|
||||
const targetLanguages = languages
|
||||
.map((code) => {
|
||||
const { language, region } = Utils.parseLanguageCode(code)
|
||||
return `${language}${region}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return `你是专业的翻译,根据用户提供的翻译文本,生成不同的翻译结果,请将以下文本翻译成${targetLanguages}多种语言,\r\n
|
||||
如果翻译文本包含{riskNum}包裹的字符,保持{}和包裹的字符,以及翻译文本本身是英文的时候,直接跳过翻译,输出原文按当前格式返回即可,\r\n
|
||||
其他的内容继续翻译,返回JSON格式,注意要严格按照JSON格式返回,返回前先检查是否符合JSON格式,字符串内部不能有换行,输出格式示例:\n{
|
||||
"zhCN": "中文",
|
||||
"enUS": "English"
|
||||
}`
|
||||
}
|
||||
|
||||
// 生成智谱AI API所需的JWT token
|
||||
async getToken() {
|
||||
const [id, secret] = this.apiKey.split('.')
|
||||
const header = { alg: 'HS256', sign_type: 'SIGN' }
|
||||
const payload = {
|
||||
api_key: id,
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
}
|
||||
const headerBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(header))).replace(
|
||||
/=/g,
|
||||
'',
|
||||
)
|
||||
const payloadBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(payload))).replace(
|
||||
/=/g,
|
||||
'',
|
||||
)
|
||||
|
||||
const signature = CryptoJS.enc.Base64.stringify(
|
||||
CryptoJS.HmacSHA256(`${headerBase64}.${payloadBase64}`, secret),
|
||||
).replace(/=/g, '')
|
||||
return `${headerBase64}.${payloadBase64}.${signature}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用智谱AI进行翻译
|
||||
* @param {string} text - 待翻译文本
|
||||
* @param {string[]} languages - 目标语言列表
|
||||
* @returns {Promise<{text: string, translations: Object}>}
|
||||
*/
|
||||
async translate({ text, languages }) {
|
||||
try {
|
||||
const translations = {}
|
||||
// 判断当前翻译内容是否为纯英文,如果是,则直接返回原文
|
||||
if (/^[\x00-\x7F]*$/.test(text)) {
|
||||
for (const code of languages) {
|
||||
translations[code] = text
|
||||
}
|
||||
} else {
|
||||
const prompt = this.generatePrompt(text, languages)
|
||||
const token = await this.getToken()
|
||||
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
url: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: prompt,
|
||||
},
|
||||
{ role: 'user', content: `翻译文本:${text}` },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.data || !response.data.choices || !response.data.choices[0]) {
|
||||
throw new Error('无效的API响应')
|
||||
}
|
||||
|
||||
// 解析智谱AI翻译结果
|
||||
const rawTranslations = this.parseTranslations(response.data.choices[0].message.content)
|
||||
|
||||
// console.log(rawTranslations, text)
|
||||
// 转换语言代码格式
|
||||
for (const [code, value] of Object.entries(rawTranslations)) {
|
||||
translations[code] = value
|
||||
}
|
||||
}
|
||||
return {
|
||||
text,
|
||||
translations: Utils.formatTranslations(translations),
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`智谱AI翻译失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析智谱AI翻译结果,转换为标准格式
|
||||
* @param {string} text - 待翻译文本
|
||||
* @returns {Object} - 标准格式的翻译结果
|
||||
*/
|
||||
parseTranslations(text) {
|
||||
text = text.replace('```json\n', '').replace('```', '')
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查API密钥是否有效
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async validateApiKey() {
|
||||
try {
|
||||
await axios.get(`${this.baseURL}/validate`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ZhipuAITranslator
|
||||
@@ -0,0 +1,85 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export class TraditionalApi1 {
|
||||
constructor() {
|
||||
this.baseURL = 'https://api.example.com/translate'
|
||||
this.supportedLanguages = ['zhCN', 'zhTW', 'enUS', 'jaJP', 'koKR']
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行翻译
|
||||
* @param {Object} requestData - 请求数据
|
||||
* @returns {Promise<{translations: Object}>}
|
||||
*/
|
||||
async translate(requestData) {
|
||||
const { text, apiKey, languages } = requestData
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
this.baseURL,
|
||||
{
|
||||
q: text,
|
||||
target: languages.map((lang) => this.formatLanguageCode(lang)),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.data || !response.data.translations) {
|
||||
throw new Error('无效的API响应')
|
||||
}
|
||||
|
||||
// 转换响应格式
|
||||
const translations = {}
|
||||
response.data.translations.forEach((translation, index) => {
|
||||
translations[languages[index]] = translation.text
|
||||
})
|
||||
|
||||
return { translations }
|
||||
} catch (error) {
|
||||
throw new Error(`API请求失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证API密钥
|
||||
* @param {string} apiKey - API密钥
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async validateApiKey(apiKey) {
|
||||
try {
|
||||
await axios.get(`${this.baseURL}/validate`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getSupportedLanguages() {
|
||||
return this.supportedLanguages
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化语言代码
|
||||
* @param {string} code - 语言代码
|
||||
* @returns {string}
|
||||
*/
|
||||
formatLanguageCode(code) {
|
||||
return `${code.slice(0, 2).toLowerCase()}-${code.slice(2).toUpperCase()}`
|
||||
}
|
||||
}
|
||||
|
||||
export const api1 = new TraditionalApi1()
|
||||
export default api1
|
||||
196
frontend/plugin/vite-plugin-i18n/src/utils/index.js
Normal file
196
frontend/plugin/vite-plugin-i18n/src/utils/index.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
/**
|
||||
* 工具函数集合
|
||||
*/
|
||||
export class Utils {
|
||||
/**
|
||||
* 将数组分块
|
||||
* @param {Array} array - 待分块的数组
|
||||
* @param {number} size - 块大小
|
||||
* @returns {Array[]} - 分块后的数组
|
||||
*/
|
||||
static chunkArray(array, size) {
|
||||
return _.chunk(array, size)
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟执行
|
||||
* @param {number} ms - 延迟时间(毫秒)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为中文字符
|
||||
* @param {string} text - 待检查的文本
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isChineseText(text) {
|
||||
return /[\u4e00-\u9fa5]/.test(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本中提取中文内容
|
||||
* @param {string} content - 文件内容
|
||||
* @param {RegExp} templateRegex - 模板变量正则表达式
|
||||
* @returns {Set<string>} - 中文内容集合
|
||||
*/
|
||||
static extractChineseTexts(content, templateRegex) {
|
||||
const texts = new Set()
|
||||
let match
|
||||
while ((match = templateRegex.exec(content)) !== null) {
|
||||
if (this.isChineseText(match[1])) {
|
||||
texts.add(match[1])
|
||||
}
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化翻译结果
|
||||
* @param {Object} translations - 翻译结果
|
||||
* @returns {Object} - 格式化后的翻译结果
|
||||
*/
|
||||
static formatTranslations(translations) {
|
||||
const formatted = {}
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
formatted[key] = typeof value === 'string' ? value.trim() : value
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译键名
|
||||
* @param {string} text - 原始中文文本
|
||||
* @param {string} namespace - 命名空间,通常是文件路径
|
||||
* @returns {string} - 生成的键名
|
||||
*/
|
||||
static renderTranslateName() {
|
||||
const time = Date.now()
|
||||
return `t_${time}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的哈希函数,用于为文本生成唯一标识
|
||||
* @param {string} str - 输入字符串
|
||||
* @returns {string} - 哈希值(十六进制)
|
||||
*/
|
||||
static simpleHash(str) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + char
|
||||
hash = hash & hash // 转换为32位整数
|
||||
}
|
||||
return Math.abs(hash).toString(16).substring(0, 6)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对于项目源代码目录的路径
|
||||
* @param {string} filePath - 完整文件路径
|
||||
* @param {string} projectPath - 项目源代码根目录
|
||||
* @returns {string} - 相对路径,用作命名空间
|
||||
*/
|
||||
static getNamespace(filePath, projectPath) {
|
||||
// 移除项目路径前缀并转换为点分隔的路径
|
||||
const relativePath = filePath.replace(projectPath, '').replace(/^\/+/, '')
|
||||
// 移除文件扩展名,并将目录分隔符转为点
|
||||
return relativePath
|
||||
.replace(/\.[^/.]+$/, '')
|
||||
.split('/')
|
||||
.join('.')
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并翻译结果
|
||||
* @param {Object} target - 目标对象
|
||||
* @param {Object} source - 源对象
|
||||
* @returns {Object} - 合并后的对象
|
||||
*/
|
||||
static mergeTranslations(target, source) {
|
||||
return _.mergeWith(target, source, (objValue, srcValue) => {
|
||||
if (_.isString(objValue) && _.isString(srcValue)) {
|
||||
return srcValue // 使用新的翻译结果
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证语言代码
|
||||
* @param {string} code - 语言代码
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isValidLanguageCode(code) {
|
||||
const languageCodePattern = /^[a-z]{2}[A-Z]{2}$/
|
||||
return languageCodePattern.test(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置对象
|
||||
* @param {Object} config - 配置对象
|
||||
* @returns {string[]} - 错误信息数组
|
||||
*/
|
||||
static validateConfig(config) {
|
||||
const errors = []
|
||||
|
||||
if (!config.apiKey || typeof config.apiKey !== 'object') {
|
||||
errors.push('apiKey 必须是一个对象')
|
||||
}
|
||||
|
||||
if (config.languages && Array.isArray(config.languages)) {
|
||||
const invalidCodes = config.languages.filter((code) => !this.isValidLanguageCode(code))
|
||||
if (invalidCodes.length > 0) {
|
||||
errors.push(`无效的语言代码: ${invalidCodes.join(', ')}`)
|
||||
}
|
||||
} else {
|
||||
errors.push('languages 必须是一个数组')
|
||||
}
|
||||
|
||||
if (typeof config.concurrency !== 'number' || config.concurrency <= 0) {
|
||||
errors.push('concurrency 必须是一个正数')
|
||||
}
|
||||
|
||||
if (typeof config.interval !== 'number' || config.interval < 0) {
|
||||
errors.push('interval 必须是一个非负数')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一标识符
|
||||
* @returns {string}
|
||||
*/
|
||||
static generateId() {
|
||||
return _.uniqueId('translation_')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化错误信息
|
||||
* @param {Error} error - 错误对象
|
||||
* @returns {Object}
|
||||
*/
|
||||
static formatError(error) {
|
||||
return {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析语言代码
|
||||
* @param {string} code - 语言代码
|
||||
* @returns {Object}
|
||||
*/
|
||||
static parseLanguageCode(code) {
|
||||
const language = code.slice(0, 2).toLowerCase()
|
||||
const region = code.slice(2).toUpperCase()
|
||||
return { language, region }
|
||||
}
|
||||
}
|
||||
|
||||
export default Utils
|
||||
Reference in New Issue
Block a user