【新增】插件git同步模块,用于同步项目内容,加速项目开发

【调整】前端暗色问题
This commit is contained in:
chudong
2025-05-14 16:50:56 +08:00
parent dc43da936b
commit e6947ec5c4
215 changed files with 19918 additions and 9710 deletions

View 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

View File

@@ -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

View 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()

View 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',
}

View 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

View 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

View 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

View 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
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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?.() || []
}
}

View File

@@ -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

View 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

View 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

View File

@@ -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

View 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