chore: src/modules/base/service/translate.ts
This commit is contained in:
@@ -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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user