From aa2b57366f2e062c571796c50a97fc539ccf26e1 Mon Sep 17 00:00:00 2001 From: snowgitea Date: Wed, 1 Jul 2026 17:56:20 +0800 Subject: [PATCH] chore: src/modules/base/service/translate.ts --- src/modules/base/service/translate.ts | 588 ++++++++++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 src/modules/base/service/translate.ts diff --git a/src/modules/base/service/translate.ts b/src/modules/base/service/translate.ts new file mode 100644 index 0000000..84a616b --- /dev/null +++ b/src/modules/base/service/translate.ts @@ -0,0 +1,588 @@ +import { I18N } from '@cool-midway/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseSysMenuEntity } from '../entity/sys/menu'; +import { + App, + Config, + ILogger, + IMidwayApplication, + Inject, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/core'; +import * as path from 'path'; +import * as fs from 'fs'; +import axios from 'axios'; +import { DictInfoEntity } from '../../dict/entity/info'; +import { DictTypeEntity } from '../../dict/entity/type'; +/** + * 翻译服务 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class BaseTranslateService { + @InjectEntityModel(BaseSysMenuEntity) + baseSysMenuEntity: Repository; + + @InjectEntityModel(DictInfoEntity) + dictInfoEntity: Repository; + + @InjectEntityModel(DictTypeEntity) + dictTypeEntity: Repository; + + // 基础路径 + basePath: string; + + @App() + app: IMidwayApplication; + + @Inject() + logger: ILogger; + + @Config('cool.i18n') + config: { + /** 是否开启 */ + enable: boolean; + /** 语言 */ + languages: string[]; + /** 翻译服务 */ + serviceUrl?: string; + }; + + menuMap: Record = {}; + + msgMap: Record = {}; + + commMap: Record = {}; + + // 添加字典映射 + dictMap: Record = {}; + + /** + * 检查是否存在锁文件 + */ + private checkLockFile(type: 'menu' | 'msg' | 'comm'): boolean { + const lockFile = path.join(this.basePath, type, '.lock'); + return fs.existsSync(lockFile); + } + + /** + * 创建锁文件 + */ + private createLockFile(type: 'menu' | 'msg' | 'comm'): void { + const lockFile = path.join(this.basePath, type, '.lock'); + fs.writeFileSync(lockFile, new Date().toISOString()); + } + + /** + * 加载翻译文件到内存 + */ + async loadTranslations() { + if (!this.config?.enable) { + return; + } + if (!this.basePath) { + this.basePath = path.join(this.app.getBaseDir(), '..', 'src', 'locales'); + } + + // 清空现有映射 + this.menuMap = {}; + this.msgMap = {}; + this.dictMap = {}; + this.commMap = {}; + // 加载菜单翻译 + await this.loadTypeTranslations('menu', this.menuMap); + + // 加载消息翻译 + await this.loadTypeTranslations('msg', this.msgMap); + + // 加载通用消息翻译 + await this.loadTypeTranslations('comm', this.commMap); + + // 加载字典翻译 + await this.loadDictTranslations(); + } + + /** + * 加载指定类型的翻译 + * @param type 翻译类型 + * @param map 映射对象 + */ + private async loadTypeTranslations( + type: 'menu' | 'msg' | 'comm', + map: Record + ) { + const dirPath = path.join(this.basePath, type); + if (fs.existsSync(dirPath)) { + const files = fs.readdirSync(dirPath); + for (const file of files) { + if (file.endsWith('.json')) { + const language = file.replace('.json', ''); + const content = fs.readFileSync(path.join(dirPath, file), 'utf-8'); + const translations = JSON.parse(content); + for (const [key, value] of Object.entries(translations)) { + map[`${language}:${key}`] = value as string; + } + } + } + } + } + + /** + * 加载字典翻译 + */ + private async loadDictTranslations() { + const dictTypes = ['info', 'type']; + + for (const dictType of dictTypes) { + const dirPath = path.join(this.basePath, 'dict', dictType); + if (fs.existsSync(dirPath)) { + const files = fs.readdirSync(dirPath); + for (const file of files) { + if (file.endsWith('.json')) { + const language = file.replace('.json', ''); + const content = fs.readFileSync(path.join(dirPath, file), 'utf-8'); + const translations = JSON.parse(content); + for (const [key, value] of Object.entries(translations)) { + this.dictMap[`${language}:dict:${dictType}:${key}`] = + value as string; + } + } + } + } + } + } + + /** + * 更新翻译映射 + * @param type 类型 menu | msg + * @param language 语言 + */ + async updateTranslationMap(type: 'menu' | 'msg', language: string) { + const dirPath = path.join(this.basePath, type); + const file = path.join(dirPath, `${language}.json`); + + if (fs.existsSync(file)) { + const content = fs.readFileSync(file, 'utf-8'); + const translations = JSON.parse(content); + const map = type === 'menu' ? this.menuMap : this.msgMap; + + for (const [key, value] of Object.entries(translations)) { + map[`${language}:${key}`] = value as string; + } + } + } + + /** + * 翻译 + * @param type 类型 menu | msg | dict + * @param language 语言 + * @param text 原文 + * @returns 翻译后的文本 + */ + translate( + type: 'menu' | 'msg' | 'dict:info' | 'dict:type' | 'comm', + language: string, + text: string + ): string { + // 处理字典翻译 + if (type === 'dict:info' || type === 'dict:type') { + const key = `${language}:${type}:${text}`; + return this.dictMap[key] || text.split(':').pop() || text; + } + + // 处理菜单和消息翻译 + const map = type === 'menu' ? this.menuMap : this.msgMap; + const key = `${language}:${text}`; + return map[key] || text; + } + + /** + * 检查翻译 + */ + async check() { + if (this.config?.enable && this.app.getEnv() == 'local') { + this.basePath = path.join(this.app.getBaseDir(), '..', 'src', 'locales'); + const menuLockExists = this.checkLockFile('menu'); + const msgLockExists = this.checkLockFile('msg'); + const commLockExists = this.checkLockFile('comm'); + const dictLockExists = this.checkDictLockFile(); + + if ( + !menuLockExists || + !msgLockExists || + !dictLockExists || + !commLockExists + ) { + const tasks = []; + if (!msgLockExists) { + tasks.push(this.genBaseMsg()); + } + if (!menuLockExists) { + tasks.push(this.genBaseMenu()); + } + if (!dictLockExists) { + tasks.push(this.genBaseDict()); + } + if (!commLockExists) { + tasks.push(this.genCommMsg()); + } + // 启动旋转动画 + const spinner = ['|', '/', '-', '\\']; + let index = 0; + const interval = setInterval(() => { + process.stdout.write(`\r${spinner[index++]} i18n translate...`); + index %= spinner.length; + }, 200); + try { + await Promise.all(tasks); + } finally { + clearInterval(interval); + // 加载翻译文件到内存 + await this.loadTranslations(); + await this.loadDictTranslations(); + process.stdout.write('\r✅ i18n translate success!!!\n'); + } + } else { + this.logger.debug('Translation lock files exist, skipping translation'); + // 直接加载翻译文件到内存 + await this.loadTranslations(); + await this.loadDictTranslations(); + } + } + } + + /** + * 检查字典锁文件 + */ + private checkDictLockFile(): boolean { + const lockFile = path.join(this.basePath, 'dict', '.lock'); + return fs.existsSync(lockFile); + } + + /** + * 创建字典锁文件 + */ + private createDictLockFile(): void { + const lockFile = path.join(this.basePath, 'dict', '.lock'); + fs.writeFileSync(lockFile, new Date().toISOString()); + } + + /** + * 生成基础字典 + */ + async genBaseDict() { + try { + // 检查是否存在锁文件 + if (this.checkDictLockFile()) { + this.logger.debug('Dictionary lock file exists, skipping translation'); + return; + } + + const infos = await this.dictInfoEntity.find(); + const types = await this.dictTypeEntity.find(); + + // 确保目录存在 + const infoDir = path.join(this.basePath, 'dict', 'info'); + const typeDir = path.join(this.basePath, 'dict', 'type'); + fs.mkdirSync(infoDir, { recursive: true }); + fs.mkdirSync(typeDir, { recursive: true }); + + // 生成中文基础文件 + const infoContent = {}; + const typeContent = {}; + + for (const info of infos) { + infoContent[info.name] = info.name; + } + for (const type of types) { + typeContent[type.name] = type.name; + } + + const infoFile = path.join(infoDir, 'zh-cn.json'); + const typeFile = path.join(typeDir, 'zh-cn.json'); + + const infoText = JSON.stringify(infoContent, null, 2); + const typeText = JSON.stringify(typeContent, null, 2); + + fs.writeFileSync(infoFile, infoText); + fs.writeFileSync(typeFile, typeText); + + this.logger.debug('Base dictionary files generated successfully'); + + // 翻译其他语言 + if (this.config?.enable && this.config.languages) { + const translatePromises = []; + + for (const language of this.config.languages) { + if (language !== 'zh-cn') { + // 翻译 info 字典 + translatePromises.push( + this.invokeTranslate(infoText, language, infoDir, 'dict') + ); + + // 翻译 type 字典 + translatePromises.push( + this.invokeTranslate(typeText, language, typeDir, 'dict') + ); + } + } + + await Promise.all(translatePromises); + this.logger.debug('Dictionary translations completed successfully'); + } + + // 创建锁文件 + this.createDictLockFile(); + + // 更新翻译映射 + await this.loadDictTranslations(); + } catch (error) { + this.logger.error('Failed to generate dictionary:', error); + throw error; + } + } + + /** + * 更新字典翻译映射 + * @param language 语言 + */ + async updateDictTranslationMap(language: string) { + const infoFile = path.join( + this.basePath, + 'dict', + 'info', + `${language}.json` + ); + const typeFile = path.join( + this.basePath, + 'dict', + 'type', + `${language}.json` + ); + + if (fs.existsSync(infoFile)) { + const content = fs.readFileSync(infoFile, 'utf-8'); + const translations = JSON.parse(content); + for (const [key, value] of Object.entries(translations)) { + this.dictMap[`${language}:dict:info:${key}`] = value as string; + } + } + + if (fs.existsSync(typeFile)) { + const content = fs.readFileSync(typeFile, 'utf-8'); + const translations = JSON.parse(content); + for (const [key, value] of Object.entries(translations)) { + this.dictMap[`${language}:dict:type:${key}`] = value as string; + } + } + } + + /** + * 生成基础菜单 + */ + async genBaseMenu() { + const menus = await this.baseSysMenuEntity.find(); + const file = path.join(this.basePath, 'menu', 'zh-cn.json'); + const content = {}; + for (const menu of menus) { + content[menu.name] = menu.name; + } + // 确保目录存在 + const msgDir = path.dirname(file); + if (!fs.existsSync(msgDir)) { + fs.mkdirSync(msgDir, { recursive: true }); + } + const text = JSON.stringify(content, null, 2); + fs.writeFileSync(file, text); + this.logger.debug('base menu generate success'); + const translatePromises = []; + for (const language of this.config.languages) { + if (language !== 'zh-cn') { + translatePromises.push( + this.invokeTranslate( + text, + language, + path.join(this.basePath, 'menu'), + 'menu' + ) + ); + } + } + await Promise.all(translatePromises); + this.createLockFile('menu'); + } + + /** + * 生成基础消息 + */ + async genBaseMsg() { + const file = path.join(this.basePath, 'msg', 'zh-cn.json'); + const scanPath = path.join(this.app.getBaseDir(), '..', 'src', 'modules'); + const messages = {}; + + // 递归扫描目录 + const scanDir = (dir: string) => { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + scanDir(fullPath); + } else if (file.endsWith('.ts')) { + const content = fs.readFileSync(fullPath, 'utf-8'); + const matches = content.match( + /throw new CoolCommException\((['"])(.*?)\1\)/g + ); + if (matches) { + matches.forEach(match => { + const message = match.match(/(['"])(.*?)\1/)[2]; + messages[message] = message; + }); + } + } + } + }; + + // 开始扫描 + scanDir(scanPath); + + // 确保目录存在 + const msgDir = path.dirname(file); + if (!fs.existsSync(msgDir)) { + fs.mkdirSync(msgDir, { recursive: true }); + } + + // 写入文件 + const text = JSON.stringify(messages, null, 2); + fs.writeFileSync(file, text); + this.logger.debug('base msg generate success'); + + const translatePromises = []; + for (const language of this.config.languages) { + if (language !== 'zh-cn') { + translatePromises.push( + this.invokeTranslate( + text, + language, + path.join(this.basePath, 'msg'), + 'msg' + ) + ); + } + } + await Promise.all(translatePromises); + this.createLockFile('msg'); + } + + /** + * 生成通用消息 + */ + async genCommMsg() { + const file = path.join(this.basePath, 'comm', 'zh-cn.json'); + const scanPath = path.join(this.app.getBaseDir(), '..', 'src', 'modules'); + const messages = {}; + + // 递归扫描目录 + const scanDir = (dir: string) => { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + scanDir(fullPath); + } else if (file.endsWith('.ts')) { + const content = fs.readFileSync(fullPath, 'utf-8'); + const matches = content.match( + /this.translate.comm\((['"])(.*?)\1\)/g + ); + if (matches) { + matches.forEach(match => { + const message = match.match(/(['"])(.*?)\1/)[2]; + messages[message] = message; + }); + } + } + } + }; + + // 开始扫描 + scanDir(scanPath); + + // 确保目录存在 + const msgDir = path.dirname(file); + if (!fs.existsSync(msgDir)) { + fs.mkdirSync(msgDir, { recursive: true }); + } + + // 写入文件 + const text = JSON.stringify(messages, null, 2); + fs.writeFileSync(file, text); + this.logger.debug('base comm generate success'); + + const translatePromises = []; + for (const language of this.config.languages) { + if (language !== 'zh-cn') { + translatePromises.push( + this.invokeTranslate( + text, + language, + path.join(this.basePath, 'comm'), + 'comm' + ) + ); + } + } + await Promise.all(translatePromises); + this.createLockFile('comm'); + } + + /** + * 通用消息翻译 + * @param text 文本 + * @returns 翻译后的文本对象,包含各语言的翻译 + */ + comm(text: string) { + const translations = {}; + for (const lang of this.config.languages) { + const langFile = path.join(this.basePath, 'comm', `${lang}.json`); + if (fs.existsSync(langFile)) { + const content = JSON.parse(fs.readFileSync(langFile, 'utf-8')); + translations[lang] = content[text] || text; + } + } + return translations; + } + + /** + * 调用翻译 + * @param text 文本 + * @param language 语言 + * @param dirPath 目录 + * @param type 类型 + * @returns + */ + async invokeTranslate( + text: string, + language: string, + dirPath: string, + type: 'menu' | 'msg' | 'dict' | 'comm' = 'msg' + ) { + this.logger.debug(`${type} ${language} translate start`); + const response = await axios.post(I18N.DEFAULT_SERVICE_URL, { + label: 'i18n-node', + params: { + text, + language, + }, + stream: false, + }); + const file = path.join(dirPath, `${language}.json`); + fs.writeFileSync(file, response.data.data.result.data); + this.logger.debug(`${type} ${language} translate success`); + } +}