【新增】插件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,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