diff --git a/frontend/bindings/voidraft/internal/common/translator/index.ts b/frontend/bindings/voidraft/internal/common/translator/index.ts new file mode 100644 index 0000000..c9d993a --- /dev/null +++ b/frontend/bindings/voidraft/internal/common/translator/index.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export * from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/common/translator/models.ts b/frontend/bindings/voidraft/internal/common/translator/models.ts new file mode 100644 index 0000000..b38314b --- /dev/null +++ b/frontend/bindings/voidraft/internal/common/translator/models.ts @@ -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 = {}) { + 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); + } +} + +/** + * 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", +}; diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 850ca32..08b2a23 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -380,6 +380,11 @@ export enum ExtensionID { */ ExtensionCheckbox = "checkbox", + /** + * 划词翻译 + */ + ExtensionTranslator = "translator", + /** * UI增强扩展 * 小地图 diff --git a/frontend/bindings/voidraft/internal/services/translationservice.ts b/frontend/bindings/voidraft/internal/services/translationservice.ts index 24a91a8..449babf 100644 --- a/frontend/bindings/voidraft/internal/services/translationservice.ts +++ b/frontend/bindings/voidraft/internal/services/translationservice.ts @@ -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 & { cancel(): void } /** - * SetActiveTranslator 设置活跃翻译器 + * GetStandardLanguageCode 获取标准化的语言代码 + */ +export function GetStandardLanguageCode(translatorType: translator$0.TranslatorType, languageCode: string): Promise & { 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 & { 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 & { 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 & { cancel(): void } { - let $resultPromise = $Call.ByID(2536995103, text, $from, to) as any; +export function IsLanguageSupported(translatorType: translator$0.TranslatorType, languageCode: string): Promise & { 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); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 31abff4..a7ae827 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 45f76bc..331d549 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 4335fcf..813c279 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -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' diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 8a60e14..88e0824 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -198,6 +198,10 @@ export default { name: '颜色选择器', description: '颜色值的可视化和选择' }, + translator: { + name: '划词翻译', + description: '选择文本后显示翻译按钮,支持多种翻译服务' + }, minimap: { name: '小地图', description: '显示小地图视图' diff --git a/frontend/src/stores/translationStore.ts b/frontend/src/stores/translationStore.ts new file mode 100644 index 0000000..8c00f96 --- /dev/null +++ b/frontend/src/stores/translationStore.ts @@ -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 = { + // 常见语言 + '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 = { + '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([]); + const isTranslating = ref(false); + const lastResult = ref(null); + const error = ref(null); + // 语言列表 - 将类型设置为any以避免类型错误 + const languageMaps = ref>>({}); + + // 语言使用频率计数 - 使用pinia持久化 + const languageUsageCount = ref>({}); + // 最近使用的翻译语言 - 最多记录10个 + const recentLanguages = ref([]); + + // 默认配置 + // 注意:确保默认值在初始化和持久化后正确设置 + 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 => { + 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 => { + 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 => { + // 使用提供的参数或默认值 + 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'] + } +}); \ No newline at end of file diff --git a/frontend/src/views/editor/extensions/translator/index.ts b/frontend/src/views/editor/extensions/translator/index.ts new file mode 100644 index 0000000..e7301e9 --- /dev/null +++ b/frontend/src/views/editor/extensions/translator/index.ts @@ -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 = ` + + +`; + +// 用于设置翻译气泡的状态效果 +const setTranslationTooltip = StateEffect.define(); + +/** + * 翻译气泡的状态字段 + */ +const translationTooltipField = StateField.define({ + 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({ + 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): 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; \ No newline at end of file diff --git a/frontend/src/views/editor/extensions/translator/tooltip.ts b/frontend/src/views/editor/extensions/translator/tooltip.ts new file mode 100644 index 0000000..ab2582a --- /dev/null +++ b/frontend/src/views/editor/extensions/translator/tooltip.ts @@ -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; + + // 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 = ``; + + // 目标语言选择 + 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 = '
Loading...
'; + + // 加载翻译器选项 + 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 = ``; + + copyButton.addEventListener('click', () => { + navigator.clipboard.writeText(this.translatedText).then(() => { + // 显示复制成功提示 + const originalText = copyButton.innerHTML; + copyButton.innerHTML = ``; + 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) + }; +} \ No newline at end of file diff --git a/frontend/src/views/editor/manager/factories.ts b/frontend/src/views/editor/manager/factories.ts index 3f7ff3b..ace83c7 100644 --- a/frontend/src/views/editor/manager/factories.ts +++ b/frontend/src/views/editor/manager/factories.ts @@ -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, diff --git a/internal/common/translator/bing_translator.go b/internal/common/translator/bing_translator.go index ce661bf..2ddeb7b 100644 --- a/internal/common/translator/bing_translator.go +++ b/internal/common/translator/bing_translator.go @@ -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 // 如果没有找到映射,返回原始代码 } diff --git a/internal/common/translator/deepl_translator.go b/internal/common/translator/deepl_translator.go index a591c82..bcdbb34 100644 --- a/internal/common/translator/deepl_translator.go +++ b/internal/common/translator/deepl_translator.go @@ -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) +} diff --git a/internal/common/translator/google_translator.go b/internal/common/translator/google_translator.go index 43e20da..c42731f 100644 --- a/internal/common/translator/google_translator.go +++ b/internal/common/translator/google_translator.go @@ -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) +} diff --git a/internal/common/translator/translator.go b/internal/common/translator/translator.go index 2c2594b..6b2dd9f 100644 --- a/internal/common/translator/translator.go +++ b/internal/common/translator/translator.go @@ -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 翻译器工厂,用于创建不同类型的翻译器 diff --git a/internal/common/translator/youdao_translator.go b/internal/common/translator/youdao_translator.go index a8a291a..7ff0b36 100644 --- a/internal/common/translator/youdao_translator.go +++ b/internal/common/translator/youdao_translator.go @@ -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) +} diff --git a/internal/models/extensions.go b/internal/models/extensions.go index f2d29ce..ec0734a 100644 --- a/internal/models/extensions.go +++ b/internal/models/extensions.go @@ -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增强扩展 { diff --git a/internal/services/translation_service.go b/internal/services/translation_service.go index 6543fdd..a82aa7f 100644 --- a/internal/services/translation_service.go +++ b/internal/services/translation_service.go @@ -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) }