Add translation features

This commit is contained in:
2025-07-06 23:40:14 +08:00
parent a2a332e735
commit 7c2318a13f
19 changed files with 2449 additions and 530 deletions

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,71 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* LanguageInfo 语言信息结构体
*/
export class LanguageInfo {
/**
* 语言代码
*/
"Code": string;
/**
* 语言名称
*/
"Name": string;
/** Creates a new LanguageInfo instance. */
constructor($$source: Partial<LanguageInfo> = {}) {
if (!("Code" in $$source)) {
this["Code"] = "";
}
if (!("Name" in $$source)) {
this["Name"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new LanguageInfo instance from a string or object.
*/
static createFrom($$source: any = {}): LanguageInfo {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new LanguageInfo($$parsedSource as Partial<LanguageInfo>);
}
}
/**
* TranslatorType 翻译器类型
*/
export enum TranslatorType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* GoogleTranslatorType 谷歌翻译器
*/
GoogleTranslatorType = "google",
/**
* BingTranslatorType 必应翻译器
*/
BingTranslatorType = "bing",
/**
* YoudaoTranslatorType 有道翻译器
*/
YoudaoTranslatorType = "youdao",
/**
* DeeplTranslatorType DeepL翻译器
*/
DeeplTranslatorType = "deepl",
};

View File

@@ -380,6 +380,11 @@ export enum ExtensionID {
*/
ExtensionCheckbox = "checkbox",
/**
* 划词翻译
*/
ExtensionTranslator = "translator",
/**
* UI增强扩展
* 小地图

View File

@@ -10,6 +10,10 @@
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as translator$0 from "../common/translator/models.js";
/**
* GetAvailableTranslators 获取所有可用翻译器类型
* @returns {[]string} 翻译器类型列表
@@ -24,34 +28,33 @@ export function GetAvailableTranslators(): Promise<string[]> & { cancel(): void
}
/**
* SetActiveTranslator 设置活跃翻译器
* GetStandardLanguageCode 获取标准化的语言代码
*/
export function GetStandardLanguageCode(translatorType: translator$0.TranslatorType, languageCode: string): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(1158131995, translatorType, languageCode) as any;
return $resultPromise;
}
/**
* GetTranslatorLanguages 获取翻译器的语言列表
* @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
* @returns {map[string]string} 语言代码到名称的映射
* @returns {error} 可能的错误
*/
export function SetActiveTranslator(translatorType: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(620567821, translatorType) as any;
return $resultPromise;
export function GetTranslatorLanguages(translatorType: translator$0.TranslatorType): Promise<{ [_: string]: translator$0.LanguageInfo }> & { cancel(): void } {
let $resultPromise = $Call.ByID(3976114458, translatorType) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* SetTimeout 设置翻译超时时间
* @param {int} seconds - 超时秒数
* IsLanguageSupported 检查指定的语言代码是否受支持
*/
export function SetTimeout(seconds: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3787687384, seconds) as any;
return $resultPromise;
}
/**
* Translate 使用当前活跃翻译器进行翻译
* @param {string} text - 待翻译文本
* @param {string} from - 源语言代码 (如 "en", "zh", "auto")
* @param {string} to - 目标语言代码 (如 "en", "zh")
* @returns {string} 翻译后的文本
* @returns {error} 可能的错误
*/
export function Translate(text: string, $from: string, to: string): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2536995103, text, $from, to) as any;
export function IsLanguageSupported(translatorType: translator$0.TranslatorType, languageCode: string): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(2819945417, translatorType, languageCode) as any;
return $resultPromise;
}
@@ -69,19 +72,7 @@ export function TranslateWith(text: string, $from: string, to: string, translato
return $resultPromise;
}
/**
* TranslateWithFallback 尝试使用当前活跃翻译器翻译,如果失败则尝试备用翻译器
* @param {string} text - 待翻译文本
* @param {string} from - 源语言代码 (如 "en", "zh", "auto")
* @param {string} to - 目标语言代码 (如 "en", "zh")
* @returns {string} 翻译后的文本
* @returns {string} 使用的翻译器类型
* @returns {error} 可能的错误
*/
export function TranslateWithFallback(text: string, $from: string, to: string): Promise<[string, string]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1705788405, text, $from, to) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $Create.Array($Create.Any);
const $$createType1 = translator$0.LanguageInfo.createFrom;
const $$createType2 = $Create.Map($Create.Any, $$createType1);

View File

@@ -44,6 +44,7 @@
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
"colors-named-hex": "^1.0.2",
"franc-min": "^6.2.0",
"hsl-matcher": "^1.2.4",
"lezer": "^0.13.5",
"pinia": "^3.0.3",
@@ -58,6 +59,7 @@
"devDependencies": {
"@eslint/js": "^9.30.1",
"@lezer/generator": "^1.8.0",
"@types/lodash": "^4.17.20",
"@types/node": "^24.0.10",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.0",
@@ -2121,6 +2123,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.10",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.10.tgz",
@@ -2893,6 +2902,16 @@
"lezer-elixir": "^1.0.0"
}
},
"node_modules/collapse-white-space": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
"integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -3489,6 +3508,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/franc-min": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/franc-min/-/franc-min-6.2.0.tgz",
"integrity": "sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==",
"license": "MIT",
"dependencies": {
"trigram-utils": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -3963,6 +3995,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/n-gram": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/n-gram/-/n-gram-2.0.2.tgz",
"integrity": "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
@@ -4713,6 +4755,20 @@
"node": ">=8.0"
}
},
"node_modules/trigram-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/trigram-utils/-/trigram-utils-2.0.1.tgz",
"integrity": "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==",
"license": "MIT",
"dependencies": {
"collapse-white-space": "^2.0.0",
"n-gram": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz",

View File

@@ -48,6 +48,7 @@
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
"colors-named-hex": "^1.0.2",
"franc-min": "^6.2.0",
"hsl-matcher": "^1.2.4",
"lezer": "^0.13.5",
"pinia": "^3.0.3",
@@ -62,6 +63,7 @@
"devDependencies": {
"@eslint/js": "^9.30.1",
"@lezer/generator": "^1.8.0",
"@types/lodash": "^4.17.20",
"@types/node": "^24.0.10",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.0",

View File

@@ -197,6 +197,10 @@ export default {
name: 'Color Selector',
description: 'Visual color picker and color value display'
},
translator: {
name: 'Text Translator',
description: 'Translate selected text with multiple translation services'
},
minimap: {
name: 'Minimap',
description: 'Display minimap overview of the document'

View File

@@ -198,6 +198,10 @@ export default {
name: '颜色选择器',
description: '颜色值的可视化和选择'
},
translator: {
name: '划词翻译',
description: '选择文本后显示翻译按钮,支持多种翻译服务'
},
minimap: {
name: '小地图',
description: '显示小地图视图'

View File

@@ -0,0 +1,594 @@
import {defineStore} from 'pinia';
import {computed, ref, watch} from 'vue';
import {TranslationService} from '@/../bindings/voidraft/internal/services';
import {franc} from 'franc-min';
export interface TranslationResult {
sourceText: string;
translatedText: string;
sourceLang: string;
targetLang: string;
translatorType: string;
error?: string;
}
/**
* ISO 639-3 到 ISO 639-1/2 语言代码的映射
* franc-min 返回的是 ISO 639-3 代码需要转换为翻译API常用的 ISO 639-1/2 代码
*/
const ISO_LANGUAGE_MAP: Record<string, string> = {
// 常见语言
'cmn': 'zh', // 中文 (Mandarin Chinese)
'eng': 'en', // 英文 (English)
'jpn': 'ja', // 日语 (Japanese)
'kor': 'ko', // 韩语 (Korean)
'fra': 'fr', // 法语 (French)
'deu': 'de', // 德语 (German)
'spa': 'es', // 西班牙语 (Spanish)
'rus': 'ru', // 俄语 (Russian)
'ita': 'it', // 意大利语 (Italian)
'nld': 'nl', // 荷兰语 (Dutch)
'por': 'pt', // 葡萄牙语 (Portuguese)
'vie': 'vi', // 越南语 (Vietnamese)
'arb': 'ar', // 阿拉伯语 (Arabic)
'hin': 'hi', // 印地语 (Hindi)
'ben': 'bn', // 孟加拉语 (Bengali)
'tha': 'th', // 泰语 (Thai)
'tur': 'tr', // 土耳其语 (Turkish)
'heb': 'he', // 希伯来语 (Hebrew)
'pol': 'pl', // 波兰语 (Polish)
'swe': 'sv', // 瑞典语 (Swedish)
'fin': 'fi', // 芬兰语 (Finnish)
'dan': 'da', // 丹麦语 (Danish)
'ron': 'ro', // 罗马尼亚语 (Romanian)
'hun': 'hu', // 匈牙利语 (Hungarian)
'ces': 'cs', // 捷克语 (Czech)
'ell': 'el', // 希腊语 (Greek)
'bul': 'bg', // 保加利亚语 (Bulgarian)
'cat': 'ca', // 加泰罗尼亚语 (Catalan)
'ukr': 'uk', // 乌克兰语 (Ukrainian)
'hrv': 'hr', // 克罗地亚语 (Croatian)
'ind': 'id', // 印尼语 (Indonesian)
'mal': 'ms', // 马来语 (Malay)
'nob': 'no', // 挪威语 (Norwegian)
'lat': 'la', // 拉丁语 (Latin)
'lit': 'lt', // 立陶宛语 (Lithuanian)
'slk': 'sk', // 斯洛伐克语 (Slovak)
'slv': 'sl', // 斯洛文尼亚语 (Slovenian)
'srp': 'sr', // 塞尔维亚语 (Serbian)
'est': 'et', // 爱沙尼亚语 (Estonian)
'lav': 'lv', // 拉脱维亚语 (Latvian)
'fil': 'tl', // 菲律宾语/他加禄语 (Filipino/Tagalog)
// 未知/不确定
'und': 'auto' // 未知语言
};
// 语言代码的通用映射关系,适用于大部分翻译器
const COMMON_LANGUAGE_ALIASES: Record<string, string[]> = {
'zh': ['zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant', 'chinese', 'zhong'],
'en': ['en-US', 'en-GB', 'english', 'eng'],
'ja': ['jp', 'jpn', 'japanese'],
'ko': ['kr', 'kor', 'korean'],
'fr': ['fra', 'french'],
'de': ['deu', 'german', 'ger'],
'es': ['spa', 'spanish', 'esp'],
'ru': ['rus', 'russian'],
'pt': ['por', 'portuguese'],
'it': ['ita', 'italian'],
'nl': ['nld', 'dutch'],
'ar': ['ara', 'arabic'],
'hi': ['hin', 'hindi'],
'th': ['tha', 'thai'],
'tr': ['tur', 'turkish'],
'vi': ['vie', 'vietnamese'],
'id': ['ind', 'indonesian'],
'ms': ['mal', 'malay'],
'fi': ['fin', 'finnish'],
};
/**
* 翻译存储
*/
export const useTranslationStore = defineStore('translation', () => {
// 状态
const availableTranslators = ref<string[]>([]);
const isTranslating = ref(false);
const lastResult = ref<TranslationResult | null>(null);
const error = ref<string | null>(null);
// 语言列表 - 将类型设置为any以避免类型错误
const languageMaps = ref<Record<string, Record<string, any>>>({});
// 语言使用频率计数 - 使用pinia持久化
const languageUsageCount = ref<Record<string, number>>({});
// 最近使用的翻译语言 - 最多记录10个
const recentLanguages = ref<string[]>([]);
// 默认配置
// 注意:确保默认值在初始化和持久化后正确设置
const defaultTargetLang = ref('zh');
const defaultTranslator = ref('bing');
// 检测到的源语言,初始为空字符串表示尚未检测
const detectedSourceLang = ref('');
// 计算属性
const hasTranslators = computed(() => availableTranslators.value.length > 0);
const currentLanguageMap = computed(() => {
return languageMaps.value[defaultTranslator.value] || {};
});
// 监听默认语言变更,确保目标语言在当前翻译器支持的范围内
watch([defaultTranslator], () => {
// 当切换翻译器时,验证默认目标语言是否支持
if (Object.keys(languageMaps.value).length > 0) {
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
if (validatedLang !== defaultTargetLang.value) {
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
defaultTargetLang.value = validatedLang;
}
}
});
/**
* 加载可用翻译器
*/
const loadAvailableTranslators = async (): Promise<void> => {
try {
const translators = await TranslationService.GetAvailableTranslators();
availableTranslators.value = translators;
// 如果默认翻译器不在可用列表中,则使用第一个可用的翻译器
if (translators.length > 0 && !translators.includes(defaultTranslator.value)) {
defaultTranslator.value = translators[0];
}
// 加载所有翻译器的语言列表
await Promise.all(translators.map(loadTranslatorLanguages));
// 在加载完所有语言列表后,确保默认目标语言有效
if (defaultTargetLang.value) {
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
if (validatedLang !== defaultTargetLang.value) {
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
defaultTargetLang.value = validatedLang;
}
}
} catch (err) {
error.value = 'no available translators';
}
};
/**
* 加载指定翻译器的语言列表
* @param translatorType 翻译器类型
*/
const loadTranslatorLanguages = async (translatorType: string): Promise<void> => {
try {
const languages = await TranslationService.GetTranslatorLanguages(translatorType as any);
if (languages) {
languageMaps.value[translatorType] = languages;
}
} catch (err) {
console.error(`Failed to load languages for ${translatorType}:`, err);
}
};
/**
* 检测文本语言
* @param text 待检测的文本
* @returns 检测到的语言代码,如未检测到则返回空字符串
*/
const detectLanguage = (text: string): string => {
if (!text || text.trim().length < 10) {
return '';
}
try {
// franc返回ISO 639-3代码
const detectedIso639_3 = franc(text);
// 如果是未知语言,返回空字符串
if (detectedIso639_3 === 'und') {
return '';
}
// 转换为常用语言代码
return ISO_LANGUAGE_MAP[detectedIso639_3] || '';
} catch (err) {
console.error('语言检测失败:', err);
return '';
}
};
/**
* 在翻译器语言列表中查找相似的语言代码
* @param langCode 待查找的语言代码
* @param translatorType 翻译器类型
* @returns 找到的语言代码或空字符串
*/
const findSimilarLanguage = (langCode: string, translatorType: string): string => {
if (!langCode) return '';
const languageMap = languageMaps.value[translatorType] || {};
const langCodeLower = langCode.toLowerCase();
// 1. 尝试精确匹配
if (languageMap[langCode]) {
return langCode;
}
// 2. 检查通用别名映射
const possibleAliases = Object.entries(COMMON_LANGUAGE_ALIASES).find(
([code, aliases]) => code === langCodeLower || aliases.includes(langCodeLower)
);
if (possibleAliases) {
// 检查主代码是否可用
const [mainCode, aliases] = possibleAliases;
if (languageMap[mainCode]) {
return mainCode;
}
// 检查别名是否可用
for (const alias of aliases) {
if (languageMap[alias]) {
return alias;
}
}
}
// 3. 尝试正则表达式匹配
// 创建一个基于语言代码的正则表达式:例如 'en' 会匹配 'en-US', 'en_GB' 等
const codePattern = new RegExp(`^${langCodeLower}[-_]?`, 'i');
// 在语言列表中查找匹配的语言代码
const availableCodes = Object.keys(languageMap);
const matchedCode = availableCodes.find(code =>
codePattern.test(code.toLowerCase())
);
if (matchedCode) {
return matchedCode;
}
// 4. 反向匹配,例如 'zh-CN' 应该能匹配到 'zh'
if (langCodeLower.includes('-') || langCodeLower.includes('_')) {
const baseCode = langCodeLower.split(/[-_]/)[0];
if (languageMap[baseCode]) {
return baseCode;
}
// 通过基础代码查找别名
const baseCodeAliases = Object.entries(COMMON_LANGUAGE_ALIASES).find(
([code, aliases]) => code === baseCode || aliases.includes(baseCode)
);
if (baseCodeAliases) {
const [mainCode, aliases] = baseCodeAliases;
if (languageMap[mainCode]) {
return mainCode;
}
for (const alias of aliases) {
if (languageMap[alias]) {
return alias;
}
}
}
}
// 5. 最后尝试查找与部分代码匹配的任何语言
const partialMatch = availableCodes.find(code =>
code.toLowerCase().includes(langCodeLower) ||
langCodeLower.includes(code.toLowerCase())
);
if (partialMatch) {
return partialMatch;
}
// 如果所有匹配都失败,返回英语作为默认值
return 'en';
};
/**
* 验证语言代码是否受当前翻译器支持
* @param langCode 语言代码
* @param translatorType 翻译器类型(可选,默认使用当前翻译器)
* @returns 验证后的语言代码
*/
const validateLanguage = (langCode: string, translatorType?: string): string => {
// 如果语言代码为空返回auto作为API调用的默认值
if (!langCode) return 'auto';
const currentType = translatorType || defaultTranslator.value;
// 尝试在指定翻译器的语言列表中查找相似的语言代码
return findSimilarLanguage(langCode, currentType) || 'auto';
};
/**
* 增加语言使用次数并添加到最近使用列表
* @param langCode 语言代码
* @param weight 权重默认为1
*/
const incrementLanguageUsage = (langCode: string, weight: number = 1): void => {
if (!langCode || langCode === 'auto') return;
// 转换为小写,确保一致性
const normalizedCode = langCode.toLowerCase();
// 更新使用次数,乘以权重
const currentCount = languageUsageCount.value[normalizedCode] || 0;
languageUsageCount.value[normalizedCode] = currentCount + weight;
// 更新最近使用的语言列表
updateRecentLanguages(normalizedCode);
};
/**
* 更新最近使用的语言列表
* @param langCode 语言代码
*/
const updateRecentLanguages = (langCode: string): void => {
if (!langCode) return;
// 如果已经在列表中,先移除它
const index = recentLanguages.value.indexOf(langCode);
if (index !== -1) {
recentLanguages.value.splice(index, 1);
}
// 添加到列表开头
recentLanguages.value.unshift(langCode);
// 保持列表最多10个元素
if (recentLanguages.value.length > 10) {
recentLanguages.value = recentLanguages.value.slice(0, 10);
}
};
/**
* 获取按使用频率排序的语言列表
* @param translatorType 翻译器类型
* @param grouped 是否分组返回(常用/其他)
* @returns 排序后的语言列表或分组后的语言列表
*/
const getSortedLanguages = (translatorType: string, grouped: boolean = false): [string, any][] | {frequent: [string, any][], others: [string, any][]} => {
const languageMap = languageMaps.value[translatorType] || {};
// 获取语言列表
const languages = Object.entries(languageMap);
// 按使用频率排序
const sortedLanguages = languages.sort(([codeA, infoA], [codeB, infoB]) => {
// 获取使用次数默认为0
const countA = languageUsageCount.value[codeA.toLowerCase()] || 0;
const countB = languageUsageCount.value[codeB.toLowerCase()] || 0;
// 首先按使用频率降序排序
if (countB !== countA) {
return countB - countA;
}
// 其次按最近使用情况排序
const recentIndexA = recentLanguages.value.indexOf(codeA.toLowerCase());
const recentIndexB = recentLanguages.value.indexOf(codeB.toLowerCase());
if (recentIndexA !== -1 && recentIndexB !== -1) {
return recentIndexA - recentIndexB;
} else if (recentIndexA !== -1) {
return -1;
} else if (recentIndexB !== -1) {
return 1;
}
// 如果使用频率和最近使用情况都相同,按名称排序
const nameA = infoA.Name || infoA.name || codeA;
const nameB = infoB.Name || infoB.name || codeB;
return nameA.localeCompare(nameB);
});
// 如果不需要分组,直接返回排序后的列表
if (!grouped) {
return sortedLanguages;
}
// 分组:将有使用记录的语言归为常用组,其他归为其他组
const frequentLanguages: [string, any][] = [];
const otherLanguages: [string, any][] = [];
sortedLanguages.forEach(lang => {
const [code] = lang;
const usageCount = languageUsageCount.value[code.toLowerCase()] || 0;
const isInRecent = recentLanguages.value.includes(code.toLowerCase());
if (usageCount > 0 || isInRecent) {
frequentLanguages.push(lang);
} else {
otherLanguages.push(lang);
}
});
return {
frequent: frequentLanguages,
others: otherLanguages
};
};
/**
* 翻译文本
* @param text 待翻译文本
* @param to 目标语言代码
* @param translatorType 翻译器类型
* @returns 翻译结果
*/
const translateText = async (
text: string,
to?: string,
translatorType?: string
): Promise<TranslationResult> => {
// 使用提供的参数或默认值
const targetLang = to || defaultTargetLang.value;
const translator = translatorType || defaultTranslator.value;
// 处理空文本
if (!text) {
return {
sourceText: '',
translatedText: '',
sourceLang: '',
targetLang: targetLang,
translatorType: translator,
error: 'no text to translate'
};
}
// 检测源语言
const detected = detectLanguage(text);
if (detected) {
detectedSourceLang.value = detected;
}
// 使用检测到的语言或回退到auto
let actualSourceLang = detectedSourceLang.value || 'auto';
// 确认语言代码有效并针对当前翻译器进行匹配
actualSourceLang = validateLanguage(actualSourceLang, translator);
const actualTargetLang = validateLanguage(targetLang, translator);
// 如果源语言和目标语言相同,则直接返回原文
if (actualSourceLang !== 'auto' && actualSourceLang === actualTargetLang) {
return {
sourceText: text,
translatedText: text,
sourceLang: actualSourceLang,
targetLang: actualTargetLang,
translatorType: translator
};
}
isTranslating.value = true;
error.value = null;
try {
console.log(`翻译文本: 从 ${actualSourceLang}${actualTargetLang} 使用 ${translator} 翻译器`);
// 调用翻译服务
const translatedText = await TranslationService.TranslateWith(
text,
actualSourceLang,
actualTargetLang,
translator
);
// 增加目标语言的使用频率,使用较大的权重
incrementLanguageUsage(actualTargetLang, 3);
// 如果源语言不是auto也记录其使用情况但权重较小
if (actualSourceLang !== 'auto') {
incrementLanguageUsage(actualSourceLang, 1);
}
// 构建结果
const result: TranslationResult = {
sourceText: text,
translatedText,
sourceLang: actualSourceLang,
targetLang: actualTargetLang,
translatorType: translator
};
lastResult.value = result;
return result;
} catch (err) {
// 处理错误
const errorMessage = err instanceof Error ? err.message : 'translation failed';
error.value = errorMessage;
const result: TranslationResult = {
sourceText: text,
translatedText: '',
sourceLang: actualSourceLang,
targetLang: actualTargetLang,
translatorType: translator,
error: errorMessage
};
lastResult.value = result;
return result;
} finally {
isTranslating.value = false;
}
};
/**
* 设置默认翻译配置
* @param config 配置对象
*/
const setDefaultConfig = (config: {
targetLang?: string;
translatorType?: string;
}): void => {
let changed = false;
if (config.translatorType && config.translatorType !== defaultTranslator.value) {
defaultTranslator.value = config.translatorType;
// 切换翻译器时清空检测到的源语言,以便重新检测
detectedSourceLang.value = '';
changed = true;
}
if (config.targetLang) {
// 验证目标语言是否支持
const validLang = validateLanguage(config.targetLang, defaultTranslator.value);
defaultTargetLang.value = validLang;
changed = true;
}
if (changed) {
console.log(`已更新默认翻译配置:翻译器=${defaultTranslator.value},目标语言=${defaultTargetLang.value}`);
}
};
// 初始化时加载可用翻译器
loadAvailableTranslators();
return {
// 状态
availableTranslators,
isTranslating,
lastResult,
error,
detectedSourceLang,
defaultTargetLang,
defaultTranslator,
languageMaps,
languageUsageCount,
recentLanguages,
// 计算属性
hasTranslators,
currentLanguageMap,
// 方法
loadAvailableTranslators,
loadTranslatorLanguages,
translateText,
setDefaultConfig,
detectLanguage,
validateLanguage,
findSimilarLanguage,
getSortedLanguages,
incrementLanguageUsage
};
}, {
persist: {
key: 'voidraft-translation',
storage: localStorage,
pick: ['languageUsageCount', 'defaultTargetLang', 'defaultTranslator', 'recentLanguages']
}
});

View File

@@ -0,0 +1,364 @@
import { Extension, StateField, StateEffect } from '@codemirror/state';
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
import { createTranslationTooltip } from './tooltip';
/**
* 翻译器扩展配置
*/
export interface TranslatorConfig {
/** 默认翻译服务提供商 */
defaultTranslator: string;
/** 最小选择字符数才显示翻译按钮 */
minSelectionLength: number;
/** 最大翻译字符数 */
maxTranslationLength: number;
}
/**
* 默认翻译器配置
*/
export const defaultConfig: TranslatorConfig = {
defaultTranslator: 'bing',
minSelectionLength: 2,
maxTranslationLength: 5000,
};
// 全局配置存储
let currentConfig: TranslatorConfig = {...defaultConfig};
// 存储选择的文本用于翻译
let selectedTextForTranslation = "";
/**
* 翻译图标SVG
*/
const translationIconSvg = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;
// 用于设置翻译气泡的状态效果
const setTranslationTooltip = StateEffect.define<Tooltip | null>();
/**
* 翻译气泡的状态字段
*/
const translationTooltipField = StateField.define<readonly Tooltip[]>({
create() {
return [];
},
update(tooltips, tr) {
// 如果文档或选择变化,隐藏气泡
if (tr.docChanged || tr.selection) {
return [];
}
// 检查是否有特定的状态效果来更新tooltips
for (const effect of tr.effects) {
if (effect.is(setTranslationTooltip)) {
return effect.value ? [effect.value] : [];
}
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
/**
* 根据当前选择获取翻译按钮tooltip
*/
function getTranslationButtonTooltips(state: any): readonly Tooltip[] {
// 如果气泡已显示,则不显示按钮
if (state.field(translationTooltipField).length > 0) return [];
const selection = state.selection.main;
// 如果没有选中文本,不显示按钮
if (selection.empty) return [];
// 获取选中的文本
const selectedText = state.sliceDoc(selection.from, selection.to);
// 检查文本是否只包含空格
if (!selectedText.trim()) {
return [];
}
// 检查文本长度条件
if (selectedText.length < currentConfig.minSelectionLength ||
selectedText.length > currentConfig.maxTranslationLength) {
return [];
}
// 保存选中的文本用于翻译
selectedTextForTranslation = selectedText;
// 返回翻译按钮tooltip配置
return [{
pos: selection.to,
above: false,
strictSide: true,
arrow: false,
create: (view) => {
// 创建按钮DOM
const dom = document.createElement('div');
dom.className = 'cm-translator-button';
dom.innerHTML = translationIconSvg;
// 点击事件
dom.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
// 显示翻译气泡
showTranslationTooltip(view);
});
return { dom };
}
}];
}
/**
* 显示翻译气泡
*/
function showTranslationTooltip(view: EditorView) {
if (!selectedTextForTranslation) return;
// 创建翻译气泡
const tooltip = createTranslationTooltip(view, selectedTextForTranslation);
// 更新状态以显示气泡
view.dispatch({
effects: setTranslationTooltip.of(tooltip)
});
}
/**
* 翻译按钮的状态字段
*/
const translationButtonField = StateField.define<readonly Tooltip[]>({
create(state) {
return getTranslationButtonTooltips(state);
},
update(tooltips, tr) {
// 如果文档或选择变化重新计算tooltip
if (tr.docChanged || tr.selection) {
return getTranslationButtonTooltips(tr.state);
}
// 检查是否有翻译气泡显示,如果有则不显示按钮
if (tr.state.field(translationTooltipField).length > 0) {
return [];
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
/**
* 创建翻译扩展
*/
export function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
// 更新配置
currentConfig = { ...defaultConfig, ...config };
return [
// 翻译按钮tooltip
translationButtonField,
// 翻译气泡tooltip
translationTooltipField,
// 添加基础样式
EditorView.baseTheme({
".cm-translator-button": {
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
background: "var(--bg-secondary, transparent)",
color: "var(--text-muted, #4285f4)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
padding: "2px",
width: "24px",
height: "24px",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
userSelect: "none",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
},
// 翻译气泡样式
".cm-translation-tooltip": {
background: "var(--bg-secondary, #fff)",
color: "var(--text-primary, #333)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
padding: "8px",
maxWidth: "300px",
maxHeight: "200px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
fontSize: "11px"
},
".cm-translation-header": {
marginBottom: "8px",
flexShrink: "0"
},
".cm-translation-controls": {
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "nowrap"
},
".cm-translation-select": {
padding: "2px 4px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, #f5f5f5)",
fontSize: "11px",
color: "var(--text-primary, #333)",
flex: "1",
minWidth: "0",
maxWidth: "80px"
},
".cm-translation-swap": {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "16px",
height: "16px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, transparent)",
color: "var(--text-muted, #666)",
cursor: "pointer",
padding: "0",
flexShrink: "0",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
},
// 滚动容器
".cm-translation-scroll-container": {
overflowY: "auto",
flex: "1",
minHeight: "0"
},
".cm-translation-result": {
display: "flex",
flexDirection: "column"
},
".cm-translation-result-header": {
display: "flex",
justifyContent: "flex-end",
marginBottom: "4px"
},
".cm-translation-result-wrapper": {
position: "relative",
width: "100%"
},
".cm-translation-copy-btn": {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "20px",
height: "20px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, transparent)",
color: "var(--text-muted, #666)",
cursor: "pointer",
padding: "0",
position: "absolute",
top: "4px",
right: "4px",
zIndex: "2",
opacity: "0.7",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
opacity: "1"
},
"&.copied": {
background: "var(--bg-success, #4caf50)",
color: "white",
border: "1px solid var(--bg-success, #4caf50)",
opacity: "1"
}
},
".cm-translation-target": {
padding: "6px",
paddingRight: "28px", // 为复制按钮留出空间
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
color: "var(--text-primary, #333)",
borderRadius: "3px",
whiteSpace: "pre-wrap",
wordBreak: "break-word"
},
".cm-translation-notice": {
fontSize: "10px",
color: "var(--text-muted, #888)",
padding: "2px 0",
fontStyle: "italic",
textAlign: "center",
marginBottom: "2px"
},
".cm-translation-error": {
color: "var(--text-danger, #d32f2f)",
fontStyle: "italic"
},
".cm-translation-loading": {
padding: "8px",
textAlign: "center",
color: "var(--text-muted, #666)",
fontSize: "11px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "6px"
},
".cm-translation-loading::before": {
content: "''",
display: "inline-block",
width: "12px",
height: "12px",
borderRadius: "50%",
border: "2px solid var(--text-muted, #666)",
borderTopColor: "transparent",
animation: "cm-translation-spin 1s linear infinite"
},
"@keyframes cm-translation-spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
}
})
];
}
export default createTranslatorExtension;

View File

@@ -0,0 +1,507 @@
import { EditorView, Tooltip, TooltipView } from '@codemirror/view';
import { useTranslationStore } from '@/stores/translationStore';
// 创建翻译气泡弹窗
export class TranslationTooltip implements TooltipView {
dom: HTMLElement;
sourceText: string;
translationStore: ReturnType<typeof useTranslationStore>;
// UI元素
private translatorSelector: HTMLSelectElement;
private sourceLangSelector: HTMLSelectElement;
private targetLangSelector: HTMLSelectElement;
private resultContainer: HTMLDivElement;
private loadingIndicator: HTMLDivElement;
private translatedText: string = '';
private detectedSourceLang: string = ''; // 保存检测到的语言代码
constructor(_view: EditorView, text: string) {
this.sourceText = text;
this.translationStore = useTranslationStore();
// 创建气泡弹窗容器
this.dom = document.createElement('div');
this.dom.className = 'cm-translation-tooltip';
// 创建头部控制区域 - 固定在顶部
const header = document.createElement('div');
header.className = 'cm-translation-header';
// 控制选项容器 - 所有选择器在一行
const controlsContainer = document.createElement('div');
controlsContainer.className = 'cm-translation-controls';
// 创建选择器(初始为空,稍后填充)
this.sourceLangSelector = document.createElement('select');
this.sourceLangSelector.className = 'cm-translation-select';
// 交换语言按钮
const swapButton = document.createElement('button');
swapButton.className = 'cm-translation-swap';
swapButton.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/></svg>`;
// 目标语言选择
this.targetLangSelector = document.createElement('select');
this.targetLangSelector.className = 'cm-translation-select';
// 创建一个临时的翻译器选择器,稍后会被替换
this.translatorSelector = document.createElement('select');
this.translatorSelector.className = 'cm-translation-select';
const tempOption = document.createElement('option');
tempOption.textContent = 'Loading...';
this.translatorSelector.appendChild(tempOption);
// 添加所有控制元素到一行
controlsContainer.appendChild(this.sourceLangSelector);
controlsContainer.appendChild(swapButton);
controlsContainer.appendChild(this.targetLangSelector);
controlsContainer.appendChild(this.translatorSelector);
// 添加到头部
header.appendChild(controlsContainer);
// 创建内容滚动区域
const scrollContainer = document.createElement('div');
scrollContainer.className = 'cm-translation-scroll-container';
// 创建结果区域
this.resultContainer = document.createElement('div');
this.resultContainer.className = 'cm-translation-result';
// 加载指示器
this.loadingIndicator = document.createElement('div');
this.loadingIndicator.className = 'cm-translation-loading';
this.loadingIndicator.textContent = 'Translation...';
this.loadingIndicator.style.display = 'none';
// 将结果和加载指示器添加到滚动区域
scrollContainer.appendChild(this.loadingIndicator);
scrollContainer.appendChild(this.resultContainer);
// 将所有元素添加到主容器
this.dom.appendChild(header);
this.dom.appendChild(scrollContainer);
// 添加事件监听
this.sourceLangSelector.addEventListener('change', () => {
// 检查源语言和目标语言是否相同
this.handleLanguageChange();
this.translate();
});
this.targetLangSelector.addEventListener('change', () => {
// 检查源语言和目标语言是否相同
this.handleLanguageChange();
// 增加选中语言的使用频率
const targetLang = this.targetLangSelector.value;
if (targetLang) {
this.translationStore.incrementLanguageUsage(targetLang);
}
this.translate();
});
swapButton.addEventListener('click', () => {
// 交换语言
const temp = this.sourceLangSelector.value;
this.sourceLangSelector.value = this.targetLangSelector.value;
this.targetLangSelector.value = temp;
this.translate();
});
// 显示加载中
this.loadingIndicator.style.display = 'block';
this.resultContainer.innerHTML = '<div class="cm-translation-loading">Loading...</div>';
// 加载翻译器选项
this.loadTranslators().then(() => {
// 尝试自动检测语言
if (this.sourceText.length >= 10) {
this.detectedSourceLang = this.translationStore.detectLanguage(this.sourceText);
if (this.detectedSourceLang) {
// 如果检测到语言,更新选择器
this.updateLanguageSelectorsForDetectedLanguage(this.detectedSourceLang);
}
}
// 初始翻译
this.translate();
});
}
// 处理语言变更,防止源和目标语言相同
private handleLanguageChange() {
// 防止源语言和目标语言相同
if (this.sourceLangSelector.value === this.targetLangSelector.value) {
// 寻找一个不同的目标语言
const options = Array.from(this.targetLangSelector.options);
for (const option of options) {
if (option.value !== this.sourceLangSelector.value) {
this.targetLangSelector.value = option.value;
break;
}
}
}
}
// 加载翻译器选项
private async loadTranslators() {
try {
// 确保翻译器列表已加载
if (!this.translationStore.hasTranslators) {
await this.translationStore.loadAvailableTranslators();
}
// 清空现有选项
while (this.translatorSelector.firstChild) {
this.translatorSelector.removeChild(this.translatorSelector.firstChild);
}
// 添加翻译器选项
const translators = this.translationStore.availableTranslators;
if (translators.length === 0) {
// 如果没有可用翻译器,添加一个默认选项
const option = document.createElement('option');
option.value = 'bing';
option.textContent = 'Bing';
this.translatorSelector.appendChild(option);
} else {
translators.forEach(translator => {
const option = document.createElement('option');
option.value = translator;
option.textContent = this.getTranslatorDisplayName(translator);
option.selected = translator === this.translationStore.defaultTranslator;
this.translatorSelector.appendChild(option);
});
}
// 添加事件监听
this.translatorSelector.addEventListener('change', () => {
// 更新当前翻译器
this.translationStore.setDefaultConfig({
translatorType: this.translatorSelector.value
});
// 重置检测到的语言
this.detectedSourceLang = '';
// 当切换翻译器时,可能需要重新排序语言列表
// 加载该翻译器的语言列表
this.updateLanguageSelectors();
// 执行翻译
this.translate();
});
// 加载默认翻译器的语言列表
await this.updateLanguageSelectors();
return true;
} catch (error) {
console.error('Failed to load translators:', error);
// 清空现有选项
while (this.translatorSelector.firstChild) {
this.translatorSelector.removeChild(this.translatorSelector.firstChild);
}
// 添加默认翻译器选项
const defaultTranslators = ['bing', 'google', 'youdao', 'deepl'];
defaultTranslators.forEach(translator => {
const option = document.createElement('option');
option.value = translator;
option.textContent = this.getTranslatorDisplayName(translator);
option.selected = translator === 'bing';
this.translatorSelector.appendChild(option);
});
// 添加事件监听
this.translatorSelector.addEventListener('change', () => {
// 更新选择器并重新翻译
this.updateLanguageSelectors();
this.translate();
});
// 加载默认翻译器的语言列表
await this.updateLanguageSelectors();
return false;
}
}
// 更新语言选择器
private async updateLanguageSelectors() {
const currentTranslator = this.translatorSelector.value;
// 保存当前选中的语言
const currentSourceLang = this.sourceLangSelector.value || '';
const currentTargetLang = this.targetLangSelector.value || 'zh';
// 清空源语言选择器
while (this.sourceLangSelector.firstChild) {
this.sourceLangSelector.removeChild(this.sourceLangSelector.firstChild);
}
// 清空目标语言选择器
while (this.targetLangSelector.firstChild) {
this.targetLangSelector.removeChild(this.targetLangSelector.firstChild);
}
// 获取当前翻译器的语言列表
const languageMap = this.translationStore.currentLanguageMap;
// 如果语言列表为空,直接返回
if (!languageMap || Object.keys(languageMap).length === 0) {
return;
}
// 获取按使用频率排序的语言列表
const sortedLanguages = this.translationStore.getSortedLanguages(currentTranslator);
// 添加所有语言选项
if (Array.isArray(sortedLanguages)) {
// 处理非分组返回值
sortedLanguages.forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
} else {
// 处理分组返回值
// 先添加常用语言
sortedLanguages.frequent.forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
// 再添加其他语言
sortedLanguages.others.forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
}
// 匹配之前的语言选项或使用默认值
this.updateSelectedLanguages(currentSourceLang, currentTargetLang, currentTranslator);
}
// 添加语言选项到选择器
private addLanguageOption(code: string, langInfo: any) {
// 使用后端提供的名称,而不是代码
const displayName = langInfo.Name || langInfo.name || code;
// 源语言选项
const sourceOption = document.createElement('option');
sourceOption.value = code;
sourceOption.textContent = displayName;
this.sourceLangSelector.appendChild(sourceOption);
// 目标语言选项
const targetOption = document.createElement('option');
targetOption.value = code;
// 不再显示使用次数,直接使用语言名称
targetOption.textContent = displayName;
this.targetLangSelector.appendChild(targetOption);
}
// 更新选中的语言选项,确保语言代码在当前翻译器中有效
private updateSelectedLanguages(sourceLang: string, targetLang: string, translatorType: string) {
// 尝试在当前翻译器中找到匹配的语言代码
const validSourceLang = this.translationStore.validateLanguage(sourceLang, translatorType);
// 如果找到有效的语言代码,且该代码在选择器中存在,则选中它
if (validSourceLang && this.hasLanguageOption(this.sourceLangSelector, validSourceLang)) {
this.sourceLangSelector.value = validSourceLang;
} else if (this.detectedSourceLang) {
// 如果没有找到匹配但有检测到的语言,尝试使用它
const validDetectedLang = this.translationStore.validateLanguage(this.detectedSourceLang, translatorType);
if (this.hasLanguageOption(this.sourceLangSelector, validDetectedLang)) {
this.sourceLangSelector.value = validDetectedLang;
} else if (this.sourceLangSelector.options.length > 0) {
// 如果没有检测到的语言,使用第一个可用选项
this.sourceLangSelector.selectedIndex = 0;
}
} else if (this.sourceLangSelector.options.length > 0) {
// 如果没有检测到的语言,使用第一个可用选项
this.sourceLangSelector.selectedIndex = 0;
}
// 对于目标语言,尝试找到匹配的语言代码
const validTargetLang = this.translationStore.validateLanguage(targetLang, translatorType);
if (this.hasLanguageOption(this.targetLangSelector, validTargetLang)) {
this.targetLangSelector.value = validTargetLang;
} else {
// 如果没有找到匹配,使用默认目标语言或第一个可用选项
const defaultTarget = this.translationStore.defaultTargetLang;
if (this.hasLanguageOption(this.targetLangSelector, defaultTarget)) {
this.targetLangSelector.value = defaultTarget;
} else if (this.targetLangSelector.options.length > 0) {
this.targetLangSelector.selectedIndex = 0;
}
}
// 确保源语言和目标语言不同
this.handleLanguageChange();
}
// 检查选择器是否有指定语言选项
private hasLanguageOption(selector: HTMLSelectElement, langCode: string): boolean {
return Array.from(selector.options).some(option => option.value === langCode);
}
// 为检测到的语言更新语言选择器
private updateLanguageSelectorsForDetectedLanguage(detectedLang: string) {
if (!detectedLang) return;
// 根据当前翻译器验证检测到的语言
const currentTranslator = this.translatorSelector.value;
const validLang = this.translationStore.validateLanguage(detectedLang, currentTranslator);
// 检查验证后的语言是否在选择器中
const hasDetectedOption = this.hasLanguageOption(this.sourceLangSelector, validLang);
// 设置检测到的语言为源语言
if (hasDetectedOption) {
this.sourceLangSelector.value = validLang;
}
// 存储检测到的语言代码,以便后续使用
this.detectedSourceLang = validLang;
}
// 获取翻译器显示名称
private getTranslatorDisplayName(translatorType: string): string {
switch (translatorType) {
case 'google': return 'Google';
case 'bing': return 'Bing';
case 'youdao': return 'YouDao';
case 'deepl': return 'DeepL';
default: return translatorType;
}
}
// 执行翻译
private async translate() {
const targetLang = this.targetLangSelector.value;
const translatorType = this.translatorSelector.value;
// 显示加载状态
this.loadingIndicator.style.display = 'block';
this.resultContainer.innerHTML = '';
try {
// 执行翻译 - 源语言将在store中自动检测
const result = await this.translationStore.translateText(
this.sourceText,
targetLang,
translatorType
);
// 如果检测到了语言,更新源语言选择器
if (result.sourceLang) {
this.detectedSourceLang = result.sourceLang;
this.updateLanguageSelectorsForDetectedLanguage(result.sourceLang);
}
// 显示翻译结果
this.displayTranslationResult(result);
} catch (err) {
console.error('Translation failed:', err);
this.resultContainer.innerHTML = '';
this.translatedText = '';
} finally {
// 隐藏加载状态
this.loadingIndicator.style.display = 'none';
}
}
// 显示翻译结果
private displayTranslationResult(result: any) {
// 更新结果显示
this.resultContainer.innerHTML = '';
// 创建结果容器
const resultWrapper = document.createElement('div');
resultWrapper.className = 'cm-translation-result-wrapper';
// 只显示翻译结果区域
const translatedTextElem = document.createElement('div');
translatedTextElem.className = 'cm-translation-target';
if (result.error) {
translatedTextElem.classList.add('cm-translation-error');
translatedTextElem.textContent = result.error;
this.translatedText = '';
} else {
this.translatedText = result.translatedText || '';
translatedTextElem.textContent = this.translatedText || '';
}
// 添加复制按钮
if (this.translatedText) {
const copyButton = document.createElement('button');
copyButton.className = 'cm-translation-copy-btn';
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(this.translatedText).then(() => {
// 显示复制成功提示
const originalText = copyButton.innerHTML;
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`;
copyButton.classList.add('copied');
setTimeout(() => {
copyButton.innerHTML = originalText;
copyButton.classList.remove('copied');
}, 1500);
});
});
// 将复制按钮添加到结果包装器
resultWrapper.appendChild(copyButton);
}
// 添加翻译结果到包装器
resultWrapper.appendChild(translatedTextElem);
// 添加到结果容器
this.resultContainer.appendChild(resultWrapper);
}
// 更新默认配置
private updateDefaultConfig() {
const targetLang = this.targetLangSelector.value;
// 增加目标语言的使用频率
if (targetLang) {
this.translationStore.incrementLanguageUsage(targetLang);
}
this.translationStore.setDefaultConfig({
targetLang: targetLang,
translatorType: this.translatorSelector.value
});
}
// 当气泡弹窗被销毁时
destroy() {
// 保存当前配置作为默认值
this.updateDefaultConfig();
}
}
// 创建翻译气泡
export function createTranslationTooltip(view: EditorView, text: string): Tooltip {
return {
pos: view.state.selection.main.to, // 紧贴文本末尾
above: false,
strictSide: false,
arrow: true,
create: () => new TranslationTooltip(view, text)
};
}

View File

@@ -11,6 +11,7 @@ import {hyperLink} from '../extensions/hyperlink'
import {minimap} from '../extensions/minimap'
import {vscodeSearch} from '../extensions/vscodeSearch'
import {createCheckboxExtension} from '../extensions/checkbox'
import {createTranslatorExtension} from '../extensions/translator'
import {foldingOnIndent} from '../extensions/fold/foldExtension'
@@ -79,8 +80,6 @@ export const minimapFactory: ExtensionFactory = {
}
}
/**
* 超链接扩展工厂
*/
@@ -126,8 +125,6 @@ export const searchFactory: ExtensionFactory = {
}
}
export const foldFactory: ExtensionFactory = {
create(config: any) {
return foldingOnIndent;
@@ -155,6 +152,29 @@ export const checkboxFactory: ExtensionFactory = {
}
}
/**
* 翻译扩展工厂
*/
export const translatorFactory: ExtensionFactory = {
create(config: any) {
return createTranslatorExtension({
defaultTranslator: config.defaultTranslator || 'bing',
minSelectionLength: config.minSelectionLength || 2,
maxTranslationLength: config.maxTranslationLength || 5000,
})
},
getDefaultConfig() {
return {
defaultTranslator: 'bing',
minSelectionLength: 2,
maxTranslationLength: 5000,
}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
/**
* 所有扩展的统一配置
* 排除$zero值以避免TypeScript类型错误
@@ -177,6 +197,11 @@ const EXTENSION_CONFIGS = {
displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description'
},
[ExtensionID.ExtensionTranslator]: {
factory: translatorFactory,
displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description'
},
// UI增强扩展
[ExtensionID.ExtensionMinimap]: {
@@ -185,7 +210,6 @@ const EXTENSION_CONFIGS = {
descriptionKey: 'extensions.minimap.description'
},
// 工具扩展
[ExtensionID.ExtensionSearch]: {
factory: searchFactory,

View File

@@ -6,7 +6,9 @@ import (
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"strings"
@@ -17,51 +19,296 @@ import (
// BingTranslator Bing翻译器结构体
type BingTranslator struct {
BingHost string // Bing服务主机
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
session *BingSession // Bing会话
languages map[string]LanguageInfo // 支持的语言列表
}
// BingSession 保持Bing翻译会话状态
type BingSession struct {
Cookie map[string]string // 会话Cookie
Headers map[string]string // 会话请求头
Token string // 翻译Token
Key string // 翻译Key
IG string // IG参数
}
// 常量定义
const (
bingDefaultTimeout = 30 * time.Second
defaultBingHost = "cn.bing.com" // 使用cn.bing.com作为默认域名
bingDefaultTimeout = 30 * time.Second
bingTranslatorURL = "https://cn.bing.com/translator"
bingTranslateAPIURL = "https://cn.bing.com/ttranslatev3"
)
// 错误定义
var (
ErrBingNetworkError = errors.New("bing translator network error")
ErrBingParseError = errors.New("bing translator parse error")
ErrBingTokenError = errors.New("failed to get bing translator token")
ErrBingNetworkError = errors.New("bing translator network error")
ErrBingParseError = errors.New("bing translator parse error")
ErrBingTokenError = errors.New("failed to get bing translator token")
ErrBingEmptyResponse = errors.New("empty response from bing translator")
ErrBingRateLimit = errors.New("bing translator rate limit reached")
)
// BingTranslationParams Bing翻译所需的参数
type BingTranslationParams struct {
Token string // token参数
Key string // key参数
IG string // IG参数
// 用户代理列表
var userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
}
// NewBingTranslator 创建一个新的Bing翻译器实例
func NewBingTranslator() *BingTranslator {
// 初始化随机数种子
rand.New(rand.NewSource(time.Now().UnixNano()))
// 创建带Cookie存储的HTTP客户端
jar, _ := cookiejar.New(nil)
translator := &BingTranslator{
BingHost: defaultBingHost,
Timeout: bingDefaultTimeout,
httpClient: &http.Client{Timeout: bingDefaultTimeout},
httpClient: &http.Client{
Timeout: bingDefaultTimeout,
// 启用Cookie存储
Jar: jar,
},
Timeout: bingDefaultTimeout,
session: &BingSession{
Headers: make(map[string]string),
Cookie: make(map[string]string),
},
languages: initBingLanguages(),
}
// 初始化会话
translator.refreshSession()
return translator
}
// initBingLanguages 初始化Bing翻译器支持的语言列表
func initBingLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 添加所有支持的语言
// 基于 Microsoft Translator 支持的语言列表
// 参考: https://learn.microsoft.com/en-us/azure/ai-services/translator/language-support
// 常用语言
languages["en"] = LanguageInfo{Code: "en", Name: "English"}
languages["zh-Hans"] = LanguageInfo{Code: "zh-Hans", Name: "Chinese Simplified"}
languages["zh-Hant"] = LanguageInfo{Code: "zh-Hant", Name: "Chinese Traditional"}
languages["ja"] = LanguageInfo{Code: "ja", Name: "Japanese"}
languages["ko"] = LanguageInfo{Code: "ko", Name: "Korean"}
languages["fr"] = LanguageInfo{Code: "fr", Name: "French"}
languages["fr-ca"] = LanguageInfo{Code: "fr-ca", Name: "French (Canada)"}
languages["de"] = LanguageInfo{Code: "de", Name: "German"}
languages["es"] = LanguageInfo{Code: "es", Name: "Spanish"}
languages["ru"] = LanguageInfo{Code: "ru", Name: "Russian"}
languages["pt"] = LanguageInfo{Code: "pt", Name: "Portuguese (Brazil)"}
languages["pt-br"] = LanguageInfo{Code: "pt-br", Name: "Portuguese (Brazil)"}
languages["pt-pt"] = LanguageInfo{Code: "pt-pt", Name: "Portuguese (Portugal)"}
languages["it"] = LanguageInfo{Code: "it", Name: "Italian"}
languages["ar"] = LanguageInfo{Code: "ar", Name: "Arabic"}
// 特殊语言
languages["yue"] = LanguageInfo{Code: "yue", Name: "Cantonese (Traditional)"}
languages["lzh"] = LanguageInfo{Code: "lzh", Name: "Chinese (Literary)"}
// 其他语言
languages["af"] = LanguageInfo{Code: "af", Name: "Afrikaans"}
languages["am"] = LanguageInfo{Code: "am", Name: "Amharic"}
languages["as"] = LanguageInfo{Code: "as", Name: "Assamese"}
languages["az"] = LanguageInfo{Code: "az", Name: "Azerbaijani (Latin)"}
languages["ba"] = LanguageInfo{Code: "ba", Name: "Bashkir"}
languages["bg"] = LanguageInfo{Code: "bg", Name: "Bulgarian"}
languages["bn"] = LanguageInfo{Code: "bn", Name: "Bangla"}
languages["bo"] = LanguageInfo{Code: "bo", Name: "Tibetan"}
languages["bs"] = LanguageInfo{Code: "bs", Name: "Bosnian (Latin)"}
languages["ca"] = LanguageInfo{Code: "ca", Name: "Catalan"}
languages["cs"] = LanguageInfo{Code: "cs", Name: "Czech"}
languages["cy"] = LanguageInfo{Code: "cy", Name: "Welsh"}
languages["da"] = LanguageInfo{Code: "da", Name: "Danish"}
languages["dv"] = LanguageInfo{Code: "dv", Name: "Divehi"}
languages["el"] = LanguageInfo{Code: "el", Name: "Greek"}
languages["et"] = LanguageInfo{Code: "et", Name: "Estonian"}
languages["eu"] = LanguageInfo{Code: "eu", Name: "Basque"}
languages["fa"] = LanguageInfo{Code: "fa", Name: "Persian"}
languages["fi"] = LanguageInfo{Code: "fi", Name: "Finnish"}
languages["fil"] = LanguageInfo{Code: "fil", Name: "Filipino"}
languages["fj"] = LanguageInfo{Code: "fj", Name: "Fijian"}
languages["fo"] = LanguageInfo{Code: "fo", Name: "Faroese"}
languages["ga"] = LanguageInfo{Code: "ga", Name: "Irish"}
languages["gl"] = LanguageInfo{Code: "gl", Name: "Galician"}
languages["gu"] = LanguageInfo{Code: "gu", Name: "Gujarati"}
languages["ha"] = LanguageInfo{Code: "ha", Name: "Hausa"}
languages["he"] = LanguageInfo{Code: "he", Name: "Hebrew"}
languages["hi"] = LanguageInfo{Code: "hi", Name: "Hindi"}
languages["hr"] = LanguageInfo{Code: "hr", Name: "Croatian"}
languages["ht"] = LanguageInfo{Code: "ht", Name: "Haitian Creole"}
languages["hu"] = LanguageInfo{Code: "hu", Name: "Hungarian"}
languages["hy"] = LanguageInfo{Code: "hy", Name: "Armenian"}
languages["id"] = LanguageInfo{Code: "id", Name: "Indonesian"}
languages["ig"] = LanguageInfo{Code: "ig", Name: "Igbo"}
languages["is"] = LanguageInfo{Code: "is", Name: "Icelandic"}
languages["ka"] = LanguageInfo{Code: "ka", Name: "Georgian"}
languages["kk"] = LanguageInfo{Code: "kk", Name: "Kazakh"}
languages["km"] = LanguageInfo{Code: "km", Name: "Khmer"}
languages["kn"] = LanguageInfo{Code: "kn", Name: "Kannada"}
languages["ku"] = LanguageInfo{Code: "ku", Name: "Kurdish (Arabic) (Central)"}
languages["ky"] = LanguageInfo{Code: "ky", Name: "Kyrgyz (Cyrillic)"}
languages["lo"] = LanguageInfo{Code: "lo", Name: "Lao"}
languages["lt"] = LanguageInfo{Code: "lt", Name: "Lithuanian"}
languages["lv"] = LanguageInfo{Code: "lv", Name: "Latvian"}
languages["mg"] = LanguageInfo{Code: "mg", Name: "Malagasy"}
languages["mi"] = LanguageInfo{Code: "mi", Name: "Maori"}
languages["mk"] = LanguageInfo{Code: "mk", Name: "Macedonian"}
languages["ml"] = LanguageInfo{Code: "ml", Name: "Malayalam"}
languages["mn-Cyrl"] = LanguageInfo{Code: "mn-Cyrl", Name: "Mongolian (Cyrillic)"}
languages["mr"] = LanguageInfo{Code: "mr", Name: "Marathi"}
languages["ms"] = LanguageInfo{Code: "ms", Name: "Malay (Latin)"}
languages["mt"] = LanguageInfo{Code: "mt", Name: "Maltese"}
languages["mww"] = LanguageInfo{Code: "mww", Name: "Hmong Daw (Latin)"}
languages["my"] = LanguageInfo{Code: "my", Name: "Myanmar (Burmese)"}
languages["nb"] = LanguageInfo{Code: "nb", Name: "Norwegian Bokmål"}
languages["ne"] = LanguageInfo{Code: "ne", Name: "Nepali"}
languages["nl"] = LanguageInfo{Code: "nl", Name: "Dutch"}
languages["or"] = LanguageInfo{Code: "or", Name: "Odia"}
languages["otq"] = LanguageInfo{Code: "otq", Name: "Queretaro Otomi"}
languages["pa"] = LanguageInfo{Code: "pa", Name: "Punjabi"}
languages["pl"] = LanguageInfo{Code: "pl", Name: "Polish"}
languages["prs"] = LanguageInfo{Code: "prs", Name: "Dari"}
languages["ps"] = LanguageInfo{Code: "ps", Name: "Pashto"}
languages["ro"] = LanguageInfo{Code: "ro", Name: "Romanian"}
languages["rw"] = LanguageInfo{Code: "rw", Name: "Kinyarwanda"}
languages["sk"] = LanguageInfo{Code: "sk", Name: "Slovak"}
languages["sl"] = LanguageInfo{Code: "sl", Name: "Slovenian"}
languages["sm"] = LanguageInfo{Code: "sm", Name: "Samoan (Latin)"}
languages["sn"] = LanguageInfo{Code: "sn", Name: "chiShona"}
languages["so"] = LanguageInfo{Code: "so", Name: "Somali"}
languages["sq"] = LanguageInfo{Code: "sq", Name: "Albanian"}
languages["sr-Cyrl"] = LanguageInfo{Code: "sr-Cyrl", Name: "Serbian (Cyrillic)"}
languages["sr"] = LanguageInfo{Code: "sr", Name: "Serbian (Latin)"}
languages["sr-latn"] = LanguageInfo{Code: "sr-latn", Name: "Serbian (Latin)"}
languages["sv"] = LanguageInfo{Code: "sv", Name: "Swedish"}
languages["sw"] = LanguageInfo{Code: "sw", Name: "Swahili (Latin)"}
languages["ta"] = LanguageInfo{Code: "ta", Name: "Tamil"}
languages["te"] = LanguageInfo{Code: "te", Name: "Telugu"}
languages["th"] = LanguageInfo{Code: "th", Name: "Thai"}
languages["ti"] = LanguageInfo{Code: "ti", Name: "Tigrinya"}
languages["tk"] = LanguageInfo{Code: "tk", Name: "Turkmen (Latin)"}
languages["tlh-Latn"] = LanguageInfo{Code: "tlh-Latn", Name: "Klingon"}
languages["tlh-Piqd"] = LanguageInfo{Code: "tlh-Piqd", Name: "Klingon (plqaD)"}
languages["to"] = LanguageInfo{Code: "to", Name: "Tongan"}
languages["tr"] = LanguageInfo{Code: "tr", Name: "Turkish"}
languages["tt"] = LanguageInfo{Code: "tt", Name: "Tatar (Latin)"}
languages["ty"] = LanguageInfo{Code: "ty", Name: "Tahitian"}
languages["ug"] = LanguageInfo{Code: "ug", Name: "Uyghur (Arabic)"}
languages["uk"] = LanguageInfo{Code: "uk", Name: "Ukrainian"}
languages["ur"] = LanguageInfo{Code: "ur", Name: "Urdu"}
languages["uz"] = LanguageInfo{Code: "uz", Name: "Uzbek (Latin)"}
languages["vi"] = LanguageInfo{Code: "vi", Name: "Vietnamese"}
languages["yua"] = LanguageInfo{Code: "yua", Name: "Yucatec Maya"}
languages["zu"] = LanguageInfo{Code: "zu", Name: "Zulu"}
// 添加一些特殊情况的映射
languages["zh"] = LanguageInfo{Code: "zh-Hans", Name: "Chinese Simplified"} // 将zh映射到zh-Hans
return languages
}
// SetTimeout 设置请求超时时间
func (t *BingTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
t.httpClient.Timeout = timeout
}
// SetBingHost 设置Bing主机
func (t *BingTranslator) SetBingHost(host string) {
t.BingHost = host
// getRandomUserAgent 获取随机用户代理
func getRandomUserAgent() string {
return userAgents[rand.Intn(len(userAgents))]
}
// refreshSession 刷新翻译会话
func (t *BingTranslator) refreshSession() error {
// 设置随机用户代理
userAgent := getRandomUserAgent()
t.session.Headers["User-Agent"] = userAgent
t.session.Headers["Referer"] = bingTranslatorURL
t.session.Headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
t.session.Headers["Accept-Language"] = "en-US,en;q=0.5"
t.session.Headers["Connection"] = "keep-alive"
t.session.Headers["Upgrade-Insecure-Requests"] = "1"
t.session.Headers["Cache-Control"] = "max-age=0"
// 创建请求
req, err := http.NewRequest("GET", bingTranslatorURL, nil)
if err != nil {
return fmt.Errorf("the creation request failed: %w", err)
}
// 设置请求头
for k, v := range t.session.Headers {
req.Header.Set(k, v)
}
// 发送请求
resp, err := t.httpClient.Do(req)
if err != nil {
return fmt.Errorf("%w: %v", ErrBingNetworkError, err)
}
defer resp.Body.Close()
// 保存Cookie
for _, cookie := range resp.Cookies() {
t.session.Cookie[cookie.Name] = cookie.Value
}
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response failed: %w", err)
}
content := string(body)
// 提取参数
// 1. 提取key和token
paramsPattern := regexp.MustCompile(`params_AbusePreventionHelper\s*=\s*(\[.*?\]);`)
paramsMatch := paramsPattern.FindStringSubmatch(content)
if paramsMatch == nil || len(paramsMatch) < 2 {
return fmt.Errorf("%w: params_AbusePreventionHelper could not be extracted", ErrBingTokenError)
}
// 解析参数数组
paramsStr := paramsMatch[1]
paramsStr = strings.ReplaceAll(paramsStr, "[", "")
paramsStr = strings.ReplaceAll(paramsStr, "]", "")
paramsParts := strings.Split(paramsStr, ",")
if len(paramsParts) < 2 {
return fmt.Errorf("%w: params_AbusePreventionHelper format is incorrect", ErrBingTokenError)
}
// 提取key和token
t.session.Key = strings.Trim(paramsParts[0], `"' `)
t.session.Token = strings.Trim(paramsParts[1], `"' `)
// 2. 提取IG值
igPattern := regexp.MustCompile(`IG:"(\w+)"`)
igMatch := igPattern.FindStringSubmatch(content)
if igMatch == nil || len(igMatch) < 2 {
return fmt.Errorf("%w: Unable to extract IG values", ErrBingTokenError)
}
t.session.IG = igMatch[1]
// 更新会话头部
t.session.Headers["IG"] = t.session.IG
t.session.Headers["key"] = t.session.Key
t.session.Headers["token"] = t.session.Token
return nil
}
// Translate 使用标准语言标签进行文本翻译
@@ -71,207 +318,166 @@ func (t *BingTranslator) Translate(text string, from language.Tag, to language.T
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *BingTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
return t.translate(text, params.From, params.To)
}
// translate 执行实际翻译操作
func (t *BingTranslator) translate(text, from, to string) (string, error) {
// 获取翻译所需的参数
params, err := t.ExtractBingTranslationParams()
if err != nil {
return "", fmt.Errorf("failed to extract bing translation params: %w", err)
}
// 执行翻译
return t.GetBingTranslation(params.Token, params.Key, params.IG, text, from, to)
}
// ExtractBingTranslationParams 提取Bing翻译所需的参数
func (t *BingTranslator) ExtractBingTranslationParams() (*BingTranslationParams, error) {
// 发送GET请求获取网页内容
url := fmt.Sprintf("https://%s/translator?mkt=zh-CN", t.BingHost)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
resp, err := t.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrBingNetworkError, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to access Bing translator page: status code %d", resp.StatusCode)
}
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
pageContent := string(body)
// 模式1: 标准的params_AbusePreventionHelper数组
keyPattern := regexp.MustCompile(`params_AbusePreventionHelper\s*=\s*\[([^\]]+)\]`)
keyMatch := keyPattern.FindStringSubmatch(pageContent)
var key, token string
if len(keyMatch) >= 2 {
// 提取并解析数组
paramsStr := keyMatch[1]
paramsList := strings.Split(paramsStr, ",")
if len(paramsList) >= 2 {
// 清理引号
key = strings.Trim(paramsList[0], `"' `)
token = strings.Trim(paramsList[1], `"' `)
// 如果没有会话或关键参数缺失,刷新会话
if t.session == nil || t.session.Token == "" || t.session.Key == "" || t.session.IG == "" {
if err := t.refreshSession(); err != nil {
return "", fmt.Errorf("the refresh session failed: %w", err)
}
}
// 如果标准模式失败,尝试备用模式
if key == "" || token == "" {
// 模式2: 查找_G.Token和_G.Key
tokenPattern := regexp.MustCompile(`_G\.Token\s*=\s*["']([^"']+)["']`)
tokenMatch := tokenPattern.FindStringSubmatch(pageContent)
// 生成随机IID
randNum := rand.Intn(10) // 0-9的随机数
iid := fmt.Sprintf("translator.5019.%d", 1+randNum%3) // 生成随机IID
keyPattern := regexp.MustCompile(`_G\.Key\s*=\s*["']?([^"',]+)["']?`)
keyMatch := keyPattern.FindStringSubmatch(pageContent)
// 构建URL - 确保使用双&符号
reqURL := fmt.Sprintf("%s?isVertical=1&&IG=%s&IID=%s",
bingTranslateAPIURL, t.session.IG, iid)
if len(tokenMatch) >= 2 && len(keyMatch) >= 2 {
token = tokenMatch[1]
key = keyMatch[1]
}
}
// 标准化语言代码
fromLang := t.GetStandardLanguageCode(from)
toLang := t.GetStandardLanguageCode(to)
// 如果仍然失败尝试JSON格式
if key == "" || token == "" {
jsonPattern := regexp.MustCompile(`"token"\s*:\s*"([^"]+)"\s*,\s*"key"\s*:\s*"?([^",]+)"?`)
jsonMatch := jsonPattern.FindStringSubmatch(pageContent)
// 构建表单数据
formData := url.Values{}
formData.Set("fromLang", fromLang)
formData.Set("text", text)
formData.Set("to", toLang)
formData.Set("token", t.session.Token)
formData.Set("key", t.session.Key)
if len(jsonMatch) >= 3 {
token = jsonMatch[1]
key = jsonMatch[2]
}
}
// 如果所有模式都失败
if key == "" || token == "" {
return nil, fmt.Errorf("%w: unable to extract token and key", ErrBingTokenError)
}
// 查找并提取 IG 参数,尝试多种格式
var ig string
// 模式1: 标准IG格式
igPattern := regexp.MustCompile(`IG["']?\s*:\s*["']([^"']+)["']`)
igMatch := igPattern.FindStringSubmatch(pageContent)
if len(igMatch) >= 2 {
ig = igMatch[1]
} else {
// 模式2: 备用IG格式
igPattern = regexp.MustCompile(`"IG"\s*:\s*"([^"]+)"`)
igMatch = igPattern.FindStringSubmatch(pageContent)
if len(igMatch) >= 2 {
ig = igMatch[1]
} else {
// 模式3: _G.IG格式
igPattern = regexp.MustCompile(`_G\.IG\s*=\s*["']([^"']+)["']`)
igMatch = igPattern.FindStringSubmatch(pageContent)
if len(igMatch) >= 2 {
ig = igMatch[1]
}
}
}
// 如果所有IG提取模式都失败
if ig == "" {
return nil, fmt.Errorf("%w: unable to extract IG parameter", ErrBingTokenError)
}
return &BingTranslationParams{
Token: token,
Key: key,
IG: ig,
}, nil
}
// GetBingTranslation 获取Bing翻译结果
func (t *BingTranslator) GetBingTranslation(token, key, ig, text, fromLang, toLang string) (string, error) {
// URL编码文本
encodedText := url.QueryEscape(text)
// 构建POST请求的payload
payload := fmt.Sprintf("fromLang=%s&to=%s&text=%s&token=%s&key=%s",
fromLang, toLang, encodedText, token, key)
// 构建URL
urlStr := fmt.Sprintf("https://%s/ttranslatev3?isVertical=1&IG=%s&IID=translator.5028", t.BingHost, ig)
formDataStr := formData.Encode()
// 创建请求
req, err := http.NewRequest("POST", urlStr, strings.NewReader(payload))
req, err := http.NewRequest("POST", reqURL, strings.NewReader(formDataStr))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
return "", fmt.Errorf("The creation request failed: %w", err)
}
// 设置请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
req.Header.Set("Accept", "*/*")
req.Header.Set("Host", t.BingHost)
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Origin", fmt.Sprintf("https://%s", t.BingHost))
req.Header.Set("Referer", fmt.Sprintf("https://%s/translator", t.BingHost))
req.Header.Set("User-Agent", t.session.Headers["User-Agent"])
req.Header.Set("Referer", bingTranslatorURL)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Origin", "https://cn.bing.com")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
// 添加Cookie
for name, value := range t.session.Cookie {
req.AddCookie(&http.Cookie{
Name: name,
Value: value,
})
}
// 发送请求
resp, err := t.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrBingNetworkError, err)
}
defer resp.Body.Close()
// 判断请求是否成功
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: status code %d", resp.StatusCode)
}
// 读取响应体
// 读取响应
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
return "", fmt.Errorf("read response failed: %w", err)
}
// 检查响应内容
if len(body) == 0 {
return "", fmt.Errorf("translation API returned empty response")
return "", ErrBingEmptyResponse
}
// 使用最简单的结构体解析JSON
var response []struct {
Translations []struct {
Text string `json:"text"`
} `json:"translations"`
// 尝试解析响应
var result interface{}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("%w: %v", ErrBingParseError, err)
}
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("%w: JSON parsing error: %v", ErrBingParseError, err)
// 检查是否是字典类型
if resultDict, ok := result.(map[string]interface{}); ok {
// 检查是否需要验证码
if _, hasCaptcha := resultDict["ShowCaptcha"]; hasCaptcha {
return "", ErrBingRateLimit
}
// 检查状态码
if statusCode, hasStatus := resultDict["statusCode"]; hasStatus {
if statusCode.(float64) == 400 {
// 检查是否有错误消息
if errorMsg, hasError := resultDict["errorMessage"]; hasError && errorMsg.(string) != "" {
return "", fmt.Errorf("translation failed: %s", errorMsg)
}
// 如果没有明确的错误消息可能是API变更或其他问题
return "", fmt.Errorf("translation request failed (status code: 400)")
} else if statusCode.(float64) == 429 {
return "", ErrBingRateLimit
}
}
// 尝试从错误响应中提取详细信息
if message, hasMessage := resultDict["message"]; hasMessage {
return "", fmt.Errorf("translation failed: %v", message)
}
// 尝试从响应中获取翻译结果
if translations, hasTranslations := resultDict["translations"]; hasTranslations {
if translationsArray, ok := translations.([]interface{}); ok && len(translationsArray) > 0 {
if translation, ok := translationsArray[0].(map[string]interface{}); ok {
if text, ok := translation["text"].(string); ok {
return text, nil
}
}
}
}
// 其他错误
return "", fmt.Errorf("translation failed: %v", resultDict)
}
// 检查解析结果
if len(response) == 0 || len(response[0].Translations) == 0 {
return "", fmt.Errorf("%w: invalid response format", ErrBingParseError)
// 应该是数组类型
if resultArray, ok := result.([]interface{}); ok && len(resultArray) > 0 {
firstItem := resultArray[0]
if itemDict, ok := firstItem.(map[string]interface{}); ok {
if translations, ok := itemDict["translations"].([]interface{}); ok && len(translations) > 0 {
if translation, ok := translations[0].(map[string]interface{}); ok {
if text, ok := translation["text"].(string); ok {
return text, nil
}
}
}
}
}
// 返回翻译结果
return response[0].Translations[0].Text, nil
return "", fmt.Errorf("%w: The response format is not as expected", ErrBingParseError)
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *BingTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *BingTranslator) IsLanguageSupported(languageCode string) bool {
_, exists := t.languages[languageCode]
return exists
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *BingTranslator) GetStandardLanguageCode(languageCode string) string {
if info, exists := t.languages[languageCode]; exists {
return info.Code
}
return languageCode // 如果没有找到映射,返回原始代码
}

View File

@@ -17,9 +17,10 @@ import (
// DeeplTranslator DeepL翻译器结构体
type DeeplTranslator struct {
DeeplHost string // DeepL服务主机
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
DeeplHost string // DeepL服务主机
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
languages map[string]LanguageInfo // 支持的语言列表
}
// 常量定义
@@ -36,66 +37,6 @@ var (
ErrDeeplResponseError = errors.New("deepl translator response error")
)
// 语言映射
var deeplLangMap = map[string]string{
"auto": "auto",
"de": "DE",
"en": "EN",
"es": "ES",
"fr": "FR",
"it": "IT",
"ja": "JA",
"ko": "KO",
"nl": "NL",
"pl": "PL",
"pt": "PT",
"ru": "RU",
"zh": "ZH",
"bg": "BG",
"cs": "CS",
"da": "DA",
"el": "EL",
"et": "ET",
"fi": "FI",
"hu": "HU",
"lt": "LT",
"lv": "LV",
"ro": "RO",
"sk": "SK",
"sl": "SL",
"sv": "SV",
}
// 反向语言映射
var deeplLangMapReverse = map[string]string{
"auto": "auto",
"DE": "de",
"EN": "en",
"ES": "es",
"FR": "fr",
"IT": "it",
"JA": "ja",
"KO": "ko",
"NL": "nl",
"PL": "pl",
"PT": "pt",
"RU": "ru",
"ZH": "zh",
"BG": "bg",
"CS": "cs",
"DA": "da",
"EL": "el",
"ET": "et",
"FI": "fi",
"HU": "hu",
"LT": "lt",
"LV": "lv",
"RO": "ro",
"SK": "sk",
"SL": "sl",
"SV": "sv",
}
// DeeplRequest DeepL请求结构体
type DeeplRequest struct {
Jsonrpc string `json:"jsonrpc"`
@@ -156,11 +97,60 @@ func NewDeeplTranslator() *DeeplTranslator {
DeeplHost: defaultDeeplHost,
Timeout: deeplDefaultTimeout,
httpClient: &http.Client{Timeout: deeplDefaultTimeout},
languages: initDeeplLanguages(),
}
return translator
}
// initDeeplLanguages 初始化DeepL翻译器支持的语言列表
func initDeeplLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 添加所有支持的语言
// 基于 DeepL API 支持的语言列表
// 参考: https://developers.deepl.com/docs/resources/supported-languages
// 源语言和目标语言
languages["ar"] = LanguageInfo{Code: "AR", Name: "Arabic"}
languages["bg"] = LanguageInfo{Code: "BG", Name: "Bulgarian"}
languages["cs"] = LanguageInfo{Code: "CS", Name: "Czech"}
languages["da"] = LanguageInfo{Code: "DA", Name: "Danish"}
languages["de"] = LanguageInfo{Code: "DE", Name: "German"}
languages["el"] = LanguageInfo{Code: "EL", Name: "Greek"}
languages["en"] = LanguageInfo{Code: "EN", Name: "English"}
languages["en-gb"] = LanguageInfo{Code: "EN-GB", Name: "English (British)"}
languages["en-us"] = LanguageInfo{Code: "EN-US", Name: "English (American)"}
languages["es"] = LanguageInfo{Code: "ES", Name: "Spanish"}
languages["et"] = LanguageInfo{Code: "ET", Name: "Estonian"}
languages["fi"] = LanguageInfo{Code: "FI", Name: "Finnish"}
languages["fr"] = LanguageInfo{Code: "FR", Name: "French"}
languages["hu"] = LanguageInfo{Code: "HU", Name: "Hungarian"}
languages["id"] = LanguageInfo{Code: "ID", Name: "Indonesian"}
languages["it"] = LanguageInfo{Code: "IT", Name: "Italian"}
languages["ja"] = LanguageInfo{Code: "JA", Name: "Japanese"}
languages["ko"] = LanguageInfo{Code: "KO", Name: "Korean"}
languages["lt"] = LanguageInfo{Code: "LT", Name: "Lithuanian"}
languages["lv"] = LanguageInfo{Code: "LV", Name: "Latvian"}
languages["nb"] = LanguageInfo{Code: "NB", Name: "Norwegian Bokmål"}
languages["nl"] = LanguageInfo{Code: "NL", Name: "Dutch"}
languages["pl"] = LanguageInfo{Code: "PL", Name: "Polish"}
languages["pt"] = LanguageInfo{Code: "PT", Name: "Portuguese"}
languages["pt-br"] = LanguageInfo{Code: "PT-BR", Name: "Portuguese (Brazilian)"}
languages["pt-pt"] = LanguageInfo{Code: "PT-PT", Name: "Portuguese (Portugal)"}
languages["ro"] = LanguageInfo{Code: "RO", Name: "Romanian"}
languages["ru"] = LanguageInfo{Code: "RU", Name: "Russian"}
languages["sk"] = LanguageInfo{Code: "SK", Name: "Slovak"}
languages["sl"] = LanguageInfo{Code: "SL", Name: "Slovenian"}
languages["sv"] = LanguageInfo{Code: "SV", Name: "Swedish"}
languages["tr"] = LanguageInfo{Code: "TR", Name: "Turkish"}
languages["uk"] = LanguageInfo{Code: "UK", Name: "Ukrainian"}
languages["zh"] = LanguageInfo{Code: "ZH", Name: "Chinese"}
return languages
}
// SetTimeout 设置请求超时时间
func (t *DeeplTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
@@ -179,38 +169,34 @@ func (t *DeeplTranslator) Translate(text string, from language.Tag, to language.
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *DeeplTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
tries := params.Tries
if tries == 0 {
tries = defaultNumberOfRetries
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
var result string
var lastError error
for i := 0; i < tries; i++ {
if i > 0 && params.Delay > 0 {
time.Sleep(params.Delay)
}
result, lastError = t.translate(text, params.From, params.To)
if lastError == nil {
return result, nil
}
}
return "", lastError
// 直接执行一次翻译
return t.translate(text, params.From, params.To)
}
// translate 执行实际翻译操作
func (t *DeeplTranslator) translate(text, from, to string) (string, error) {
// 转换语言代码为DeepL格式
sourceLang, ok := deeplLangMap[strings.ToLower(from)]
if !ok && from != "auto" {
fromLower := strings.ToLower(from)
toLower := strings.ToLower(to)
var sourceLang string
if fromLower == "auto" {
sourceLang = "auto"
} else if fromLangInfo, ok := t.languages[fromLower]; ok {
sourceLang = fromLangInfo.Code
} else {
sourceLang = "auto"
}
targetLang, ok := deeplLangMap[strings.ToLower(to)]
if !ok {
var targetLang string
if toLangInfo, ok := t.languages[toLower]; ok {
targetLang = toLangInfo.Code
} else {
return "", fmt.Errorf("%w: language '%s' not supported by DeepL", ErrDeeplUnsupportedLang, to)
}
@@ -262,39 +248,45 @@ func (t *DeeplTranslator) translate(text, from, to string) (string, error) {
return "", fmt.Errorf("failed to create request: %w", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Origin", "https://www.deepl.com")
req.Header.Set("Referer", "https://www.deepl.com/translator")
// 执行请求
resp, err := t.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDeeplNetworkError, err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("%w: status code %d", ErrDeeplNetworkError, resp.StatusCode)
return "", fmt.Errorf("API error: status code %d", resp.StatusCode)
}
// 读取响应
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
// 解析响应
var deeplResp DeeplResponse
err = json.Unmarshal(body, &deeplResp)
if err != nil {
var response DeeplResponse
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("%w: %v", ErrDeeplResponseError, err)
}
// 检查是否有有效的结果
if len(deeplResp.Result.Texts) == 0 {
return "", fmt.Errorf("%w: no translation result", ErrDeeplResponseError)
// 检查结果
if len(response.Result.Texts) == 0 {
return "", fmt.Errorf("%w: empty translation result", ErrDeeplResponseError)
}
// 返回翻译结果
return deeplResp.Result.Texts[0].Text, nil
return response.Result.Texts[0].Text, nil
}
// getICount 获取文本中'i'字符的数量
@@ -316,3 +308,20 @@ func getTimeStamp(iCount int) int64 {
}
return ts
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *DeeplTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *DeeplTranslator) IsLanguageSupported(languageCode string) bool {
_, ok := t.languages[strings.ToLower(languageCode)]
return ok
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *DeeplTranslator) GetStandardLanguageCode(languageCode string) string {
// 简单返回小写版本作为标准代码
return strings.ToLower(languageCode)
}

View File

@@ -26,11 +26,12 @@ var (
// GoogleTranslator Google翻译器结构体统一管理翻译功能
type GoogleTranslator struct {
GoogleHost string // Google服务主机
vm *otto.Otto // JavaScript虚拟机
ttk otto.Value // 翻译token缓存
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
GoogleHost string // Google服务主机
vm *otto.Otto // JavaScript虚拟机
ttk otto.Value // 翻译token缓存
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
languages map[string]LanguageInfo // 支持的语言列表
}
// NewGoogleTranslator 创建一个新的Google翻译器实例
@@ -40,6 +41,7 @@ func NewGoogleTranslator() *GoogleTranslator {
vm: otto.New(),
Timeout: defaultTimeout,
httpClient: &http.Client{Timeout: defaultTimeout},
languages: initGoogleLanguages(),
}
// 初始化ttk
@@ -48,6 +50,131 @@ func NewGoogleTranslator() *GoogleTranslator {
return translator
}
// initGoogleLanguages 初始化Google翻译器支持的语言列表
func initGoogleLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 添加所有支持的语言
// 参考: https://cloud.google.com/translate/docs/languages
// 添加自动检测
languages["auto"] = LanguageInfo{Code: "auto", Name: "Auto Detect"}
// 主要语言
languages["en"] = LanguageInfo{Code: "en", Name: "English"}
languages["zh-cn"] = LanguageInfo{Code: "zh-CN", Name: "Chinese (Simplified)"}
languages["zh-tw"] = LanguageInfo{Code: "zh-TW", Name: "Chinese (Traditional)"}
languages["ja"] = LanguageInfo{Code: "ja", Name: "Japanese"}
languages["ko"] = LanguageInfo{Code: "ko", Name: "Korean"}
languages["fr"] = LanguageInfo{Code: "fr", Name: "French"}
languages["de"] = LanguageInfo{Code: "de", Name: "German"}
languages["es"] = LanguageInfo{Code: "es", Name: "Spanish"}
languages["ru"] = LanguageInfo{Code: "ru", Name: "Russian"}
languages["it"] = LanguageInfo{Code: "it", Name: "Italian"}
languages["pt"] = LanguageInfo{Code: "pt", Name: "Portuguese"}
// 其他语言
languages["af"] = LanguageInfo{Code: "af", Name: "Afrikaans"}
languages["sq"] = LanguageInfo{Code: "sq", Name: "Albanian"}
languages["am"] = LanguageInfo{Code: "am", Name: "Amharic"}
languages["ar"] = LanguageInfo{Code: "ar", Name: "Arabic"}
languages["hy"] = LanguageInfo{Code: "hy", Name: "Armenian"}
languages["az"] = LanguageInfo{Code: "az", Name: "Azerbaijani"}
languages["eu"] = LanguageInfo{Code: "eu", Name: "Basque"}
languages["be"] = LanguageInfo{Code: "be", Name: "Belarusian"}
languages["bn"] = LanguageInfo{Code: "bn", Name: "Bengali"}
languages["bs"] = LanguageInfo{Code: "bs", Name: "Bosnian"}
languages["bg"] = LanguageInfo{Code: "bg", Name: "Bulgarian"}
languages["ca"] = LanguageInfo{Code: "ca", Name: "Catalan"}
languages["ceb"] = LanguageInfo{Code: "ceb", Name: "Cebuano"}
languages["zh"] = LanguageInfo{Code: "zh", Name: "Chinese"}
languages["co"] = LanguageInfo{Code: "co", Name: "Corsican"}
languages["hr"] = LanguageInfo{Code: "hr", Name: "Croatian"}
languages["cs"] = LanguageInfo{Code: "cs", Name: "Czech"}
languages["da"] = LanguageInfo{Code: "da", Name: "Danish"}
languages["nl"] = LanguageInfo{Code: "nl", Name: "Dutch"}
languages["eo"] = LanguageInfo{Code: "eo", Name: "Esperanto"}
languages["et"] = LanguageInfo{Code: "et", Name: "Estonian"}
languages["fi"] = LanguageInfo{Code: "fi", Name: "Finnish"}
languages["fy"] = LanguageInfo{Code: "fy", Name: "Frisian"}
languages["gl"] = LanguageInfo{Code: "gl", Name: "Galician"}
languages["ka"] = LanguageInfo{Code: "ka", Name: "Georgian"}
languages["el"] = LanguageInfo{Code: "el", Name: "Greek"}
languages["gu"] = LanguageInfo{Code: "gu", Name: "Gujarati"}
languages["ht"] = LanguageInfo{Code: "ht", Name: "Haitian Creole"}
languages["ha"] = LanguageInfo{Code: "ha", Name: "Hausa"}
languages["haw"] = LanguageInfo{Code: "haw", Name: "Hawaiian"}
languages["he"] = LanguageInfo{Code: "he", Name: "Hebrew"}
languages["hi"] = LanguageInfo{Code: "hi", Name: "Hindi"}
languages["hmn"] = LanguageInfo{Code: "hmn", Name: "Hmong"}
languages["hu"] = LanguageInfo{Code: "hu", Name: "Hungarian"}
languages["is"] = LanguageInfo{Code: "is", Name: "Icelandic"}
languages["ig"] = LanguageInfo{Code: "ig", Name: "Igbo"}
languages["id"] = LanguageInfo{Code: "id", Name: "Indonesian"}
languages["ga"] = LanguageInfo{Code: "ga", Name: "Irish"}
languages["jw"] = LanguageInfo{Code: "jw", Name: "Javanese"}
languages["kn"] = LanguageInfo{Code: "kn", Name: "Kannada"}
languages["kk"] = LanguageInfo{Code: "kk", Name: "Kazakh"}
languages["km"] = LanguageInfo{Code: "km", Name: "Khmer"}
languages["ku"] = LanguageInfo{Code: "ku", Name: "Kurdish"}
languages["ky"] = LanguageInfo{Code: "ky", Name: "Kyrgyz"}
languages["lo"] = LanguageInfo{Code: "lo", Name: "Lao"}
languages["la"] = LanguageInfo{Code: "la", Name: "Latin"}
languages["lv"] = LanguageInfo{Code: "lv", Name: "Latvian"}
languages["lt"] = LanguageInfo{Code: "lt", Name: "Lithuanian"}
languages["lb"] = LanguageInfo{Code: "lb", Name: "Luxembourgish"}
languages["mk"] = LanguageInfo{Code: "mk", Name: "Macedonian"}
languages["mg"] = LanguageInfo{Code: "mg", Name: "Malagasy"}
languages["ms"] = LanguageInfo{Code: "ms", Name: "Malay"}
languages["ml"] = LanguageInfo{Code: "ml", Name: "Malayalam"}
languages["mt"] = LanguageInfo{Code: "mt", Name: "Maltese"}
languages["mi"] = LanguageInfo{Code: "mi", Name: "Maori"}
languages["mr"] = LanguageInfo{Code: "mr", Name: "Marathi"}
languages["mn"] = LanguageInfo{Code: "mn", Name: "Mongolian"}
languages["my"] = LanguageInfo{Code: "my", Name: "Myanmar (Burmese)"}
languages["ne"] = LanguageInfo{Code: "ne", Name: "Nepali"}
languages["no"] = LanguageInfo{Code: "no", Name: "Norwegian"}
languages["ny"] = LanguageInfo{Code: "ny", Name: "Nyanja (Chichewa)"}
languages["ps"] = LanguageInfo{Code: "ps", Name: "Pashto"}
languages["fa"] = LanguageInfo{Code: "fa", Name: "Persian"}
languages["pl"] = LanguageInfo{Code: "pl", Name: "Polish"}
languages["pt-br"] = LanguageInfo{Code: "pt-BR", Name: "Portuguese (Brazil)"}
languages["pt-pt"] = LanguageInfo{Code: "pt-PT", Name: "Portuguese (Portugal)"}
languages["pa"] = LanguageInfo{Code: "pa", Name: "Punjabi"}
languages["ro"] = LanguageInfo{Code: "ro", Name: "Romanian"}
languages["sm"] = LanguageInfo{Code: "sm", Name: "Samoan"}
languages["gd"] = LanguageInfo{Code: "gd", Name: "Scots Gaelic"}
languages["sr"] = LanguageInfo{Code: "sr", Name: "Serbian"}
languages["st"] = LanguageInfo{Code: "st", Name: "Sesotho"}
languages["sn"] = LanguageInfo{Code: "sn", Name: "Shona"}
languages["sd"] = LanguageInfo{Code: "sd", Name: "Sindhi"}
languages["si"] = LanguageInfo{Code: "si", Name: "Sinhala (Sinhalese)"}
languages["sk"] = LanguageInfo{Code: "sk", Name: "Slovak"}
languages["sl"] = LanguageInfo{Code: "sl", Name: "Slovenian"}
languages["so"] = LanguageInfo{Code: "so", Name: "Somali"}
languages["su"] = LanguageInfo{Code: "su", Name: "Sundanese"}
languages["sw"] = LanguageInfo{Code: "sw", Name: "Swahili"}
languages["sv"] = LanguageInfo{Code: "sv", Name: "Swedish"}
languages["tl"] = LanguageInfo{Code: "tl", Name: "Tagalog (Filipino)"}
languages["tg"] = LanguageInfo{Code: "tg", Name: "Tajik"}
languages["ta"] = LanguageInfo{Code: "ta", Name: "Tamil"}
languages["te"] = LanguageInfo{Code: "te", Name: "Telugu"}
languages["th"] = LanguageInfo{Code: "th", Name: "Thai"}
languages["tr"] = LanguageInfo{Code: "tr", Name: "Turkish"}
languages["uk"] = LanguageInfo{Code: "uk", Name: "Ukrainian"}
languages["ur"] = LanguageInfo{Code: "ur", Name: "Urdu"}
languages["uz"] = LanguageInfo{Code: "uz", Name: "Uzbek"}
languages["vi"] = LanguageInfo{Code: "vi", Name: "Vietnamese"}
languages["cy"] = LanguageInfo{Code: "cy", Name: "Welsh"}
languages["xh"] = LanguageInfo{Code: "xh", Name: "Xhosa"}
languages["yi"] = LanguageInfo{Code: "yi", Name: "Yiddish"}
languages["yo"] = LanguageInfo{Code: "yo", Name: "Yoruba"}
languages["zu"] = LanguageInfo{Code: "zu", Name: "Zulu"}
return languages
}
// SetTimeout 设置请求超时时间
func (t *GoogleTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
@@ -61,25 +188,21 @@ func (t *GoogleTranslator) SetGoogleHost(host string) {
// Translate 使用Go语言提供的标准语言标签进行文本翻译
func (t *GoogleTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
return t.translate(text, from.String(), to.String(), false, defaultNumberOfRetries, 0)
return t.translate(text, from.String(), to.String(), false)
}
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *GoogleTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
tries := params.Tries
if tries == 0 {
tries = defaultNumberOfRetries
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
return t.translate(text, params.From, params.To, true, tries, params.Delay)
return t.translate(text, params.From, params.To, true)
}
// translate 执行实际翻译操作
func (t *GoogleTranslator) translate(text, from, to string, withVerification bool, tries int, delay time.Duration) (string, error) {
if tries == 0 {
tries = defaultNumberOfRetries
}
func (t *GoogleTranslator) translate(text, from, to string, withVerification bool) (string, error) {
if withVerification {
if _, err := language.Parse(from); err != nil && from != "auto" {
log.Println("[WARNING], '" + from + "' is a invalid language, switching to 'auto'")
@@ -125,29 +248,21 @@ func (t *GoogleTranslator) translate(text, from, to string, withVerification boo
parameters.Add("tk", token)
u.RawQuery = parameters.Encode()
var r *http.Response
for tries > 0 {
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return "", err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return "", err
}
r, err = t.httpClient.Do(req)
if err != nil {
if errors.Is(err, http.ErrHandlerTimeout) {
return "", ErrBadNetwork
}
return "", err
r, err := t.httpClient.Do(req)
if err != nil {
if errors.Is(err, http.ErrHandlerTimeout) {
return "", ErrBadNetwork
}
return "", err
}
if r.StatusCode == http.StatusOK {
break
}
if r.StatusCode == http.StatusForbidden {
tries--
time.Sleep(delay)
}
if r.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: status code %d", r.StatusCode)
}
raw, err := io.ReadAll(r.Body)
@@ -309,3 +424,20 @@ func (t *GoogleTranslator) generateToken(a otto.Value, TTK otto.Value) (otto.Val
return result, nil
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *GoogleTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *GoogleTranslator) IsLanguageSupported(languageCode string) bool {
_, ok := t.languages[strings.ToLower(languageCode)]
return ok
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *GoogleTranslator) GetStandardLanguageCode(languageCode string) string {
// 简单返回小写版本作为标准代码
return strings.ToLower(languageCode)
}

View File

@@ -10,16 +10,14 @@ import (
// TranslationParams 用于指定翻译参数
type TranslationParams struct {
From string // 源语言
To string // 目标语言
Tries int // 重试次数
Delay time.Duration // 重试延迟
From string // 源语言
To string // 目标语言
Timeout time.Duration // 超时时间
}
// 常量定义
const (
defaultNumberOfRetries = 2
defaultTimeout = 30 * time.Second
defaultTimeout = 30 * time.Second
)
// TranslatorType 翻译器类型
@@ -36,6 +34,12 @@ const (
DeeplTranslatorType TranslatorType = "deepl"
)
// LanguageInfo 语言信息结构体
type LanguageInfo struct {
Code string // 语言代码
Name string // 语言名称
}
// Translator 翻译器接口,定义所有翻译器必须实现的方法
type Translator interface {
// Translate 使用Go语言提供的标准语言标签进行文本翻译
@@ -46,6 +50,15 @@ type Translator interface {
// SetTimeout 设置请求超时时间
SetTimeout(timeout time.Duration)
// GetSupportedLanguages 获取翻译器支持的语言列表
GetSupportedLanguages() map[string]LanguageInfo
// IsLanguageSupported 检查指定的语言代码是否受支持
IsLanguageSupported(languageCode string) bool
// GetStandardLanguageCode 获取标准化的语言代码
GetStandardLanguageCode(languageCode string) string
}
// TranslatorFactory 翻译器工厂,用于创建不同类型的翻译器

View File

@@ -17,8 +17,9 @@ import (
// YoudaoTranslator 有道翻译器结构体
type YoudaoTranslator struct {
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
languages map[string]LanguageInfo // 支持的语言列表
}
// 常量定义
@@ -38,11 +39,23 @@ func NewYoudaoTranslator() *YoudaoTranslator {
translator := &YoudaoTranslator{
Timeout: youdaoDefaultTimeout,
httpClient: &http.Client{Timeout: youdaoDefaultTimeout},
languages: initYoudaoLanguages(),
}
return translator
}
// initYoudaoLanguages 初始化有道翻译器支持的语言列表
func initYoudaoLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 自动检测
languages["auto"] = LanguageInfo{Code: "auto", Name: "Auto"}
return languages
}
// SetTimeout 设置请求超时时间
func (t *YoudaoTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
@@ -57,6 +70,11 @@ func (t *YoudaoTranslator) Translate(text string, from language.Tag, to language
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *YoudaoTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
// 有道翻译不需要指定源语言和目标语言,它会自动检测
return t.translate(text)
}
@@ -184,3 +202,20 @@ func (t *YoudaoTranslator) extractText(n *html.Node) string {
return result
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *YoudaoTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *YoudaoTranslator) IsLanguageSupported(languageCode string) bool {
_, ok := t.languages[strings.ToLower(languageCode)]
return ok
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *YoudaoTranslator) GetStandardLanguageCode(languageCode string) string {
// 简单返回小写版本作为标准代码
return strings.ToLower(languageCode)
}

View File

@@ -20,7 +20,8 @@ const (
ExtensionColorSelector ExtensionID = "colorSelector" // 颜色选择器
ExtensionFold ExtensionID = "fold"
ExtensionTextHighlight ExtensionID = "textHighlight"
ExtensionCheckbox ExtensionID = "checkbox" // 选择框
ExtensionCheckbox ExtensionID = "checkbox" // 选择框
ExtensionTranslator ExtensionID = "translator" // 划词翻译
// UI增强扩展
ExtensionMinimap ExtensionID = "minimap" // 小地图
@@ -101,6 +102,23 @@ func NewDefaultExtensions() []Extension {
IsDefault: true,
Config: ExtensionConfig{},
},
{
ID: ExtensionTranslator,
Enabled: true,
IsDefault: true,
Config: ExtensionConfig{
"defaultSourceLang": "auto",
"defaultTargetLang": "zh",
"defaultTranslator": "bing",
"showTranslateButton": true,
"showButtonOnSelect": true,
"buttonDisplayDelay": 300,
"tooltipTimeout": 0,
"maxTooltipWidth": 300,
"minSelectionLength": 2,
"maxTranslationLength": 5000,
},
},
// UI增强扩展
{

View File

@@ -6,69 +6,29 @@ import (
"voidraft/internal/common/translator"
"github.com/wailsapp/wails/v3/pkg/services/log"
"golang.org/x/text/language"
)
// TranslationService 翻译服务
type TranslationService struct {
logger *log.LoggerService
factory *translator.TranslatorFactory
defaultTimeout time.Duration
activeTranslator translator.TranslatorType
translators map[translator.TranslatorType]translator.Translator
mutex sync.RWMutex
logger *log.LoggerService
factory *translator.TranslatorFactory
defaultTimeout time.Duration
translators map[translator.TranslatorType]translator.Translator
mutex sync.RWMutex
}
// NewTranslationService 创建翻译服务实例
func NewTranslationService(logger *log.LoggerService) *TranslationService {
factory := translator.NewTranslatorFactory()
defaultTimeout := 10 * time.Second
// 默认使用bin翻译
activeType := translator.BingTranslatorType
// 预初始化所有翻译器
translators := make(map[translator.TranslatorType]translator.Translator)
service := &TranslationService{
logger: logger,
factory: factory,
defaultTimeout: defaultTimeout,
activeTranslator: activeType,
translators: translators,
logger: logger,
factory: translator.NewTranslatorFactory(),
defaultTimeout: 10 * time.Second,
translators: make(map[translator.TranslatorType]translator.Translator),
}
// 延迟初始化翻译器以提高启动速度
go service.initTranslators()
return service
}
// initTranslators 初始化所有翻译器
func (s *TranslationService) initTranslators() {
types := []translator.TranslatorType{
translator.GoogleTranslatorType,
translator.BingTranslatorType,
translator.YoudaoTranslatorType,
translator.DeeplTranslatorType,
}
for _, t := range types {
trans, err := s.factory.Create(t)
if err != nil {
s.logger.Error("Failed to create translator: %v", err)
continue
}
trans.SetTimeout(s.defaultTimeout)
s.mutex.Lock()
s.translators[t] = trans
s.mutex.Unlock()
}
}
// getTranslator 获取指定类型的翻译器
// getTranslator 获取指定类型的翻译器,如不存在则创建
func (s *TranslationService) getTranslator(translatorType translator.TranslatorType) (translator.Translator, error) {
s.mutex.RLock()
trans, exists := s.translators[translatorType]
@@ -78,7 +38,7 @@ func (s *TranslationService) getTranslator(translatorType translator.TranslatorT
return trans, nil
}
// 如果翻译器尚未初始化,则立即创建
// 创建新的翻译器实例
trans, err := s.factory.Create(translatorType)
if err != nil {
return nil, err
@@ -93,86 +53,6 @@ func (s *TranslationService) getTranslator(translatorType translator.TranslatorT
return trans, nil
}
// Translate 使用当前活跃翻译器进行翻译
// @param {string} text - 待翻译文本
// @param {string} from - 源语言代码 (如 "en", "zh", "auto")
// @param {string} to - 目标语言代码 (如 "en", "zh")
// @returns {string} 翻译后的文本
// @returns {error} 可能的错误
func (s *TranslationService) Translate(text string, from string, to string) (string, error) {
// 解析语言标签
var fromLang, toLang language.Tag
var err error
if from == "auto" {
fromLang = language.Und // 未定义,表示自动检测
} else {
fromLang, err = language.Parse(from)
if err != nil {
return "", err
}
}
toLang, err = language.Parse(to)
if err != nil {
return "", err
}
// 获取活跃翻译器
s.mutex.RLock()
activeType := s.activeTranslator
s.mutex.RUnlock()
trans, err := s.getTranslator(activeType)
if err != nil {
return "", err
}
// 执行翻译
return trans.Translate(text, fromLang, toLang)
}
// TranslateWithFallback 尝试使用当前活跃翻译器翻译,如果失败则尝试备用翻译器
// @param {string} text - 待翻译文本
// @param {string} from - 源语言代码 (如 "en", "zh", "auto")
// @param {string} to - 目标语言代码 (如 "en", "zh")
// @returns {string} 翻译后的文本
// @returns {string} 使用的翻译器类型
// @returns {error} 可能的错误
func (s *TranslationService) TranslateWithFallback(text string, from string, to string) (string, string, error) {
// 首先尝试活跃翻译器
s.mutex.RLock()
primaryType := s.activeTranslator
s.mutex.RUnlock()
result, err := s.TranslateWith(text, from, to, string(primaryType))
if err == nil {
return result, string(primaryType), nil
}
// 备用翻译器列表
fallbacks := []translator.TranslatorType{
translator.GoogleTranslatorType,
translator.BingTranslatorType,
translator.DeeplTranslatorType,
translator.YoudaoTranslatorType,
}
// 尝试备用翻译器
for _, fallbackType := range fallbacks {
if fallbackType == primaryType {
continue // 跳过已尝试的主要翻译器
}
result, err := s.TranslateWith(text, from, to, string(fallbackType))
if err == nil {
return result, string(fallbackType), nil
}
}
return "", "", err // 所有翻译器都失败时返回最后一个错误
}
// TranslateWith 使用指定翻译器进行翻译
// @param {string} text - 待翻译文本
// @param {string} from - 源语言代码 (如 "en", "zh", "auto")
@@ -181,9 +61,12 @@ func (s *TranslationService) TranslateWithFallback(text string, from string, to
// @returns {string} 翻译后的文本
// @returns {error} 可能的错误
func (s *TranslationService) TranslateWith(text string, from string, to string, translatorType string) (string, error) {
// 参数验证
// 空文本直接返回
if text == "" {
return "", nil // 空文本无需翻译
return "", nil
}
if translatorType == "" {
translatorType = string(translator.BingTranslatorType)
}
// 转换为翻译器类型
@@ -197,35 +80,15 @@ func (s *TranslationService) TranslateWith(text string, from string, to string,
// 创建翻译参数
params := translator.TranslationParams{
From: from,
To: to,
Tries: 2,
Delay: 500 * time.Millisecond,
From: from,
To: to,
Timeout: s.defaultTimeout,
}
// 执行翻译
return trans.TranslateWithParams(text, params)
}
// SetActiveTranslator 设置活跃翻译器
// @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
// @returns {error} 可能的错误
func (s *TranslationService) SetActiveTranslator(translatorType string) error {
transType := translator.TranslatorType(translatorType)
// 验证翻译器类型
_, err := s.factory.Create(transType)
if err != nil {
return err
}
s.mutex.Lock()
s.activeTranslator = transType
s.mutex.Unlock()
return nil
}
// GetAvailableTranslators 获取所有可用翻译器类型
// @returns {[]string} 翻译器类型列表
func (s *TranslationService) GetAvailableTranslators() []string {
@@ -237,17 +100,34 @@ func (s *TranslationService) GetAvailableTranslators() []string {
}
}
// SetTimeout 设置翻译超时时间
// @param {int} seconds - 超时秒数
func (s *TranslationService) SetTimeout(seconds int) {
timeout := time.Duration(seconds) * time.Second
s.mutex.Lock()
s.defaultTimeout = timeout
s.mutex.Unlock()
// 更新所有现有翻译器的超时设置
for _, t := range s.translators {
t.SetTimeout(timeout)
// GetTranslatorLanguages 获取翻译器的语言列表
// @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
// @returns {map[string]string} 语言代码到名称的映射
// @returns {error} 可能的错误
func (s *TranslationService) GetTranslatorLanguages(translatorType translator.TranslatorType) (map[string]translator.LanguageInfo, error) {
translator, err := s.getTranslator(translatorType)
if err != nil {
return nil, err
}
// 获取语言列表
languages := translator.GetSupportedLanguages()
return languages, nil
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (s *TranslationService) IsLanguageSupported(translatorType translator.TranslatorType, languageCode string) bool {
translator, err := s.getTranslator(translatorType)
if err != nil {
return false
}
return translator.IsLanguageSupported(languageCode)
}
// GetStandardLanguageCode 获取标准化的语言代码
func (s *TranslationService) GetStandardLanguageCode(translatorType translator.TranslatorType, languageCode string) string {
translator, err := s.getTranslator(translatorType)
if err != nil {
return ""
}
return translator.GetStandardLanguageCode(languageCode)
}