329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
/**
|
||
* NAC知识引擎 - 多语言AI翻译服务
|
||
*
|
||
* 支持七种语言:zh(中文)、en(英文)、ar(阿拉伯文)、ja(日文)、ko(韩文)、fr(法文)、ru(俄文)
|
||
*
|
||
* 使用OpenAI兼容接口(无Manus依赖,中国大陆可正常访问)
|
||
* 通过环境变量配置:
|
||
* NAC_AI_API_URL - AI接口地址(如 https://api.openai.com 或国内兼容端点)
|
||
* NAC_AI_API_KEY - AI接口密钥
|
||
* NAC_AI_MODEL - 模型名称(默认 gpt-3.5-turbo)
|
||
*
|
||
* 阿拉伯语(ar)特殊处理:
|
||
* - 文本方向:RTL(从右到左)
|
||
* - 字符集:Unicode阿拉伯字符块(U+0600-U+06FF)
|
||
* - 前端需设置 dir="rtl" 属性
|
||
*/
|
||
|
||
export const SUPPORTED_LANGUAGES = ["zh", "en", "ar", "ja", "ko", "fr", "ru"] as const;
|
||
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||
|
||
export const LANGUAGE_NAMES: Record<SupportedLanguage, string> = {
|
||
zh: "中文(简体)",
|
||
en: "English",
|
||
ar: "العربية",
|
||
ja: "日本語",
|
||
ko: "한국어",
|
||
fr: "Français",
|
||
ru: "Русский",
|
||
};
|
||
|
||
/**
|
||
* RTL语言列表(从右到左书写)
|
||
* 前端渲染时需要为这些语言设置 dir="rtl"
|
||
*/
|
||
export const RTL_LANGUAGES: SupportedLanguage[] = ["ar"];
|
||
|
||
/**
|
||
* 判断语言是否为RTL
|
||
*/
|
||
export function isRTL(lang: SupportedLanguage): boolean {
|
||
return RTL_LANGUAGES.includes(lang);
|
||
}
|
||
|
||
/**
|
||
* 多语言内容结构
|
||
*/
|
||
export interface MultiLangContent {
|
||
zh?: string;
|
||
en?: string;
|
||
ar?: string;
|
||
ja?: string;
|
||
ko?: string;
|
||
fr?: string;
|
||
ru?: string;
|
||
}
|
||
|
||
// ─── AI翻译接口(OpenAI兼容,无Manus依赖)────────────────────────
|
||
|
||
/**
|
||
* 检查AI翻译服务是否已配置
|
||
*/
|
||
export function isAiTranslationConfigured(): boolean {
|
||
return !!(process.env.NAC_AI_API_URL && process.env.NAC_AI_API_KEY);
|
||
}
|
||
|
||
/**
|
||
* 调用AI接口进行翻译(OpenAI兼容格式)
|
||
*/
|
||
async function callAiTranslation(
|
||
systemPrompt: string,
|
||
userPrompt: string
|
||
): Promise<string> {
|
||
const apiUrl = process.env.NAC_AI_API_URL;
|
||
const apiKey = process.env.NAC_AI_API_KEY;
|
||
const model = process.env.NAC_AI_MODEL || "gpt-3.5-turbo";
|
||
|
||
if (!apiUrl || !apiKey) {
|
||
throw new Error(
|
||
"[AI翻译] 未配置AI接口。请在 .env 文件中设置 NAC_AI_API_URL 和 NAC_AI_API_KEY。\n" +
|
||
"支持任何OpenAI兼容接口(如 OpenAI、Azure OpenAI、国内大模型等)。"
|
||
);
|
||
}
|
||
|
||
const endpoint = `${apiUrl.replace(/\/$/, "")}/v1/chat/completions`;
|
||
|
||
const response = await fetch(endpoint, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
authorization: `Bearer ${apiKey}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model,
|
||
messages: [
|
||
{ role: "system", content: systemPrompt },
|
||
{ role: "user", content: userPrompt },
|
||
],
|
||
max_tokens: 2048,
|
||
temperature: 0.3, // 低温度保证翻译一致性
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
throw new Error(`AI接口调用失败: ${response.status} ${response.statusText} – ${errorText}`);
|
||
}
|
||
|
||
const result = await response.json() as {
|
||
choices: Array<{ message: { content: string } }>;
|
||
};
|
||
|
||
const content = result.choices?.[0]?.message?.content;
|
||
return (typeof content === "string" ? content : "").trim();
|
||
}
|
||
|
||
/**
|
||
* 使用AI翻译单个文本到指定语言
|
||
*
|
||
* 阿拉伯语特殊说明:
|
||
* - 翻译结果为标准现代阿拉伯语(MSA)
|
||
* - 返回的文本为Unicode阿拉伯字符,前端需设置 dir="rtl"
|
||
* - 专有名词(NAC、RWA等)保持英文不翻译
|
||
*/
|
||
export async function translateText(
|
||
sourceText: string,
|
||
sourceLang: SupportedLanguage,
|
||
targetLang: SupportedLanguage,
|
||
context?: string
|
||
): Promise<string> {
|
||
if (sourceLang === targetLang) return sourceText;
|
||
if (!sourceText.trim()) return sourceText;
|
||
|
||
const contextHint = context
|
||
? `\n\n背景信息(仅供参考,不要翻译):${context}`
|
||
: "";
|
||
|
||
// 阿拉伯语特殊提示
|
||
const arabicHint = targetLang === "ar"
|
||
? "\n特别说明:翻译成标准现代阿拉伯语(MSA),使用Unicode阿拉伯字符,文本方向为从右到左(RTL)。"
|
||
: "";
|
||
|
||
const systemPrompt = `你是NAC(NewAssetChain)公链的专业法律合规翻译专家。
|
||
NAC是一条专注于RWA(真实世界资产)的原生公链,使用Charter智能合约语言、NVM虚拟机、CBPP共识协议。
|
||
你的任务是将合规规则文本从${LANGUAGE_NAMES[sourceLang]}翻译成${LANGUAGE_NAMES[targetLang]}。
|
||
要求:
|
||
1. 保持法律术语的准确性和专业性
|
||
2. 保留专有名词(如:NAC、RWA、Charter、NVM、CBPP、CSNP、CNNL、ACC-20、GNACS、XTZH)不翻译
|
||
3. 保留机构名称(如:SEC、SFC、MAS、ESMA、DFSA、DLD)不翻译
|
||
4. 只返回翻译结果,不要添加任何解释或注释${arabicHint}`;
|
||
|
||
const userPrompt = `请将以下文本翻译成${LANGUAGE_NAMES[targetLang]}:\n\n${sourceText}${contextHint}`;
|
||
|
||
try {
|
||
const translated = await callAiTranslation(systemPrompt, userPrompt);
|
||
return translated || sourceText;
|
||
} catch (error) {
|
||
console.error(`[Translation] 翻译到 ${targetLang} 失败:`, (error as Error).message);
|
||
return sourceText; // 降级返回原文
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为合规规则生成完整的七语言翻译
|
||
* @param ruleName 规则名称(源语言)
|
||
* @param description 规则描述(源语言)
|
||
* @param sourceLang 源语言
|
||
* @param existingTranslations 已有的翻译(跳过已有的)
|
||
*/
|
||
export async function generateRuleTranslations(
|
||
ruleName: string,
|
||
description: string,
|
||
sourceLang: SupportedLanguage = "zh",
|
||
existingTranslations?: { ruleNameI18n?: MultiLangContent; descriptionI18n?: MultiLangContent }
|
||
): Promise<{ ruleNameI18n: MultiLangContent; descriptionI18n: MultiLangContent }> {
|
||
const ruleNameI18n: MultiLangContent = { ...(existingTranslations?.ruleNameI18n || {}) };
|
||
const descriptionI18n: MultiLangContent = { ...(existingTranslations?.descriptionI18n || {}) };
|
||
|
||
// 设置源语言内容
|
||
ruleNameI18n[sourceLang] = ruleName;
|
||
descriptionI18n[sourceLang] = description;
|
||
|
||
// 并行翻译所有缺失的语言
|
||
const targetLangs = SUPPORTED_LANGUAGES.filter(
|
||
(lang) => lang !== sourceLang && !ruleNameI18n[lang]
|
||
);
|
||
|
||
await Promise.all(
|
||
targetLangs.map(async (targetLang) => {
|
||
const [translatedName, translatedDesc] = await Promise.all([
|
||
translateText(ruleName, sourceLang, targetLang, `这是一条${description.slice(0, 50)}...的合规规则`),
|
||
translateText(description, sourceLang, targetLang),
|
||
]);
|
||
ruleNameI18n[targetLang] = translatedName;
|
||
descriptionI18n[targetLang] = translatedDesc;
|
||
})
|
||
);
|
||
|
||
return { ruleNameI18n, descriptionI18n };
|
||
}
|
||
|
||
/**
|
||
* 批量迁移现有规则,为其生成多语言翻译
|
||
*/
|
||
export async function migrateRuleToMultiLang(rule: {
|
||
ruleName: string;
|
||
description: string;
|
||
ruleNameI18n?: MultiLangContent;
|
||
descriptionI18n?: MultiLangContent;
|
||
}): Promise<{ ruleNameI18n: MultiLangContent; descriptionI18n: MultiLangContent }> {
|
||
const sourceLang: SupportedLanguage = "zh";
|
||
|
||
return generateRuleTranslations(
|
||
rule.ruleName,
|
||
rule.description,
|
||
sourceLang,
|
||
{ ruleNameI18n: rule.ruleNameI18n, descriptionI18n: rule.descriptionI18n }
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 阿拉伯语RTL测试用例
|
||
* 用于验证阿拉伯语翻译质量和RTL布局
|
||
*/
|
||
export const ARABIC_RTL_TEST_CASES = [
|
||
{
|
||
id: "ar-rtl-001",
|
||
sourceLang: "zh" as SupportedLanguage,
|
||
targetLang: "ar" as SupportedLanguage,
|
||
sourceText: "不动产登记证要求",
|
||
expectedContains: ["عقار", "تسجيل", "شهادة"], // 应包含房产/登记/证书相关词汇
|
||
isRTL: true,
|
||
description: "房产登记证要求(阿拉伯语RTL测试)",
|
||
},
|
||
{
|
||
id: "ar-rtl-002",
|
||
sourceLang: "en" as SupportedLanguage,
|
||
targetLang: "ar" as SupportedLanguage,
|
||
sourceText: "RWA asset compliance verification",
|
||
expectedContains: ["امتثال", "أصول", "التحقق"], // 应包含合规/资产/验证相关词汇
|
||
isRTL: true,
|
||
description: "RWA资产合规验证(阿拉伯语RTL测试)",
|
||
},
|
||
{
|
||
id: "ar-rtl-003",
|
||
sourceLang: "zh" as SupportedLanguage,
|
||
targetLang: "ar" as SupportedLanguage,
|
||
sourceText: "NAC公链智能合约审批流程",
|
||
expectedContains: ["NAC", "عقد", "موافقة"], // NAC不翻译,包含合约/审批相关词汇
|
||
isRTL: true,
|
||
description: "NAC专有名词保留测试(阿拉伯语RTL)",
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 执行阿拉伯语RTL专项测试
|
||
* 返回测试结果报告
|
||
*/
|
||
export async function runArabicRTLTests(): Promise<{
|
||
passed: number;
|
||
failed: number;
|
||
results: Array<{
|
||
id: string;
|
||
description: string;
|
||
passed: boolean;
|
||
translated: string;
|
||
isRTL: boolean;
|
||
issues: string[];
|
||
}>;
|
||
}> {
|
||
if (!isAiTranslationConfigured()) {
|
||
return {
|
||
passed: 0,
|
||
failed: ARABIC_RTL_TEST_CASES.length,
|
||
results: ARABIC_RTL_TEST_CASES.map(tc => ({
|
||
id: tc.id,
|
||
description: tc.description,
|
||
passed: false,
|
||
translated: "",
|
||
isRTL: true,
|
||
issues: ["AI翻译服务未配置,请设置 NAC_AI_API_URL 和 NAC_AI_API_KEY"],
|
||
})),
|
||
};
|
||
}
|
||
|
||
const results = await Promise.all(
|
||
ARABIC_RTL_TEST_CASES.map(async (tc) => {
|
||
const issues: string[] = [];
|
||
let translated = "";
|
||
|
||
try {
|
||
translated = await translateText(tc.sourceText, tc.sourceLang, tc.targetLang);
|
||
|
||
// 检查是否包含阿拉伯字符(Unicode范围 U+0600-U+06FF)
|
||
const hasArabicChars = /[\u0600-\u06FF]/.test(translated);
|
||
if (!hasArabicChars) {
|
||
issues.push("翻译结果不包含阿拉伯字符");
|
||
}
|
||
|
||
// 检查专有名词是否保留(NAC不应被翻译)
|
||
if (tc.sourceText.includes("NAC") && !translated.includes("NAC")) {
|
||
issues.push("专有名词NAC被错误翻译,应保留英文");
|
||
}
|
||
|
||
// 检查是否为空
|
||
if (!translated.trim()) {
|
||
issues.push("翻译结果为空");
|
||
}
|
||
|
||
} catch (error) {
|
||
issues.push(`翻译失败: ${(error as Error).message}`);
|
||
}
|
||
|
||
return {
|
||
id: tc.id,
|
||
description: tc.description,
|
||
passed: issues.length === 0,
|
||
translated,
|
||
isRTL: tc.isRTL,
|
||
issues,
|
||
};
|
||
})
|
||
);
|
||
|
||
const passed = results.filter(r => r.passed).length;
|
||
const failed = results.filter(r => !r.passed).length;
|
||
|
||
return { passed, failed, results };
|
||
}
|