chore: src/modules/base/service/translate.ts

This commit is contained in:
2026-07-01 17:56:20 +08:00
parent ed688f7855
commit aa2b57366f
+588
View File
@@ -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<BaseSysMenuEntity>;
@InjectEntityModel(DictInfoEntity)
dictInfoEntity: Repository<DictInfoEntity>;
@InjectEntityModel(DictTypeEntity)
dictTypeEntity: Repository<DictTypeEntity>;
// 基础路径
basePath: string;
@App()
app: IMidwayApplication;
@Inject()
logger: ILogger;
@Config('cool.i18n')
config: {
/** 是否开启 */
enable: boolean;
/** 语言 */
languages: string[];
/** 翻译服务 */
serviceUrl?: string;
};
menuMap: Record<string, string> = {};
msgMap: Record<string, string> = {};
commMap: Record<string, string> = {};
// 添加字典映射
dictMap: Record<string, string> = {};
/**
* 检查是否存在锁文件
*/
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<string, string>
) {
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`);
}
}