🚧 Optimize
This commit is contained in:
@@ -68,4 +68,9 @@ export enum TranslatorType {
|
||||
* DeeplTranslatorType DeepL翻译器
|
||||
*/
|
||||
DeeplTranslatorType = "deepl",
|
||||
|
||||
/**
|
||||
* TartuNLPTranslatorType TartuNLP翻译器
|
||||
*/
|
||||
TartuNLPTranslatorType = "tartunlp",
|
||||
};
|
||||
|
||||
@@ -14,27 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as translator$0 from "../common/translator/models.js";
|
||||
|
||||
/**
|
||||
* GetAvailableTranslators 获取所有可用翻译器类型
|
||||
* @returns {[]string} 翻译器类型列表
|
||||
*/
|
||||
export function GetAvailableTranslators(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1186597995) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetStandardLanguageCode 获取标准化的语言代码
|
||||
*/
|
||||
export function GetStandardLanguageCode(translatorType: translator$0.TranslatorType, languageCode: string): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1158131995, translatorType, languageCode) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTranslatorLanguages 获取翻译器的语言列表
|
||||
* @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
|
||||
@@ -43,6 +22,19 @@ export function GetStandardLanguageCode(translatorType: translator$0.TranslatorT
|
||||
*/
|
||||
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 $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTranslators 获取所有可用翻译器类型
|
||||
* @returns {[]string} 翻译器类型列表
|
||||
*/
|
||||
export function GetTranslators(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3720069432) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
@@ -73,6 +65,6 @@ export function TranslateWith(text: string, $from: string, to: string, translato
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
const $$createType1 = translator$0.LanguageInfo.createFrom;
|
||||
const $$createType2 = $Create.Map($Create.Any, $$createType1);
|
||||
const $$createType0 = translator$0.LanguageInfo.createFrom;
|
||||
const $$createType1 = $Create.Map($Create.Any, $$createType0);
|
||||
const $$createType2 = $Create.Array($Create.Any);
|
||||
|
||||
393
frontend/package-lock.json
generated
393
frontend/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
"@codemirror/autocomplete": "^6.19.0",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
@@ -32,11 +32,11 @@
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.3",
|
||||
"@codemirror/view": "^6.38.4",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
@@ -48,7 +48,6 @@
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"franc-min": "^6.2.0",
|
||||
"groovy-beautify": "^0.0.17",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
@@ -60,7 +59,7 @@
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.93.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.5.1"
|
||||
@@ -72,17 +71,17 @@
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"cross-env": "^10.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"globals": "^16.4.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.8"
|
||||
"vue-tsc": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -104,12 +103,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.3.tgz",
|
||||
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.2"
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -119,9 +118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@@ -171,9 +170,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.18.7",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.7.tgz",
|
||||
"integrity": "sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==",
|
||||
"version": "6.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz",
|
||||
"integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
@@ -515,9 +514,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/legacy-modes": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz",
|
||||
"integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
|
||||
"integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0"
|
||||
@@ -555,9 +554,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.3.tgz",
|
||||
"integrity": "sha512-x2t87+oqwB1mduiQZ6huIghjMt4uZKFEdj66IcXw7+a5iBEvv9lh7EWDRHI7crnD4BMGpnyq/RzmCGbiEZLcvQ==",
|
||||
"version": "6.38.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.4.tgz",
|
||||
"integrity": "sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
@@ -2342,17 +2341,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz",
|
||||
"integrity": "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
|
||||
"integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.44.1",
|
||||
"@typescript-eslint/type-utils": "8.44.1",
|
||||
"@typescript-eslint/utils": "8.44.1",
|
||||
"@typescript-eslint/visitor-keys": "8.44.1",
|
||||
"@typescript-eslint/scope-manager": "8.45.0",
|
||||
"@typescript-eslint/type-utils": "8.45.0",
|
||||
"@typescript-eslint/utils": "8.45.0",
|
||||
"@typescript-eslint/visitor-keys": "8.45.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -2366,7 +2365,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.44.1",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -2382,16 +2381,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.44.1.tgz",
|
||||
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.45.0.tgz",
|
||||
"integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.44.1",
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
"@typescript-eslint/typescript-estree": "8.44.1",
|
||||
"@typescript-eslint/visitor-keys": "8.44.1",
|
||||
"@typescript-eslint/scope-manager": "8.45.0",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
"@typescript-eslint/typescript-estree": "8.45.0",
|
||||
"@typescript-eslint/visitor-keys": "8.45.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2407,14 +2406,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.44.1.tgz",
|
||||
"integrity": "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.45.0.tgz",
|
||||
"integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.44.1",
|
||||
"@typescript-eslint/types": "^8.44.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.45.0",
|
||||
"@typescript-eslint/types": "^8.45.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2429,14 +2428,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz",
|
||||
"integrity": "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz",
|
||||
"integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
"@typescript-eslint/visitor-keys": "8.44.1"
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
"@typescript-eslint/visitor-keys": "8.45.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2447,9 +2446,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz",
|
||||
"integrity": "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz",
|
||||
"integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2464,15 +2463,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz",
|
||||
"integrity": "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz",
|
||||
"integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
"@typescript-eslint/typescript-estree": "8.44.1",
|
||||
"@typescript-eslint/utils": "8.44.1",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
"@typescript-eslint/typescript-estree": "8.45.0",
|
||||
"@typescript-eslint/utils": "8.45.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -2489,9 +2488,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.44.1.tgz",
|
||||
"integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.45.0.tgz",
|
||||
"integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2503,16 +2502,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz",
|
||||
"integrity": "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz",
|
||||
"integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.44.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.44.1",
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
"@typescript-eslint/visitor-keys": "8.44.1",
|
||||
"@typescript-eslint/project-service": "8.45.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.45.0",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
"@typescript-eslint/visitor-keys": "8.45.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -2558,16 +2557,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.44.1.tgz",
|
||||
"integrity": "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.45.0.tgz",
|
||||
"integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.44.1",
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
"@typescript-eslint/typescript-estree": "8.44.1"
|
||||
"@typescript-eslint/scope-manager": "8.45.0",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
"@typescript-eslint/typescript-estree": "8.45.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2582,13 +2581,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz",
|
||||
"integrity": "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz",
|
||||
"integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2646,64 +2645,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
|
||||
"integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
||||
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@vue/shared": "3.5.21",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/shared": "3.5.22",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
|
||||
"integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.21",
|
||||
"@vue/shared": "3.5.21"
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
|
||||
"integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@vue/compiler-core": "3.5.21",
|
||||
"@vue/compiler-dom": "3.5.21",
|
||||
"@vue/compiler-ssr": "3.5.21",
|
||||
"@vue/shared": "3.5.21",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.18",
|
||||
"magic-string": "^0.30.19",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
|
||||
"integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
||||
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.21",
|
||||
"@vue/shared": "3.5.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
||||
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -2740,17 +2728,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.8.tgz",
|
||||
"integrity": "sha512-eYs6PF7bxoPYvek9qxceo1BCwFbJZYqJll+WaYC8o8ec60exqj+n+QRGGiJHSeUfYp0hDxARbMdxMq/fbPgU5g==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.0.tgz",
|
||||
"integrity": "sha512-a7ns+X9vTbdmk7QLrvnZs8s4E1wwtxG/sELzr6F2j4pU+r/OoAv6jJGSz+5tVTU6e4+3rjepGhSP8jDmBBcb3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.23",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/compiler-vue2": "^2.7.16",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^2.0.5",
|
||||
"alien-signals": "^3.0.0",
|
||||
"muggle-string": "^0.4.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"picomatch": "^4.0.2"
|
||||
@@ -2778,53 +2765,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz",
|
||||
"integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.21"
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.21.tgz",
|
||||
"integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.21",
|
||||
"@vue/shared": "3.5.21"
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz",
|
||||
"integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.21",
|
||||
"@vue/runtime-core": "3.5.21",
|
||||
"@vue/shared": "3.5.21",
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.21.tgz",
|
||||
"integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.21",
|
||||
"@vue/shared": "3.5.21"
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.21"
|
||||
"vue": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.21.tgz",
|
||||
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
|
||||
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@wailsio/runtime": {
|
||||
@@ -2893,9 +2880,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.7.tgz",
|
||||
"integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.0.0.tgz",
|
||||
"integrity": "sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3469,16 +3456,6 @@
|
||||
"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",
|
||||
@@ -3644,9 +3621,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.0.0.tgz",
|
||||
"integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz",
|
||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3722,13 +3699,6 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
|
||||
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
@@ -4385,19 +4355,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -4623,16 +4580,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
@@ -5279,16 +5226,6 @@
|
||||
"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",
|
||||
@@ -6643,20 +6580,6 @@
|
||||
"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",
|
||||
@@ -6712,9 +6635,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -6726,16 +6649,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.44.1.tgz",
|
||||
"integrity": "sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==",
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz",
|
||||
"integrity": "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.44.1",
|
||||
"@typescript-eslint/parser": "8.44.1",
|
||||
"@typescript-eslint/typescript-estree": "8.44.1",
|
||||
"@typescript-eslint/utils": "8.44.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
"@typescript-eslint/parser": "8.45.0",
|
||||
"@typescript-eslint/typescript-estree": "8.45.0",
|
||||
"@typescript-eslint/utils": "8.45.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -7240,16 +7163,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz",
|
||||
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.21",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
"@vue/runtime-dom": "3.5.21",
|
||||
"@vue/server-renderer": "3.5.21",
|
||||
"@vue/shared": "3.5.21"
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/runtime-dom": "3.5.22",
|
||||
"@vue/server-renderer": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -7345,14 +7268,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.8.tgz",
|
||||
"integrity": "sha512-H9yg/m6ywykmWS+pIAEs65v2FrVm5uOA0a0dHkX6Sx8dNg1a1m4iudt/6eGa9fAenmNHGlLFN9XpWQb8i5sU1w==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.0.tgz",
|
||||
"integrity": "sha512-fbMynMG7kXSnqZTRBSCh9ROYaVpXfCZbEO0gY3lqOjLbp361uuS88n6BDajiUriDIF+SGLWoinjvf6stS2J3Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "2.4.23",
|
||||
"@vue/language-core": "3.0.8"
|
||||
"@vue/language-core": "3.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
"@codemirror/autocomplete": "^6.19.0",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
@@ -37,11 +37,11 @@
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.3",
|
||||
"@codemirror/view": "^6.38.4",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
@@ -53,7 +53,6 @@
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"franc-min": "^6.2.0",
|
||||
"groovy-beautify": "^0.0.17",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
@@ -65,7 +64,7 @@
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.93.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.5.1"
|
||||
@@ -77,16 +76,16 @@
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"cross-env": "^10.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"globals": "^16.4.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.8"
|
||||
"vue-tsc": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from 'vue';
|
||||
import {onBeforeMount} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useSystemStore} from '@/stores/systemStore';
|
||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {useThemeStore} from '@/stores/themeStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
import {useTranslationStore} from "@/stores/translationStore";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const translationStore = useTranslationStore();
|
||||
|
||||
// 应用启动时加载配置和初始化系统信息
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
@@ -24,7 +25,8 @@ onMounted(async () => {
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
themeStore.initializeTheme();
|
||||
await themeStore.initializeTheme();
|
||||
await translationStore.loadTranslators();
|
||||
|
||||
// 启动时检查更新
|
||||
await updateStore.checkOnStartup();
|
||||
|
||||
49
frontend/src/common/constant/translation.ts
Normal file
49
frontend/src/common/constant/translation.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 默认翻译配置
|
||||
*/
|
||||
export const DEFAULT_TRANSLATION_CONFIG = {
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译相关的错误消息
|
||||
*/
|
||||
export const TRANSLATION_ERRORS = {
|
||||
NO_TEXT: 'no text to translate',
|
||||
TRANSLATION_FAILED: 'translation failed',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译结果接口
|
||||
*/
|
||||
export interface TranslationResult {
|
||||
translatedText: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言信息接口
|
||||
*/
|
||||
export interface LanguageInfo {
|
||||
Code: string; // 语言代码
|
||||
Name: string; // 语言名称
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译器扩展配置
|
||||
*/
|
||||
export interface TranslatorConfig {
|
||||
/** 最小选择字符数才显示翻译按钮 */
|
||||
minSelectionLength: number;
|
||||
/** 最大翻译字符数 */
|
||||
maxTranslationLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
export const TRANSLATION_ICON_SVG = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
@@ -1,11 +1,11 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, reactive} from 'vue';
|
||||
import {SystemThemeType, ThemeType, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useEditorStore} from './editorStore';
|
||||
import {defaultDarkColors} from '@/views/editor/theme/dark';
|
||||
import {defaultLightColors} from '@/views/editor/theme/light';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { ThemeService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfigStore } from './configStore';
|
||||
import { useEditorStore } from './editorStore';
|
||||
import { defaultDarkColors } from '@/views/editor/theme/dark';
|
||||
import { defaultLightColors } from '@/views/editor/theme/light';
|
||||
|
||||
/**
|
||||
* 主题管理 Store
|
||||
@@ -14,25 +14,44 @@ import {defaultLightColors} from '@/views/editor/theme/light';
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 响应式状态 - 存储当前使用的主题颜色
|
||||
// 响应式状态
|
||||
const themeColors = reactive({
|
||||
darkTheme: { ...defaultDarkColors },
|
||||
lightTheme: { ...defaultLightColors }
|
||||
});
|
||||
|
||||
// 计算属性 - 当前选择的主题类型
|
||||
// 计算属性
|
||||
const currentTheme = computed(() =>
|
||||
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
|
||||
// 初始化主题颜色 - 从数据库加载
|
||||
// 获取默认主题颜色
|
||||
const getDefaultColors = (themeType: ThemeType) =>
|
||||
themeType === ThemeType.ThemeTypeDark ? defaultDarkColors : defaultLightColors;
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
const themeMap = {
|
||||
[SystemThemeType.SystemThemeAuto]: 'auto',
|
||||
[SystemThemeType.SystemThemeDark]: 'dark',
|
||||
[SystemThemeType.SystemThemeLight]: 'light'
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', themeMap[theme]);
|
||||
};
|
||||
|
||||
// 初始化主题颜色
|
||||
const initializeThemeColors = async () => {
|
||||
try {
|
||||
const themes = await ThemeService.GetDefaultThemes();
|
||||
|
||||
// 如果没有获取到主题数据,使用默认值
|
||||
if (!themes) {
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新主题颜色
|
||||
if (themes[ThemeType.ThemeTypeDark]) {
|
||||
Object.assign(themeColors.darkTheme, themes[ThemeType.ThemeTypeDark].colors);
|
||||
}
|
||||
@@ -47,17 +66,9 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
document.documentElement.setAttribute('data-theme',
|
||||
theme === SystemThemeType.SystemThemeAuto ? 'auto' :
|
||||
theme === SystemThemeType.SystemThemeDark ? 'dark' : 'light'
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化主题
|
||||
const initializeTheme = async () => {
|
||||
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
||||
const theme = currentTheme.value;
|
||||
applyThemeToDOM(theme);
|
||||
await initializeThemeColors();
|
||||
};
|
||||
@@ -69,27 +80,25 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
refreshEditorTheme();
|
||||
};
|
||||
|
||||
// 更新主题颜色
|
||||
const updateThemeColors = (darkColors: any = null, lightColors: any = null): boolean => {
|
||||
// 更新主题颜色 - 合并逻辑,减少重复代码
|
||||
const updateThemeColors = (darkColors?: any, lightColors?: any): boolean => {
|
||||
let hasChanges = false;
|
||||
|
||||
if (darkColors) {
|
||||
Object.entries(darkColors).forEach(([key, value]) => {
|
||||
if (value !== undefined && themeColors.darkTheme[key] !== value) {
|
||||
themeColors.darkTheme[key] = value;
|
||||
hasChanges = true;
|
||||
const updateColors = (target: any, source: any) => {
|
||||
if (!source) return false;
|
||||
|
||||
let changed = false;
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
if (value !== undefined && target[key] !== value) {
|
||||
target[key] = value;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
if (lightColors) {
|
||||
Object.entries(lightColors).forEach(([key, value]) => {
|
||||
if (value !== undefined && themeColors.lightTheme[key] !== value) {
|
||||
themeColors.lightTheme[key] = value;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
hasChanges = updateColors(themeColors.darkTheme, darkColors) || hasChanges;
|
||||
hasChanges = updateColors(themeColors.lightTheme, lightColors) || hasChanges;
|
||||
|
||||
return hasChanges;
|
||||
};
|
||||
@@ -100,8 +109,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
|
||||
const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
|
||||
|
||||
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors);
|
||||
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors);
|
||||
await Promise.all([
|
||||
ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors),
|
||||
ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to save theme colors:', error);
|
||||
throw error;
|
||||
@@ -117,11 +128,8 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
await ThemeService.ResetThemeColors(dbThemeType);
|
||||
|
||||
// 2. 更新内存中的颜色状态
|
||||
if (themeType === 'darkTheme') {
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
} else {
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
}
|
||||
const defaultColors = getDefaultColors(dbThemeType);
|
||||
Object.assign(themeColors[themeType], defaultColors);
|
||||
|
||||
// 3. 刷新编辑器主题
|
||||
refreshEditorTheme();
|
||||
@@ -136,13 +144,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
// 刷新编辑器主题
|
||||
const refreshEditorTheme = () => {
|
||||
// 使用当前主题重新应用DOM主题
|
||||
const theme = currentTheme.value;
|
||||
applyThemeToDOM(theme);
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
if (editorStore) {
|
||||
editorStore.applyThemeSettings();
|
||||
}
|
||||
editorStore?.applyThemeSettings();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,593 +1,117 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import {ref} from 'vue';
|
||||
import {TranslationService} from '@/../bindings/voidraft/internal/services';
|
||||
import {franc} from 'franc-min';
|
||||
import {LanguageInfo, TRANSLATION_ERRORS, TranslationResult} from '@/common/constant/translation';
|
||||
|
||||
export interface TranslationResult {
|
||||
sourceText: string;
|
||||
translatedText: string;
|
||||
sourceLang: string;
|
||||
targetLang: string;
|
||||
translatorType: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 639-3 到 ISO 639-1/2 语言代码的映射
|
||||
* franc-min 返回的是 ISO 639-3 代码,需要转换为翻译API常用的 ISO 639-1/2 代码
|
||||
*/
|
||||
const ISO_LANGUAGE_MAP: Record<string, string> = {
|
||||
// 常见语言
|
||||
'cmn': 'zh', // 中文 (Mandarin Chinese)
|
||||
'eng': 'en', // 英文 (English)
|
||||
'jpn': 'ja', // 日语 (Japanese)
|
||||
'kor': 'ko', // 韩语 (Korean)
|
||||
'fra': 'fr', // 法语 (French)
|
||||
'deu': 'de', // 德语 (German)
|
||||
'spa': 'es', // 西班牙语 (Spanish)
|
||||
'rus': 'ru', // 俄语 (Russian)
|
||||
'ita': 'it', // 意大利语 (Italian)
|
||||
'nld': 'nl', // 荷兰语 (Dutch)
|
||||
'por': 'pt', // 葡萄牙语 (Portuguese)
|
||||
'vie': 'vi', // 越南语 (Vietnamese)
|
||||
'arb': 'ar', // 阿拉伯语 (Arabic)
|
||||
'hin': 'hi', // 印地语 (Hindi)
|
||||
'ben': 'bn', // 孟加拉语 (Bengali)
|
||||
'tha': 'th', // 泰语 (Thai)
|
||||
'tur': 'tr', // 土耳其语 (Turkish)
|
||||
'heb': 'he', // 希伯来语 (Hebrew)
|
||||
'pol': 'pl', // 波兰语 (Polish)
|
||||
'swe': 'sv', // 瑞典语 (Swedish)
|
||||
'fin': 'fi', // 芬兰语 (Finnish)
|
||||
'dan': 'da', // 丹麦语 (Danish)
|
||||
'ron': 'ro', // 罗马尼亚语 (Romanian)
|
||||
'hun': 'hu', // 匈牙利语 (Hungarian)
|
||||
'ces': 'cs', // 捷克语 (Czech)
|
||||
'ell': 'el', // 希腊语 (Greek)
|
||||
'bul': 'bg', // 保加利亚语 (Bulgarian)
|
||||
'cat': 'ca', // 加泰罗尼亚语 (Catalan)
|
||||
'ukr': 'uk', // 乌克兰语 (Ukrainian)
|
||||
'hrv': 'hr', // 克罗地亚语 (Croatian)
|
||||
'ind': 'id', // 印尼语 (Indonesian)
|
||||
'mal': 'ms', // 马来语 (Malay)
|
||||
'nob': 'no', // 挪威语 (Norwegian)
|
||||
'lat': 'la', // 拉丁语 (Latin)
|
||||
'lit': 'lt', // 立陶宛语 (Lithuanian)
|
||||
'slk': 'sk', // 斯洛伐克语 (Slovak)
|
||||
'slv': 'sl', // 斯洛文尼亚语 (Slovenian)
|
||||
'srp': 'sr', // 塞尔维亚语 (Serbian)
|
||||
'est': 'et', // 爱沙尼亚语 (Estonian)
|
||||
'lav': 'lv', // 拉脱维亚语 (Latvian)
|
||||
'fil': 'tl', // 菲律宾语/他加禄语 (Filipino/Tagalog)
|
||||
|
||||
// 未知/不确定
|
||||
'und': 'auto' // 未知语言
|
||||
};
|
||||
|
||||
// 语言代码的通用映射关系,适用于大部分翻译器
|
||||
const COMMON_LANGUAGE_ALIASES: Record<string, string[]> = {
|
||||
'zh': ['zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant', 'chinese', 'zhong'],
|
||||
'en': ['en-US', 'en-GB', 'english', 'eng'],
|
||||
'ja': ['jp', 'jpn', 'japanese'],
|
||||
'ko': ['kr', 'kor', 'korean'],
|
||||
'fr': ['fra', 'french'],
|
||||
'de': ['deu', 'german', 'ger'],
|
||||
'es': ['spa', 'spanish', 'esp'],
|
||||
'ru': ['rus', 'russian'],
|
||||
'pt': ['por', 'portuguese'],
|
||||
'it': ['ita', 'italian'],
|
||||
'nl': ['nld', 'dutch'],
|
||||
'ar': ['ara', 'arabic'],
|
||||
'hi': ['hin', 'hindi'],
|
||||
'th': ['tha', 'thai'],
|
||||
'tr': ['tur', 'turkish'],
|
||||
'vi': ['vie', 'vietnamese'],
|
||||
'id': ['ind', 'indonesian'],
|
||||
'ms': ['mal', 'malay'],
|
||||
'fi': ['fin', 'finnish'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译存储
|
||||
*/
|
||||
export const useTranslationStore = defineStore('translation', () => {
|
||||
// 状态
|
||||
const availableTranslators = ref<string[]>([]);
|
||||
const isTranslating = ref(false);
|
||||
const lastResult = ref<TranslationResult | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
// 语言列表 - 将类型设置为any以避免类型错误
|
||||
const languageMaps = ref<Record<string, Record<string, any>>>({});
|
||||
|
||||
// 语言使用频率计数 - 使用pinia持久化
|
||||
const languageUsageCount = ref<Record<string, number>>({});
|
||||
// 最近使用的翻译语言 - 最多记录10个
|
||||
const recentLanguages = ref<string[]>([]);
|
||||
|
||||
// 默认配置
|
||||
// 注意:确保默认值在初始化和持久化后正确设置
|
||||
const defaultTargetLang = ref('zh');
|
||||
const defaultTranslator = ref('bing');
|
||||
// 检测到的源语言,初始为空字符串表示尚未检测
|
||||
const detectedSourceLang = ref('');
|
||||
|
||||
// 计算属性
|
||||
const hasTranslators = computed(() => availableTranslators.value.length > 0);
|
||||
const currentLanguageMap = computed(() => {
|
||||
return languageMaps.value[defaultTranslator.value] || {};
|
||||
});
|
||||
|
||||
// 监听默认语言变更,确保目标语言在当前翻译器支持的范围内
|
||||
watch([defaultTranslator], () => {
|
||||
// 当切换翻译器时,验证默认目标语言是否支持
|
||||
if (Object.keys(languageMaps.value).length > 0) {
|
||||
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
||||
if (validatedLang !== defaultTargetLang.value) {
|
||||
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
|
||||
defaultTargetLang.value = validatedLang;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 基础状态
|
||||
const translators = ref<string[]>([]);
|
||||
const isTranslating = ref<boolean>(false);
|
||||
// 语言映射
|
||||
const translatorLanguages = ref<Record<string, Record<string, LanguageInfo>>>({});
|
||||
|
||||
/**
|
||||
* 加载可用翻译器
|
||||
* 加载可用翻译器列表并预先加载所有语言映射
|
||||
*/
|
||||
const loadAvailableTranslators = async (): Promise<void> => {
|
||||
const loadTranslators = async (): Promise<void> => {
|
||||
try {
|
||||
const translators = await TranslationService.GetAvailableTranslators();
|
||||
availableTranslators.value = translators;
|
||||
|
||||
// 如果默认翻译器不在可用列表中,则使用第一个可用的翻译器
|
||||
if (translators.length > 0 && !translators.includes(defaultTranslator.value)) {
|
||||
defaultTranslator.value = translators[0];
|
||||
}
|
||||
|
||||
// 加载所有翻译器的语言列表
|
||||
await Promise.all(translators.map(loadTranslatorLanguages));
|
||||
|
||||
// 在加载完所有语言列表后,确保默认目标语言有效
|
||||
if (defaultTargetLang.value) {
|
||||
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
||||
if (validatedLang !== defaultTargetLang.value) {
|
||||
defaultTargetLang.value = validatedLang;
|
||||
translators.value = await TranslationService.GetTranslators();
|
||||
|
||||
const loadPromises = translators.value.map(async (translatorType) => {
|
||||
try {
|
||||
const languages = await TranslationService.GetTranslatorLanguages(translatorType as any);
|
||||
if (languages) {
|
||||
translatorLanguages.value[translatorType] = languages;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to preload languages for ${translatorType}:`, err);
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
error.value = 'no available translators';
|
||||
});
|
||||
|
||||
// 等待所有语言映射加载完成
|
||||
await Promise.all(loadPromises);
|
||||
} catch (err) {
|
||||
console.error('Failed to load available translators:', err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载指定翻译器的语言列表
|
||||
* @param translatorType 翻译器类型
|
||||
*/
|
||||
const loadTranslatorLanguages = async (translatorType: string): Promise<void> => {
|
||||
try {
|
||||
const languages = await TranslationService.GetTranslatorLanguages(translatorType as any);
|
||||
|
||||
if (languages) {
|
||||
languageMaps.value[translatorType] = languages;
|
||||
translatorLanguages.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
|
||||
text: string,
|
||||
sourceLang: string,
|
||||
targetLang: string,
|
||||
translatorType: string
|
||||
): Promise<TranslationResult> => {
|
||||
// 使用提供的参数或默认值
|
||||
const targetLang = to || defaultTargetLang.value;
|
||||
const translator = translatorType || defaultTranslator.value;
|
||||
|
||||
// 处理空文本
|
||||
if (!text) {
|
||||
|
||||
if (!text.trim()) {
|
||||
return {
|
||||
sourceText: '',
|
||||
translatedText: '',
|
||||
sourceLang: '',
|
||||
targetLang: targetLang,
|
||||
translatorType: translator,
|
||||
error: 'no text to translate'
|
||||
error: TRANSLATION_ERRORS.NO_TEXT
|
||||
};
|
||||
}
|
||||
|
||||
// 检测源语言
|
||||
const detected = detectLanguage(text);
|
||||
if (detected) {
|
||||
detectedSourceLang.value = detected;
|
||||
}
|
||||
|
||||
// 使用检测到的语言或回退到auto
|
||||
let actualSourceLang = detectedSourceLang.value || 'auto';
|
||||
// 特殊处理有道翻译器:有道翻译器允许源语言和目标语言都是auto
|
||||
const isYoudaoTranslator = translatorType === 'youdao';
|
||||
const bothAuto = sourceLang === 'auto' && targetLang === 'auto';
|
||||
|
||||
// 确认语言代码有效并针对当前翻译器进行匹配
|
||||
actualSourceLang = validateLanguage(actualSourceLang, translator);
|
||||
const actualTargetLang = validateLanguage(targetLang, translator);
|
||||
|
||||
// 如果源语言和目标语言相同,则直接返回原文
|
||||
if (actualSourceLang !== 'auto' && actualSourceLang === actualTargetLang) {
|
||||
if (sourceLang === targetLang && !(isYoudaoTranslator && bothAuto)) {
|
||||
return {
|
||||
sourceText: text,
|
||||
translatedText: text,
|
||||
sourceLang: actualSourceLang,
|
||||
targetLang: actualTargetLang,
|
||||
translatorType: translator
|
||||
translatedText: text
|
||||
};
|
||||
}
|
||||
|
||||
isTranslating.value = true;
|
||||
error.value = null;
|
||||
|
||||
|
||||
try {
|
||||
console.log(`翻译文本: 从 ${actualSourceLang} 到 ${actualTargetLang} 使用 ${translator} 翻译器`);
|
||||
|
||||
// 调用翻译服务
|
||||
const translatedText = await TranslationService.TranslateWith(
|
||||
text,
|
||||
actualSourceLang,
|
||||
actualTargetLang,
|
||||
translator
|
||||
text,
|
||||
sourceLang,
|
||||
targetLang,
|
||||
translatorType
|
||||
);
|
||||
|
||||
// 增加目标语言的使用频率,使用较大的权重
|
||||
incrementLanguageUsage(actualTargetLang, 3);
|
||||
|
||||
// 如果源语言不是auto,也记录其使用情况,但权重较小
|
||||
if (actualSourceLang !== 'auto') {
|
||||
incrementLanguageUsage(actualSourceLang, 1);
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
const result: TranslationResult = {
|
||||
sourceText: text,
|
||||
translatedText,
|
||||
sourceLang: actualSourceLang,
|
||||
targetLang: actualTargetLang,
|
||||
translatorType: translator
|
||||
|
||||
return {
|
||||
translatedText
|
||||
};
|
||||
|
||||
lastResult.value = result;
|
||||
return result;
|
||||
} catch (err) {
|
||||
// 处理错误
|
||||
const errorMessage = err instanceof Error ? err.message : 'translation failed';
|
||||
const errorMessage = err instanceof Error ? err.message : TRANSLATION_ERRORS.TRANSLATION_FAILED;
|
||||
|
||||
error.value = errorMessage;
|
||||
|
||||
const result: TranslationResult = {
|
||||
sourceText: text,
|
||||
return {
|
||||
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,
|
||||
translators,
|
||||
isTranslating,
|
||||
lastResult,
|
||||
error,
|
||||
detectedSourceLang,
|
||||
defaultTargetLang,
|
||||
defaultTranslator,
|
||||
languageMaps,
|
||||
languageUsageCount,
|
||||
recentLanguages,
|
||||
|
||||
// 计算属性
|
||||
hasTranslators,
|
||||
currentLanguageMap,
|
||||
|
||||
translatorLanguages,
|
||||
|
||||
// 方法
|
||||
loadAvailableTranslators,
|
||||
loadTranslators,
|
||||
loadTranslatorLanguages,
|
||||
translateText,
|
||||
setDefaultConfig,
|
||||
detectLanguage,
|
||||
validateLanguage,
|
||||
findSimilarLanguage,
|
||||
getSortedLanguages,
|
||||
incrementLanguageUsage
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'voidraft-translation',
|
||||
storage: localStorage,
|
||||
pick: ['languageUsageCount', 'defaultTargetLang', 'defaultTranslator', 'recentLanguages']
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,364 +1,355 @@
|
||||
import { Extension, StateField, StateEffect } from '@codemirror/state';
|
||||
import { Extension, StateField, StateEffect, StateEffectType } 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 = "";
|
||||
import {
|
||||
TranslatorConfig,
|
||||
DEFAULT_TRANSLATION_CONFIG,
|
||||
TRANSLATION_ICON_SVG
|
||||
} from '@/common/constant/translation';
|
||||
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
const translationIconSvg = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
class TranslatorExtension {
|
||||
private config: TranslatorConfig;
|
||||
private setTranslationTooltip: StateEffectType<Tooltip | null>;
|
||||
private translationTooltipField: StateField<readonly Tooltip[]>;
|
||||
private translationButtonField: StateField<readonly Tooltip[]>;
|
||||
|
||||
// 用于设置翻译气泡的状态效果
|
||||
const setTranslationTooltip = StateEffect.define<Tooltip | null>();
|
||||
constructor(config?: Partial<TranslatorConfig>) {
|
||||
// 初始化配置
|
||||
this.config = {
|
||||
minSelectionLength: DEFAULT_TRANSLATION_CONFIG.minSelectionLength,
|
||||
maxTranslationLength: DEFAULT_TRANSLATION_CONFIG.maxTranslationLength,
|
||||
...config
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译气泡的状态字段
|
||||
*/
|
||||
const translationTooltipField = StateField.define<readonly Tooltip[]>({
|
||||
create() {
|
||||
return [];
|
||||
},
|
||||
update(tooltips, tr) {
|
||||
// 如果文档或选择变化,隐藏气泡
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查是否有特定的状态效果来更新tooltips
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setTranslationTooltip)) {
|
||||
return effect.value ? [effect.value] : [];
|
||||
}
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
// 初始化状态效果
|
||||
this.setTranslationTooltip = StateEffect.define<Tooltip | null>();
|
||||
|
||||
|
||||
/**
|
||||
* 根据当前选择获取翻译按钮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();
|
||||
// 初始化翻译气泡状态字段
|
||||
this.translationTooltipField = StateField.define<readonly Tooltip[]>({
|
||||
create: () => [],
|
||||
update: (tooltips, tr) => {
|
||||
// 检查是否有特定的状态效果来更新tooltips
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(this.setTranslationTooltip)) {
|
||||
return effect.value ? [effect.value] : [];
|
||||
}
|
||||
}
|
||||
|
||||
// 显示翻译气泡
|
||||
showTranslationTooltip(view);
|
||||
});
|
||||
// 如果文档或选择变化,隐藏气泡
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
|
||||
return { dom };
|
||||
}
|
||||
}];
|
||||
}
|
||||
// 初始化翻译按钮状态字段
|
||||
this.translationButtonField = StateField.define<readonly Tooltip[]>({
|
||||
create: (state) => this.getTranslationButtonTooltips(state),
|
||||
update: (tooltips, tr) => {
|
||||
// 如果文档或选择变化,重新计算tooltip
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return this.getTranslationButtonTooltips(tr.state);
|
||||
}
|
||||
|
||||
// 检查是否有翻译气泡显示,如果有则不显示按钮
|
||||
if (tr.state.field(this.translationTooltipField).length > 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示翻译气泡
|
||||
*/
|
||||
function showTranslationTooltip(view: EditorView) {
|
||||
if (!selectedTextForTranslation) return;
|
||||
|
||||
// 创建翻译气泡
|
||||
const tooltip = createTranslationTooltip(view, selectedTextForTranslation);
|
||||
|
||||
// 更新状态以显示气泡
|
||||
view.dispatch({
|
||||
effects: setTranslationTooltip.of(tooltip)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译按钮的状态字段
|
||||
*/
|
||||
const translationButtonField = StateField.define<readonly Tooltip[]>({
|
||||
create(state) {
|
||||
return getTranslationButtonTooltips(state);
|
||||
},
|
||||
|
||||
update(tooltips, tr) {
|
||||
// 如果文档或选择变化,重新计算tooltip
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return getTranslationButtonTooltips(tr.state);
|
||||
}
|
||||
/**
|
||||
* 根据当前选择获取翻译按钮tooltip
|
||||
*/
|
||||
private getTranslationButtonTooltips(state: any): readonly Tooltip[] {
|
||||
// 如果气泡已显示,则不显示按钮
|
||||
if (state.field(this.translationTooltipField).length > 0) return [];
|
||||
|
||||
// 检查是否有翻译气泡显示,如果有则不显示按钮
|
||||
if (tr.state.field(translationTooltipField).length > 0) {
|
||||
const selection = state.selection.main;
|
||||
|
||||
// 如果没有选中文本,不显示按钮
|
||||
if (selection.empty) return [];
|
||||
|
||||
// 获取选中的文本
|
||||
const selectedText = state.sliceDoc(selection.from, selection.to);
|
||||
|
||||
// 检查文本是否只包含空格
|
||||
if (!selectedText.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
// 检查文本长度条件
|
||||
if (selectedText.length < this.config.minSelectionLength ||
|
||||
selectedText.length > this.config.maxTranslationLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 返回翻译按钮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 = TRANSLATION_ICON_SVG;
|
||||
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
// 点击事件
|
||||
dom.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 显示翻译气泡
|
||||
this.showTranslationTooltip(view);
|
||||
});
|
||||
|
||||
return { dom };
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示翻译气泡
|
||||
*/
|
||||
private showTranslationTooltip(view: EditorView) {
|
||||
// 直接从当前选择获取文本
|
||||
const selection = view.state.selection.main;
|
||||
if (selection.empty) return;
|
||||
|
||||
const selectedText = view.state.sliceDoc(selection.from, selection.to);
|
||||
if (!selectedText.trim()) return;
|
||||
|
||||
// 创建翻译气泡
|
||||
const tooltip = createTranslationTooltip(view, selectedText);
|
||||
|
||||
// 更新状态以显示气泡
|
||||
view.dispatch({
|
||||
effects: this.setTranslationTooltip.of(tooltip)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建扩展
|
||||
*/
|
||||
createExtension(): Extension {
|
||||
return [
|
||||
// 翻译按钮tooltip
|
||||
this.translationButtonField,
|
||||
// 翻译气泡tooltip
|
||||
this.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",
|
||||
userSelect: "none",
|
||||
cursor: "grab"
|
||||
},
|
||||
|
||||
// 拖拽状态样式
|
||||
".cm-translation-dragging": {
|
||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)",
|
||||
zIndex: "1000",
|
||||
cursor: "grabbing !important"
|
||||
},
|
||||
|
||||
".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 function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
|
||||
// 更新配置
|
||||
currentConfig = { ...defaultConfig, ...config };
|
||||
|
||||
return [
|
||||
// 翻译按钮tooltip
|
||||
translationButtonField,
|
||||
// 翻译气泡tooltip
|
||||
translationTooltipField,
|
||||
|
||||
// 添加基础样式
|
||||
EditorView.baseTheme({
|
||||
".cm-translator-button": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
background: "var(--bg-secondary, transparent)",
|
||||
color: "var(--text-muted, #4285f4)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
padding: "2px",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
||||
userSelect: "none",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译气泡样式
|
||||
".cm-translation-tooltip": {
|
||||
background: "var(--bg-secondary, #fff)",
|
||||
color: "var(--text-primary, #333)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
padding: "8px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "200px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
|
||||
fontSize: "11px"
|
||||
},
|
||||
|
||||
".cm-translation-header": {
|
||||
marginBottom: "8px",
|
||||
flexShrink: "0"
|
||||
},
|
||||
|
||||
".cm-translation-controls": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "nowrap"
|
||||
},
|
||||
|
||||
".cm-translation-select": {
|
||||
padding: "2px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, #f5f5f5)",
|
||||
fontSize: "11px",
|
||||
color: "var(--text-primary, #333)",
|
||||
flex: "1",
|
||||
minWidth: "0",
|
||||
maxWidth: "80px"
|
||||
},
|
||||
|
||||
".cm-translation-swap": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
flexShrink: "0",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 滚动容器
|
||||
".cm-translation-scroll-container": {
|
||||
overflowY: "auto",
|
||||
flex: "1",
|
||||
minHeight: "0"
|
||||
},
|
||||
|
||||
".cm-translation-result": {
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
},
|
||||
|
||||
".cm-translation-result-header": {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: "4px"
|
||||
},
|
||||
|
||||
".cm-translation-result-wrapper": {
|
||||
position: "relative",
|
||||
width: "100%"
|
||||
},
|
||||
|
||||
".cm-translation-copy-btn": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
position: "absolute",
|
||||
top: "4px",
|
||||
right: "4px",
|
||||
zIndex: "2",
|
||||
opacity: "0.7",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
|
||||
opacity: "1"
|
||||
},
|
||||
"&.copied": {
|
||||
background: "var(--bg-success, #4caf50)",
|
||||
color: "white",
|
||||
border: "1px solid var(--bg-success, #4caf50)",
|
||||
opacity: "1"
|
||||
}
|
||||
},
|
||||
|
||||
".cm-translation-target": {
|
||||
padding: "6px",
|
||||
paddingRight: "28px", // 为复制按钮留出空间
|
||||
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
|
||||
color: "var(--text-primary, #333)",
|
||||
borderRadius: "3px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word"
|
||||
},
|
||||
|
||||
".cm-translation-notice": {
|
||||
fontSize: "10px",
|
||||
color: "var(--text-muted, #888)",
|
||||
padding: "2px 0",
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginBottom: "2px"
|
||||
},
|
||||
|
||||
".cm-translation-error": {
|
||||
color: "var(--text-danger, #d32f2f)",
|
||||
fontStyle: "italic"
|
||||
},
|
||||
|
||||
".cm-translation-loading": {
|
||||
padding: "8px",
|
||||
textAlign: "center",
|
||||
color: "var(--text-muted, #666)",
|
||||
fontSize: "11px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "6px"
|
||||
},
|
||||
|
||||
".cm-translation-loading::before": {
|
||||
content: "''",
|
||||
display: "inline-block",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
border: "2px solid var(--text-muted, #666)",
|
||||
borderTopColor: "transparent",
|
||||
animation: "cm-translation-spin 1s linear infinite"
|
||||
},
|
||||
|
||||
"@keyframes cm-translation-spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" }
|
||||
}
|
||||
})
|
||||
];
|
||||
const translatorExtension = new TranslatorExtension(config);
|
||||
return translatorExtension.createExtension();
|
||||
}
|
||||
|
||||
export default createTranslatorExtension;
|
||||
export default createTranslatorExtension;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -158,14 +158,12 @@ 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,
|
||||
};
|
||||
|
||||
@@ -8,12 +8,39 @@ import SettingItem from '../components/SettingItem.vue';
|
||||
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { defaultDarkColors } from '@/views/editor/theme/dark';
|
||||
import { defaultLightColors } from '@/views/editor/theme/light';
|
||||
import { createDebounce } from '@/common/utils/debounce';
|
||||
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||
import PickColors from 'vue-pick-colors';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// 创建防抖函数实例
|
||||
const { debouncedFn: debouncedUpdateColor } = createDebounce(
|
||||
(colorKey: string, value: string) => updateLocalColor(colorKey, value),
|
||||
{ delay: 100 }
|
||||
);
|
||||
|
||||
const { debouncedFn: debouncedResetTheme } = createDebounce(
|
||||
async () => {
|
||||
const themeType = activeThemeType.value;
|
||||
const success = await themeStore.resetThemeColors(themeType);
|
||||
|
||||
if (success) {
|
||||
tempColors.value = {
|
||||
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||
};
|
||||
hasUnsavedChanges.value = false;
|
||||
}
|
||||
},
|
||||
{ delay: 300 }
|
||||
);
|
||||
|
||||
// 创建定时器管理器
|
||||
const resetTimer = createTimerManager();
|
||||
|
||||
// 添加临时颜色状态
|
||||
const tempColors = ref({
|
||||
darkTheme: { ...defaultDarkColors },
|
||||
@@ -25,36 +52,19 @@ const hasUnsavedChanges = ref(false);
|
||||
|
||||
// 重置按钮状态
|
||||
const resetButtonState = ref({
|
||||
confirming: false,
|
||||
timer: null as number | null
|
||||
confirming: false
|
||||
});
|
||||
|
||||
// 防抖函数
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: number | undefined;
|
||||
|
||||
return function(...args: Parameters<T>): void {
|
||||
clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => {
|
||||
func(...args);
|
||||
}, wait);
|
||||
};
|
||||
};
|
||||
// 当前激活的主题类型
|
||||
const isDarkMode = computed(() =>
|
||||
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
);
|
||||
|
||||
// 当前激活的主题类型(基于当前系统主题)
|
||||
const activeThemeType = computed(() => {
|
||||
const isDark =
|
||||
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
return isDark ? 'darkTheme' : 'lightTheme';
|
||||
});
|
||||
const activeThemeType = computed(() => isDarkMode.value ? 'darkTheme' : 'lightTheme');
|
||||
|
||||
// 当前主题的颜色配置 - 使用临时状态
|
||||
// 当前主题的颜色配置
|
||||
const currentColors = computed(() => {
|
||||
const themeType = activeThemeType.value;
|
||||
return tempColors.value[themeType] ||
|
||||
@@ -62,144 +72,85 @@ const currentColors = computed(() => {
|
||||
});
|
||||
|
||||
// 获取当前主题模式
|
||||
const currentThemeMode = computed(() => {
|
||||
const isDark =
|
||||
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
return isDark ? 'dark' : 'light';
|
||||
});
|
||||
const currentThemeMode = computed(() => isDarkMode.value ? 'dark' : 'light');
|
||||
|
||||
// 监听主题颜色变更,更新临时颜色
|
||||
// 监听主题颜色变更,
|
||||
watch(
|
||||
() => themeStore.themeColors,
|
||||
(newValue) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
tempColors.value = {
|
||||
darkTheme: { ...newValue.darkTheme },
|
||||
lightTheme: { ...newValue.lightTheme }
|
||||
};
|
||||
tempColors.value.darkTheme = { ...newValue.darkTheme };
|
||||
tempColors.value.lightTheme = { ...newValue.lightTheme };
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 初始化时加载主题颜色
|
||||
onMounted(() => {
|
||||
// 使用themeStore中的颜色作为初始值
|
||||
tempColors.value = {
|
||||
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||
};
|
||||
});
|
||||
|
||||
// 颜色配置分组
|
||||
const colorGroups = computed(() => [
|
||||
// 颜色配置
|
||||
const colorConfig = [
|
||||
{
|
||||
key: 'basic',
|
||||
title: t('settings.themeColors.basic'),
|
||||
colors: [
|
||||
{ key: 'background', label: t('settings.themeColors.background') },
|
||||
{ key: 'backgroundSecondary', label: t('settings.themeColors.backgroundSecondary') },
|
||||
{ key: 'surface', label: t('settings.themeColors.surface') }
|
||||
]
|
||||
colors: ['background', 'backgroundSecondary', 'surface']
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
title: t('settings.themeColors.text'),
|
||||
colors: [
|
||||
{ key: 'foreground', label: t('settings.themeColors.foreground') },
|
||||
{ key: 'foregroundSecondary', label: t('settings.themeColors.foregroundSecondary') },
|
||||
{ key: 'comment', label: t('settings.themeColors.comment') }
|
||||
]
|
||||
key: 'text',
|
||||
colors: ['foreground', 'foregroundSecondary', 'comment']
|
||||
},
|
||||
{
|
||||
key: 'syntax',
|
||||
title: t('settings.themeColors.syntax'),
|
||||
colors: [
|
||||
{ key: 'keyword', label: t('settings.themeColors.keyword') },
|
||||
{ key: 'string', label: t('settings.themeColors.string') },
|
||||
{ key: 'function', label: t('settings.themeColors.function') },
|
||||
{ key: 'number', label: t('settings.themeColors.number') },
|
||||
{ key: 'operator', label: t('settings.themeColors.operator') },
|
||||
{ key: 'variable', label: t('settings.themeColors.variable') },
|
||||
{ key: 'type', label: t('settings.themeColors.type') }
|
||||
]
|
||||
colors: ['keyword', 'string', 'function', 'number', 'operator', 'variable', 'type']
|
||||
},
|
||||
{
|
||||
key: 'interface',
|
||||
title: t('settings.themeColors.interface'),
|
||||
colors: [
|
||||
{ key: 'cursor', label: t('settings.themeColors.cursor') },
|
||||
{ key: 'selection', label: t('settings.themeColors.selection') },
|
||||
{ key: 'selectionBlur', label: t('settings.themeColors.selectionBlur') },
|
||||
{ key: 'activeLine', label: t('settings.themeColors.activeLine') },
|
||||
{ key: 'lineNumber', label: t('settings.themeColors.lineNumber') },
|
||||
{ key: 'activeLineNumber', label: t('settings.themeColors.activeLineNumber') }
|
||||
]
|
||||
colors: ['cursor', 'selection', 'selectionBlur', 'activeLine', 'lineNumber', 'activeLineNumber']
|
||||
},
|
||||
{
|
||||
key: 'border',
|
||||
title: t('settings.themeColors.border'),
|
||||
colors: [
|
||||
{ key: 'borderColor', label: t('settings.themeColors.borderColor') },
|
||||
{ key: 'borderLight', label: t('settings.themeColors.borderLight') }
|
||||
]
|
||||
colors: ['borderColor', 'borderLight']
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
title: t('settings.themeColors.search'),
|
||||
colors: [
|
||||
{ key: 'searchMatch', label: t('settings.themeColors.searchMatch') },
|
||||
{ key: 'matchingBracket', label: t('settings.themeColors.matchingBracket') }
|
||||
]
|
||||
colors: ['searchMatch', 'matchingBracket']
|
||||
}
|
||||
]);
|
||||
];
|
||||
|
||||
// 颜色配置分组
|
||||
const colorGroups = computed(() =>
|
||||
colorConfig.map(group => ({
|
||||
key: group.key,
|
||||
title: t(`settings.themeColors.${group.key}`),
|
||||
colors: group.colors.map(colorKey => ({
|
||||
key: colorKey,
|
||||
label: t(`settings.themeColors.${colorKey}`)
|
||||
}))
|
||||
}))
|
||||
);
|
||||
|
||||
// 处理重置按钮点击
|
||||
const handleResetClick = () => {
|
||||
if (resetButtonState.value.confirming) {
|
||||
// 如果已经在确认状态,执行重置操作
|
||||
resetCurrentTheme();
|
||||
|
||||
// 重置按钮状态
|
||||
|
||||
debouncedResetTheme();
|
||||
|
||||
resetButtonState.value.confirming = false;
|
||||
if (resetButtonState.value.timer !== null) {
|
||||
clearTimeout(resetButtonState.value.timer);
|
||||
resetButtonState.value.timer = null;
|
||||
}
|
||||
resetTimer.clear();
|
||||
} else {
|
||||
// 进入确认状态
|
||||
resetButtonState.value.confirming = true;
|
||||
|
||||
// 设置3秒后自动恢复
|
||||
resetButtonState.value.timer = window.setTimeout(() => {
|
||||
resetTimer.set(() => {
|
||||
resetButtonState.value.confirming = false;
|
||||
resetButtonState.value.timer = null;
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置当前主题为默认配置
|
||||
const resetCurrentTheme = debounce(async () => {
|
||||
// 使用themeStore的原子重置操作
|
||||
const themeType = activeThemeType.value;
|
||||
const success = await themeStore.resetThemeColors(themeType);
|
||||
|
||||
if (success) {
|
||||
// 更新临时颜色状态
|
||||
tempColors.value = {
|
||||
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||
};
|
||||
|
||||
// 标记没有未保存的更改
|
||||
hasUnsavedChanges.value = false;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 更新本地颜色配置 - 仅更新临时状态,不提交到后端
|
||||
// 更新本地颜色配置
|
||||
const updateLocalColor = (colorKey: string, value: string) => {
|
||||
const themeType = activeThemeType.value;
|
||||
|
||||
@@ -211,14 +162,10 @@ const updateLocalColor = (colorKey: string, value: string) => {
|
||||
[colorKey]: value
|
||||
}
|
||||
};
|
||||
|
||||
// 标记有未保存的更改
|
||||
|
||||
hasUnsavedChanges.value = true;
|
||||
};
|
||||
|
||||
// 防抖包装的颜色更新函数
|
||||
const updateColor = debounce(updateLocalColor, 100);
|
||||
|
||||
// 应用颜色更改到系统
|
||||
const applyChanges = async () => {
|
||||
try {
|
||||
@@ -290,7 +237,7 @@ const showPickerMap = ref<Record<string, boolean>>({});
|
||||
|
||||
// 颜色变更处理
|
||||
const handleColorChange = (colorKey: string, value: string) => {
|
||||
updateColor(colorKey, value);
|
||||
debouncedUpdateColor(colorKey, value);
|
||||
};
|
||||
|
||||
// 颜色选择器关闭处理
|
||||
@@ -374,7 +321,7 @@ const handlePickerClose = () => {
|
||||
<input
|
||||
type="text"
|
||||
:value="currentColors[color.key] || ''"
|
||||
@input="updateColor(color.key, ($event.target as HTMLInputElement).value)"
|
||||
@input="debouncedUpdateColor(color.key, ($event.target as HTMLInputElement).value)"
|
||||
class="color-text-input"
|
||||
:placeholder="t('settings.colorValue')"
|
||||
/>
|
||||
|
||||
2
go.mod
2
go.mod
@@ -9,7 +9,6 @@ require (
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
github.com/knadh/koanf/providers/structs v1.0.0
|
||||
github.com/knadh/koanf/v2 v2.2.2
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25
|
||||
golang.org/x/net v0.43.0
|
||||
@@ -82,7 +81,6 @@ require (
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
|
||||
43
go.sum
43
go.sum
@@ -1,11 +1,14 @@
|
||||
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
@@ -25,8 +28,12 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.0 h1:4zuFafc/qGpymx7umexxth2y2lJXoBR49c3uI0Hr+zU=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.0/go.mod h1:Pewm8hY7Xe1ne7P8irVBAFnXjTkRuxbbkMlBeTdumNQ=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -93,6 +100,10 @@ github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/json v1.0.0 h1:1pVR1JhMwbqSg5ICzU+surJmeBbdT4bQm7jjgnA+f8o=
|
||||
@@ -103,6 +114,8 @@ github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ
|
||||
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
||||
github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
||||
github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
|
||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -133,6 +146,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -144,8 +159,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
@@ -160,14 +173,20 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.31 h1:KoDwiLF4OnHx6zqm9nqmczj3pTMe4K9w2zAj9H412yM=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.31/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -178,8 +197,12 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
@@ -191,9 +214,13 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -209,6 +236,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
@@ -217,8 +246,12 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
@@ -230,8 +263,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -250,6 +281,8 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -260,6 +293,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
package translator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,68 +20,61 @@ import (
|
||||
type BingTranslator struct {
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
session *BingSession // Bing会话
|
||||
token *tokenInfo // Token信息
|
||||
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参数
|
||||
// tokenInfo 存储token信息
|
||||
type tokenInfo struct {
|
||||
Value string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// TranslateRequest 翻译请求结构
|
||||
type TranslateRequest struct {
|
||||
Text string `json:"Text"`
|
||||
}
|
||||
|
||||
// TranslateResponse 翻译响应结构
|
||||
type TranslateResponse struct {
|
||||
Translations []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"translations"`
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应结构
|
||||
type ErrorResponse struct {
|
||||
Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
bingDefaultTimeout = 30 * time.Second
|
||||
bingTranslatorURL = "https://cn.bing.com/translator"
|
||||
bingTranslateAPIURL = "https://cn.bing.com/ttranslatev3"
|
||||
bingTokenURL = "https://edge.microsoft.com/translate/auth"
|
||||
bingTranslateAPIURL = "https://api-edge.cognitive.microsofttranslator.com/translate"
|
||||
tokenValidDuration = 25 * time.Minute // Token有效期,比实际30分钟稍短以确保安全
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
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")
|
||||
)
|
||||
|
||||
// 用户代理列表
|
||||
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{
|
||||
httpClient: &http.Client{
|
||||
Timeout: bingDefaultTimeout,
|
||||
// 启用Cookie存储
|
||||
Jar: jar,
|
||||
},
|
||||
Timeout: bingDefaultTimeout,
|
||||
session: &BingSession{
|
||||
Headers: make(map[string]string),
|
||||
Cookie: make(map[string]string),
|
||||
},
|
||||
Timeout: bingDefaultTimeout,
|
||||
languages: initBingLanguages(),
|
||||
}
|
||||
|
||||
// 初始化会话
|
||||
translator.refreshSession()
|
||||
|
||||
return translator
|
||||
}
|
||||
|
||||
@@ -91,129 +83,13 @@ func initBingLanguages() map[string]LanguageInfo {
|
||||
// 创建语言映射表
|
||||
languages := make(map[string]LanguageInfo)
|
||||
|
||||
// 添加所有支持的语言
|
||||
// 基于 Microsoft Translator 支持的语言列表
|
||||
// 参考: https://learn.microsoft.com/en-us/azure/ai-services/translator/language-support
|
||||
// 添加支持的语言
|
||||
// 基于 Microsoft Translator API 支持的语言列表
|
||||
// 参考: https://docs.microsoft.com/en-us/azure/cognitive-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
|
||||
languages["zh-Hans"] = LanguageInfo{Code: "zh-Hans", Name: "Chinese (Simplified)"}
|
||||
languages["zh-Hant"] = LanguageInfo{Code: "zh-Hant", Name: "Chinese (Traditional)"}
|
||||
|
||||
return languages
|
||||
}
|
||||
@@ -224,91 +100,59 @@ func (t *BingTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// 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"
|
||||
// getToken 获取访问token
|
||||
func (t *BingTranslator) getToken(ctx context.Context) (string, error) {
|
||||
// 检查token是否有效
|
||||
if t.token != nil && time.Now().Before(t.token.ExpiresAt) {
|
||||
return t.token.Value, nil
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest("GET", bingTranslatorURL, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", bingTokenURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the creation request failed: %w", err)
|
||||
return "", fmt.Errorf("failed to create token request: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
for k, v := range t.session.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Accept-Language", "zh-TW,zh;q=0.9,ja;q=0.8,zh-CN;q=0.7,en-US;q=0.6,en;q=0.5")
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Pragma", "no-cache")
|
||||
req.Header.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
req.Header.Set("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Set("Sec-Fetch-Site", "none")
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrBingNetworkError, err)
|
||||
return "", fmt.Errorf("failed to get token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 保存Cookie
|
||||
for _, cookie := range resp.Cookies() {
|
||||
t.session.Cookie[cookie.Name] = cookie.Value
|
||||
// 检查状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("token request failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read response failed: %w", err)
|
||||
return "", fmt.Errorf("failed to read token response: %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)
|
||||
token := strings.TrimSpace(string(body))
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("empty token received")
|
||||
}
|
||||
|
||||
// 解析参数数组
|
||||
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)
|
||||
// 缓存token
|
||||
t.token = &tokenInfo{
|
||||
Value: token,
|
||||
ExpiresAt: time.Now().Add(tokenValidDuration),
|
||||
}
|
||||
|
||||
// 提取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
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
@@ -328,139 +172,94 @@ func (t *BingTranslator) TranslateWithParams(text string, params TranslationPara
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *BingTranslator) translate(text, from, to string) (string, error) {
|
||||
// 如果没有会话或关键参数缺失,刷新会话
|
||||
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 text == "" {
|
||||
return "", fmt.Errorf("text cannot be empty")
|
||||
}
|
||||
|
||||
// 生成随机IID
|
||||
randNum := rand.Intn(10) // 0-9的随机数
|
||||
iid := fmt.Sprintf("translator.5019.%d", 1+randNum%3) // 生成随机IID
|
||||
// 创建带超时的context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), t.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// 构建URL - 确保使用双&符号
|
||||
reqURL := fmt.Sprintf("%s?isVertical=1&&IG=%s&IID=%s",
|
||||
bingTranslateAPIURL, t.session.IG, iid)
|
||||
|
||||
// 标准化语言代码
|
||||
fromLang := t.GetStandardLanguageCode(from)
|
||||
toLang := t.GetStandardLanguageCode(to)
|
||||
|
||||
// 构建表单数据
|
||||
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)
|
||||
|
||||
formDataStr := formData.Encode()
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest("POST", reqURL, strings.NewReader(formDataStr))
|
||||
// 获取token
|
||||
token, err := t.getToken(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("The creation request failed: %w", err)
|
||||
return "", fmt.Errorf("failed to get token: %w", err)
|
||||
}
|
||||
|
||||
// 构建请求URL
|
||||
params := url.Values{}
|
||||
if from != "auto" {
|
||||
params.Set("from", from)
|
||||
}
|
||||
params.Set("to", to)
|
||||
params.Set("api-version", "3.0")
|
||||
params.Set("includeSentenceLength", "true")
|
||||
|
||||
fullURL := fmt.Sprintf("%s?%s", bingTranslateAPIURL, params.Encode())
|
||||
|
||||
// 构建请求体
|
||||
requestBody := []TranslateRequest{{Text: text}}
|
||||
jsonBody, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// 创建HTTP请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", fullURL, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
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,
|
||||
})
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrBingNetworkError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read response failed: %w", err)
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return "", ErrBingEmptyResponse
|
||||
// 检查HTTP状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 尝试解析响应
|
||||
var result interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
// 首先尝试解析为错误响应
|
||||
var errorResp ErrorResponse
|
||||
if err := json.Unmarshal(body, &errorResp); err == nil && errorResp.Error.Code != 0 {
|
||||
return "", fmt.Errorf("translation error %d: %s", errorResp.Error.Code, errorResp.Error.Message)
|
||||
}
|
||||
|
||||
// 解析翻译响应
|
||||
var translateResp []TranslateResponse
|
||||
if err := json.Unmarshal(body, &translateResp); err != nil {
|
||||
return "", fmt.Errorf("%w: %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(translateResp) == 0 || len(translateResp[0].Translations) == 0 {
|
||||
return "", ErrBingEmptyResponse
|
||||
}
|
||||
|
||||
// 应该是数组类型
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 合并所有翻译片段
|
||||
var result strings.Builder
|
||||
for i, translation := range translateResp[0].Translations {
|
||||
if i > 0 {
|
||||
result.WriteString(" ")
|
||||
}
|
||||
result.WriteString(translation.Text)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: The response format is not as expected", ErrBingParseError)
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// GetSupportedLanguages 获取翻译器支持的语言列表
|
||||
@@ -473,11 +272,3 @@ 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 // 如果没有找到映射,返回原始代码
|
||||
}
|
||||
|
||||
@@ -112,40 +112,8 @@ func initDeeplLanguages() 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
|
||||
@@ -157,11 +125,6 @@ func (t *DeeplTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// SetDeeplHost 设置DeepL主机
|
||||
func (t *DeeplTranslator) SetDeeplHost(host string) {
|
||||
t.DeeplHost = host
|
||||
}
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
func (t *DeeplTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
return t.translate(text, from.String(), to.String())
|
||||
@@ -319,9 +282,3 @@ 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)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
package translator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
@@ -24,405 +21,228 @@ var (
|
||||
ErrBadNetwork = errors.New("bad network, please check your internet connection")
|
||||
)
|
||||
|
||||
// GoogleTranslator Google翻译器结构体,统一管理翻译功能
|
||||
// 常量定义
|
||||
const (
|
||||
googleTranslateTKK = "448487.932609646" // 固定TKK值
|
||||
)
|
||||
|
||||
// GoogleTranslator 带token的Google翻译器(使用translate.google.com)
|
||||
type GoogleTranslator struct {
|
||||
GoogleHost string // Google服务主机
|
||||
vm *otto.Otto // JavaScript虚拟机
|
||||
ttk otto.Value // 翻译token缓存
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
languages map[string]LanguageInfo // 支持的语言列表
|
||||
}
|
||||
|
||||
// NewGoogleTranslator 创建一个新的Google翻译器实例
|
||||
// GoogleTranslatorTokenFree 无token的Google翻译器(使用translate.googleapis.com)
|
||||
type GoogleTranslatorTokenFree struct {
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
languages map[string]LanguageInfo // 支持的语言列表
|
||||
}
|
||||
|
||||
// NewGoogleTranslator 创建一个新的带token的Google翻译器实例
|
||||
func NewGoogleTranslator() *GoogleTranslator {
|
||||
translator := &GoogleTranslator{
|
||||
GoogleHost: "google.com",
|
||||
vm: otto.New(),
|
||||
Timeout: defaultTimeout,
|
||||
return &GoogleTranslator{
|
||||
httpClient: &http.Client{Timeout: defaultTimeout},
|
||||
Timeout: defaultTimeout,
|
||||
languages: initGoogleLanguages(),
|
||||
}
|
||||
|
||||
// 初始化ttk
|
||||
translator.ttk, _ = otto.ToValue("0")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// generateToken 生成翻译token
|
||||
func generateToken(query string) string {
|
||||
// 实现TypeScript中的token生成逻辑
|
||||
tkkSplited := strings.Split(googleTranslateTKK, ".")
|
||||
tkkIndex, _ := strconv.Atoi(tkkSplited[0])
|
||||
tkkKey, _ := strconv.Atoi(tkkSplited[1])
|
||||
|
||||
// 转换查询字符串为字节数组
|
||||
bytesArray := transformQuery(query)
|
||||
|
||||
// 计算hash
|
||||
encodingRound := tkkIndex
|
||||
for _, b := range bytesArray {
|
||||
encodingRound += int(b)
|
||||
encodingRound = shiftLeftOrRightThenSumOrXor(encodingRound, "+-a^+6")
|
||||
}
|
||||
encodingRound = shiftLeftOrRightThenSumOrXor(encodingRound, "+-3^+b+-f")
|
||||
|
||||
encodingRound ^= tkkKey
|
||||
if encodingRound <= 0 {
|
||||
encodingRound = (encodingRound & 2147483647) + 2147483648
|
||||
}
|
||||
|
||||
normalizedResult := encodingRound % 1000000
|
||||
return fmt.Sprintf("%d.%d", normalizedResult, normalizedResult^tkkIndex)
|
||||
}
|
||||
|
||||
// transformQuery 转换查询字符串
|
||||
func transformQuery(query string) []byte {
|
||||
var bytesArray []byte
|
||||
runes := []rune(query)
|
||||
|
||||
for i := 0; i < len(runes); i++ {
|
||||
charCode := int(runes[i])
|
||||
|
||||
if charCode < 128 {
|
||||
bytesArray = append(bytesArray, byte(charCode))
|
||||
} else if charCode < 2048 {
|
||||
bytesArray = append(bytesArray, byte((charCode>>6)|192))
|
||||
bytesArray = append(bytesArray, byte((charCode&63)|128))
|
||||
} else {
|
||||
if (charCode&64512) == 55296 && i+1 < len(runes) && (int(runes[i+1])&64512) == 56320 {
|
||||
charCode = 65536 + ((charCode & 1023) << 10) + (int(runes[i+1]) & 1023)
|
||||
i++
|
||||
bytesArray = append(bytesArray, byte((charCode>>18)|240))
|
||||
bytesArray = append(bytesArray, byte(((charCode>>12)&63)|128))
|
||||
} else {
|
||||
bytesArray = append(bytesArray, byte((charCode>>12)|224))
|
||||
}
|
||||
bytesArray = append(bytesArray, byte(((charCode>>6)&63)|128))
|
||||
bytesArray = append(bytesArray, byte((charCode&63)|128))
|
||||
}
|
||||
}
|
||||
|
||||
return bytesArray
|
||||
}
|
||||
|
||||
// shiftLeftOrRightThenSumOrXor 位运算操作
|
||||
func shiftLeftOrRightThenSumOrXor(num int, optString string) int {
|
||||
for i := 0; i < len(optString)-2; i += 3 {
|
||||
acc := int(optString[i+2])
|
||||
if acc >= 'a' {
|
||||
acc = acc - 87
|
||||
} else {
|
||||
acc = acc - '0'
|
||||
}
|
||||
|
||||
if optString[i+1] == '+' {
|
||||
acc = num >> uint(acc)
|
||||
} else {
|
||||
acc = num << uint(acc)
|
||||
}
|
||||
|
||||
if optString[i] == '+' {
|
||||
num = (num + acc) & 4294967295
|
||||
} else {
|
||||
num ^= acc
|
||||
}
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *GoogleTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// SetGoogleHost 设置Google主机
|
||||
func (t *GoogleTranslator) SetGoogleHost(host string) {
|
||||
t.GoogleHost = host
|
||||
}
|
||||
|
||||
// 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)
|
||||
return t.translate(text, from.String(), to.String())
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *GoogleTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
// 设置超时时间(如果有指定)
|
||||
if params.Timeout > 0 {
|
||||
t.SetTimeout(params.Timeout)
|
||||
}
|
||||
|
||||
return t.translate(text, params.From, params.To, true)
|
||||
return t.translate(text, params.From, params.To)
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
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'")
|
||||
from = "auto"
|
||||
}
|
||||
if _, err := language.Parse(to); err != nil {
|
||||
log.Println("[WARNING], '" + to + "' is a invalid language, switching to 'en'")
|
||||
to = "en"
|
||||
}
|
||||
// translate 执行实际翻译操作(带token版本)
|
||||
func (t *GoogleTranslator) translate(text, from, to string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), t.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// 生成token
|
||||
token := generateToken(text)
|
||||
|
||||
// 构建请求URL
|
||||
apiURL := "https://translate.google.com/translate_a/single"
|
||||
params := url.Values{}
|
||||
params.Set("client", "t")
|
||||
params.Set("sl", from)
|
||||
params.Set("tl", to)
|
||||
params.Set("hl", to)
|
||||
params.Set("ie", "UTF-8")
|
||||
params.Set("oe", "UTF-8")
|
||||
params.Set("otf", "1")
|
||||
params.Set("ssel", "0")
|
||||
params.Set("tsel", "0")
|
||||
params.Set("kc", "7")
|
||||
params.Set("q", text)
|
||||
params.Set("tk", token)
|
||||
|
||||
// 添加dt参数
|
||||
dtParams := []string{"at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t"}
|
||||
for _, dt := range dtParams {
|
||||
params.Add("dt", dt)
|
||||
}
|
||||
|
||||
textValue, _ := otto.ToValue(text)
|
||||
urlStr := fmt.Sprintf("https://translate.%s/translate_a/single", t.GoogleHost)
|
||||
token := t.getToken(textValue)
|
||||
fullURL := apiURL + "?" + params.Encode()
|
||||
|
||||
data := map[string]string{
|
||||
"client": "gtx",
|
||||
"sl": from,
|
||||
"tl": to,
|
||||
"hl": to,
|
||||
"ie": "UTF-8",
|
||||
"oe": "UTF-8",
|
||||
"otf": "1",
|
||||
"ssel": "0",
|
||||
"tsel": "0",
|
||||
"kc": "7",
|
||||
"q": text,
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parameters := url.Values{}
|
||||
for k, v := range data {
|
||||
parameters.Add(k, v)
|
||||
}
|
||||
for _, v := range []string{"at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t"} {
|
||||
parameters.Add("dt", v)
|
||||
}
|
||||
|
||||
parameters.Add("tk", token)
|
||||
u.RawQuery = parameters.Encode()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API error: status code %d", r.StatusCode)
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var resp []interface{}
|
||||
err = json.Unmarshal(raw, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
responseText := ""
|
||||
for _, obj := range resp[0].([]interface{}) {
|
||||
if len(obj.([]interface{})) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t, ok := obj.([]interface{})[0].(string)
|
||||
if ok {
|
||||
responseText += t
|
||||
}
|
||||
}
|
||||
|
||||
return responseText, nil
|
||||
}
|
||||
|
||||
// getToken 获取翻译API所需的token
|
||||
func (t *GoogleTranslator) getToken(text otto.Value) string {
|
||||
ttk, err := t.updateTTK()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
tk, err := t.generateToken(text, ttk)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Replace(tk.String(), "&tk=", "", -1)
|
||||
}
|
||||
|
||||
// updateTTK 更新TTK值
|
||||
func (t *GoogleTranslator) updateTTK() (otto.Value, error) {
|
||||
timestamp := time.Now().UnixNano() / 3600000
|
||||
now := math.Floor(float64(timestamp))
|
||||
ttk, err := strconv.ParseFloat(t.ttk.String(), 64)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
if ttk == now {
|
||||
return t.ttk, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://translate.%s", t.GoogleHost), nil)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
return "", ErrBadNetwork
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
return "", err
|
||||
}
|
||||
|
||||
matches := regexp.MustCompile(`tkk:\s?'(.+?)'`).FindStringSubmatch(string(body))
|
||||
if len(matches) > 0 {
|
||||
v, err := otto.ToValue(matches[0])
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
t.ttk = v
|
||||
return v, nil
|
||||
// 解析JSON响应
|
||||
var result []interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return t.ttk, nil
|
||||
}
|
||||
|
||||
// generateToken 生成翻译API所需的token
|
||||
func (t *GoogleTranslator) generateToken(a otto.Value, TTK otto.Value) (otto.Value, error) {
|
||||
err := t.vm.Set("x", a)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
if len(result) == 0 {
|
||||
return "", errors.New("unexpected response format")
|
||||
}
|
||||
|
||||
_ = t.vm.Set("internalTTK", TTK)
|
||||
// 提取翻译文本
|
||||
translations, ok := result[0].([]interface{})
|
||||
if !ok {
|
||||
return "", errors.New("unexpected response format")
|
||||
}
|
||||
|
||||
result, err := t.vm.Run(`
|
||||
function sM(a) {
|
||||
var b;
|
||||
if (null !== yr)
|
||||
b = yr;
|
||||
else {
|
||||
b = wr(String.fromCharCode(84));
|
||||
var c = wr(String.fromCharCode(75));
|
||||
b = [b(), b()];
|
||||
b[1] = c();
|
||||
b = (yr = window[b.join(c())] || "") || ""
|
||||
}
|
||||
var d = wr(String.fromCharCode(116))
|
||||
, c = wr(String.fromCharCode(107))
|
||||
, d = [d(), d()];
|
||||
d[1] = c();
|
||||
c = "&" + d.join("") + "=";
|
||||
d = b.split(".");
|
||||
b = Number(d[0]) || 0;
|
||||
for (var e = [], f = 0, g = 0; g < a.length; g++) {
|
||||
var l = a.charCodeAt(g);
|
||||
128 > l ? e[f++] = l : (2048 > l ? e[f++] = l >> 6 | 192 : (55296 == (l & 64512) && g + 1 < a.length && 56320 == (a.charCodeAt(g + 1) & 64512) ? (l = 65536 + ((l & 1023) << 10) + (a.charCodeAt(++g) & 1023),
|
||||
e[f++] = l >> 18 | 240,
|
||||
e[f++] = l >> 12 & 63 | 128) : e[f++] = l >> 12 | 224,
|
||||
e[f++] = l >> 6 & 63 | 128),
|
||||
e[f++] = l & 63 | 128)
|
||||
}
|
||||
a = b;
|
||||
for (f = 0; f < e.length; f++)
|
||||
a += e[f],
|
||||
a = xr(a, "+-a^+6");
|
||||
a = xr(a, "+-3^+b+-f");
|
||||
a ^= Number(d[1]) || 0;
|
||||
0 > a && (a = (a & 2147483647) + 2147483648);
|
||||
a %= 1E6;
|
||||
return c + (a.toString() + "." + (a ^ b))
|
||||
}
|
||||
|
||||
var yr = null;
|
||||
var wr = function(a) {
|
||||
return function() {
|
||||
return a
|
||||
var translatedText strings.Builder
|
||||
for _, translation := range translations {
|
||||
if chunk, ok := translation.([]interface{}); ok && len(chunk) > 0 {
|
||||
if text, ok := chunk[0].(string); ok {
|
||||
translatedText.WriteString(text)
|
||||
}
|
||||
}
|
||||
, xr = function(a, b) {
|
||||
for (var c = 0; c < b.length - 2; c += 3) {
|
||||
var d = b.charAt(c + 2)
|
||||
, d = "a" <= d ? d.charCodeAt(0) - 87 : Number(d)
|
||||
, d = "+" == b.charAt(c + 1) ? a >>> d : a << d;
|
||||
a = "+" == b.charAt(c) ? a + d & 4294967295 : a ^ d
|
||||
}
|
||||
return a
|
||||
};
|
||||
|
||||
var window = {
|
||||
TKK: internalTTK
|
||||
};
|
||||
|
||||
sM(x)
|
||||
`)
|
||||
if err != nil {
|
||||
return otto.UndefinedValue(), err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return translatedText.String(), nil
|
||||
}
|
||||
|
||||
// GetSupportedLanguages 获取翻译器支持的语言列表
|
||||
@@ -436,8 +256,24 @@ func (t *GoogleTranslator) IsLanguageSupported(languageCode string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetStandardLanguageCode 获取标准化的语言代码
|
||||
func (t *GoogleTranslator) GetStandardLanguageCode(languageCode string) string {
|
||||
// 简单返回小写版本作为标准代码
|
||||
return strings.ToLower(languageCode)
|
||||
// visitArrayItems 递归访问数组项
|
||||
func visitArrayItems(arr []interface{}, visitor func(interface{})) {
|
||||
for _, obj := range arr {
|
||||
if subArr, ok := obj.([]interface{}); ok {
|
||||
visitArrayItems(subArr, visitor)
|
||||
} else {
|
||||
visitor(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedLanguages 获取翻译器支持的语言列表
|
||||
func (t *GoogleTranslatorTokenFree) GetSupportedLanguages() map[string]LanguageInfo {
|
||||
return t.languages
|
||||
}
|
||||
|
||||
// IsLanguageSupported 检查指定的语言代码是否受支持
|
||||
func (t *GoogleTranslatorTokenFree) IsLanguageSupported(languageCode string) bool {
|
||||
_, ok := t.languages[strings.ToLower(languageCode)]
|
||||
return ok
|
||||
}
|
||||
|
||||
103
internal/common/translator/google_translator_free.go
Normal file
103
internal/common/translator/google_translator_free.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package translator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/text/language"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewGoogleTranslatorTokenFree 创建一个新的无token的Google翻译器实例
|
||||
func NewGoogleTranslatorTokenFree() *GoogleTranslatorTokenFree {
|
||||
return &GoogleTranslatorTokenFree{
|
||||
httpClient: &http.Client{Timeout: defaultTimeout},
|
||||
Timeout: defaultTimeout,
|
||||
languages: initGoogleLanguages(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *GoogleTranslatorTokenFree) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// Translate 使用Go语言提供的标准语言标签进行文本翻译
|
||||
func (t *GoogleTranslatorTokenFree) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
return t.translate(text, from.String(), to.String())
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *GoogleTranslatorTokenFree) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
if params.Timeout > 0 {
|
||||
t.SetTimeout(params.Timeout)
|
||||
}
|
||||
return t.translate(text, params.From, params.To)
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作(无token版本)
|
||||
func (t *GoogleTranslatorTokenFree) translate(text, from, to string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), t.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// 构建请求URL(无token版本)
|
||||
apiURL := "https://translate.googleapis.com/translate_a/t"
|
||||
params := url.Values{}
|
||||
params.Set("client", "dict-chrome-ex")
|
||||
params.Set("sl", from)
|
||||
params.Set("tl", to)
|
||||
params.Set("q", text)
|
||||
|
||||
fullURL := apiURL + "?" + params.Encode()
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", ErrBadNetwork
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var result []interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 提取翻译文本
|
||||
var translatedTexts []string
|
||||
visitArrayItems(result, func(obj interface{}) {
|
||||
if text, ok := obj.(string); ok {
|
||||
translatedTexts = append(translatedTexts, text)
|
||||
}
|
||||
})
|
||||
|
||||
if len(translatedTexts) == 0 {
|
||||
return "", errors.New("no translation found")
|
||||
}
|
||||
|
||||
// 返回第一个翻译结果
|
||||
return translatedTexts[0], nil
|
||||
}
|
||||
210
internal/common/translator/tartunlp_translator.go
Normal file
210
internal/common/translator/tartunlp_translator.go
Normal file
@@ -0,0 +1,210 @@
|
||||
// Package translator 提供文本翻译功能
|
||||
package translator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// TartuNLPTranslator TartuNLP翻译器结构体
|
||||
type TartuNLPTranslator struct {
|
||||
httpClient *http.Client // HTTP客户端
|
||||
Timeout time.Duration // 请求超时时间
|
||||
languages map[string]LanguageInfo // 支持的语言列表
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
tartuNLPDefaultTimeout = 300 * time.Second // 默认超时时间300秒
|
||||
tartuNLPAPIURL = "https://api.tartunlp.ai/translation/v2" // TartuNLP API地址
|
||||
tartuNLPLengthLimit = 5000 // 长度限制5000字符
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrTartuNLPNetworkError = errors.New("tartunlp translator network error")
|
||||
ErrTartuNLPUnsupportedLang = errors.New("tartunlp translator unsupported language")
|
||||
ErrTartuNLPResponseError = errors.New("tartunlp translator response error")
|
||||
ErrTartuNLPLengthExceeded = errors.New("tartunlp translator text length exceeded")
|
||||
)
|
||||
|
||||
// TartuNLPRequest TartuNLP请求结构体
|
||||
type TartuNLPRequest struct {
|
||||
Text []string `json:"text"` // 要翻译的文本数组
|
||||
Src string `json:"src"` // 源语言
|
||||
Tgt string `json:"tgt"` // 目标语言
|
||||
}
|
||||
|
||||
// TartuNLPResponse TartuNLP响应结构体
|
||||
type TartuNLPResponse struct {
|
||||
Result []string `json:"result"` // 翻译结果数组
|
||||
}
|
||||
|
||||
// NewTartuNLPTranslator 创建一个新的TartuNLP翻译器实例
|
||||
func NewTartuNLPTranslator() *TartuNLPTranslator {
|
||||
translator := &TartuNLPTranslator{
|
||||
httpClient: &http.Client{
|
||||
Timeout: tartuNLPDefaultTimeout,
|
||||
},
|
||||
Timeout: tartuNLPDefaultTimeout,
|
||||
languages: initTartuNLPLanguages(),
|
||||
}
|
||||
|
||||
return translator
|
||||
}
|
||||
|
||||
// initTartuNLPLanguages 初始化TartuNLP翻译器支持的语言列表
|
||||
func initTartuNLPLanguages() map[string]LanguageInfo {
|
||||
// 创建语言映射表
|
||||
languages := make(map[string]LanguageInfo)
|
||||
|
||||
// 添加支持的语言
|
||||
// 基于 TartuNLP API 支持的语言列表
|
||||
languages["en"] = LanguageInfo{Code: "en", Name: "English"}
|
||||
languages["et"] = LanguageInfo{Code: "et", Name: "Estonian"}
|
||||
languages["de"] = LanguageInfo{Code: "de", Name: "German"}
|
||||
languages["lt"] = LanguageInfo{Code: "lt", Name: "Lithuanian"}
|
||||
languages["lv"] = LanguageInfo{Code: "lv", Name: "Latvian"}
|
||||
languages["fi"] = LanguageInfo{Code: "fi", Name: "Finnish"}
|
||||
languages["ru"] = LanguageInfo{Code: "ru", Name: "Russian"}
|
||||
languages["no"] = LanguageInfo{Code: "no", Name: "Norwegian"}
|
||||
languages["hu"] = LanguageInfo{Code: "hu", Name: "Hungarian"}
|
||||
languages["se"] = LanguageInfo{Code: "se", Name: "Swedish"}
|
||||
|
||||
return languages
|
||||
}
|
||||
|
||||
// SetTimeout 设置请求超时时间
|
||||
func (t *TartuNLPTranslator) SetTimeout(timeout time.Duration) {
|
||||
t.Timeout = timeout
|
||||
t.httpClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
func (t *TartuNLPTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
return t.translate(text, from.String(), to.String())
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
func (t *TartuNLPTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
|
||||
// 设置超时时间(如果有指定)
|
||||
if params.Timeout > 0 {
|
||||
t.SetTimeout(params.Timeout)
|
||||
}
|
||||
|
||||
return t.translate(text, params.From, params.To)
|
||||
}
|
||||
|
||||
// checkLengthLimit 检查文本长度是否超出限制
|
||||
func (t *TartuNLPTranslator) checkLengthLimit(text string) error {
|
||||
if len(text) > tartuNLPLengthLimit {
|
||||
return fmt.Errorf("%w: text length %d exceeds limit %d", ErrTartuNLPLengthExceeded, len(text), tartuNLPLengthLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *TartuNLPTranslator) translate(text, from, to string) (string, error) {
|
||||
if text == "" {
|
||||
return "", fmt.Errorf("text cannot be empty")
|
||||
}
|
||||
|
||||
// 检查文本长度限制
|
||||
if err := t.checkLengthLimit(text); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 转换语言代码为TartuNLP格式
|
||||
fromLower := strings.ToLower(from)
|
||||
toLower := strings.ToLower(to)
|
||||
|
||||
// 验证源语言支持
|
||||
if _, ok := t.languages[fromLower]; !ok {
|
||||
return "", fmt.Errorf("%w: source language '%s' not supported by TartuNLP", ErrTartuNLPUnsupportedLang, from)
|
||||
}
|
||||
|
||||
// 验证目标语言支持
|
||||
if _, ok := t.languages[toLower]; !ok {
|
||||
return "", fmt.Errorf("%w: target language '%s' not supported by TartuNLP", ErrTartuNLPUnsupportedLang, to)
|
||||
}
|
||||
|
||||
// 创建带超时的context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), t.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// 构建请求体
|
||||
request := TartuNLPRequest{
|
||||
Text: []string{text},
|
||||
Src: fromLower,
|
||||
Tgt: toLower,
|
||||
}
|
||||
|
||||
// 序列化请求
|
||||
jsonData, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// 创建HTTP请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tartuNLPAPIURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
// 发送请求
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrTartuNLPNetworkError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查HTTP状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("%w: HTTP %d - %s", ErrTartuNLPResponseError, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var response TartuNLPResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return "", fmt.Errorf("%w: failed to parse response: %v", ErrTartuNLPResponseError, err)
|
||||
}
|
||||
|
||||
// 检查响应结果
|
||||
if len(response.Result) == 0 {
|
||||
return "", fmt.Errorf("%w: empty translation result", ErrTartuNLPResponseError)
|
||||
}
|
||||
|
||||
return response.Result[0], nil
|
||||
}
|
||||
|
||||
// GetSupportedLanguages 获取翻译器支持的语言列表
|
||||
func (t *TartuNLPTranslator) GetSupportedLanguages() map[string]LanguageInfo {
|
||||
return t.languages
|
||||
}
|
||||
|
||||
// IsLanguageSupported 检查指定的语言代码是否受支持
|
||||
func (t *TartuNLPTranslator) IsLanguageSupported(languageCode string) bool {
|
||||
_, exists := t.languages[strings.ToLower(languageCode)]
|
||||
return exists
|
||||
}
|
||||
@@ -32,6 +32,8 @@ const (
|
||||
YoudaoTranslatorType TranslatorType = "youdao"
|
||||
// DeeplTranslatorType DeepL翻译器
|
||||
DeeplTranslatorType TranslatorType = "deepl"
|
||||
// TartuNLPTranslatorType TartuNLP翻译器
|
||||
TartuNLPTranslatorType TranslatorType = "tartunlp"
|
||||
)
|
||||
|
||||
// LanguageInfo 语言信息结构体
|
||||
@@ -56,9 +58,6 @@ type Translator interface {
|
||||
|
||||
// IsLanguageSupported 检查指定的语言代码是否受支持
|
||||
IsLanguageSupported(languageCode string) bool
|
||||
|
||||
// GetStandardLanguageCode 获取标准化的语言代码
|
||||
GetStandardLanguageCode(languageCode string) string
|
||||
}
|
||||
|
||||
// TranslatorFactory 翻译器工厂,用于创建不同类型的翻译器
|
||||
@@ -80,6 +79,8 @@ func (f *TranslatorFactory) Create(translatorType TranslatorType) (Translator, e
|
||||
return NewYoudaoTranslator(), nil
|
||||
case DeeplTranslatorType:
|
||||
return NewDeeplTranslator(), nil
|
||||
case TartuNLPTranslatorType:
|
||||
return NewTartuNLPTranslator(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported translator type: %s", translatorType)
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ func initYoudaoLanguages() map[string]LanguageInfo {
|
||||
// 创建语言映射表
|
||||
languages := make(map[string]LanguageInfo)
|
||||
|
||||
// 自动检测
|
||||
languages["auto"] = LanguageInfo{Code: "auto", Name: "Auto"}
|
||||
languages["auto"] = LanguageInfo{Code: "AUTO", Name: "Auto"}
|
||||
|
||||
return languages
|
||||
}
|
||||
@@ -64,8 +63,7 @@ func (t *YoudaoTranslator) SetTimeout(timeout time.Duration) {
|
||||
|
||||
// Translate 使用标准语言标签进行文本翻译
|
||||
func (t *YoudaoTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
|
||||
// 有道翻译不需要指定源语言和目标语言,它会自动检测
|
||||
return t.translate(text)
|
||||
return t.translate(text, to.String())
|
||||
}
|
||||
|
||||
// TranslateWithParams 使用简单字符串参数进行文本翻译
|
||||
@@ -75,16 +73,15 @@ func (t *YoudaoTranslator) TranslateWithParams(text string, params TranslationPa
|
||||
t.SetTimeout(params.Timeout)
|
||||
}
|
||||
|
||||
// 有道翻译不需要指定源语言和目标语言,它会自动检测
|
||||
return t.translate(text)
|
||||
return t.translate(text, params.To)
|
||||
}
|
||||
|
||||
// translate 执行实际翻译操作
|
||||
func (t *YoudaoTranslator) translate(text string) (string, error) {
|
||||
func (t *YoudaoTranslator) translate(text string, typeName string) (string, error) {
|
||||
// 构建表单数据
|
||||
form := url.Values{}
|
||||
form.Add("inputtext", text)
|
||||
form.Add("type", "AUTO")
|
||||
form.Add("type", typeName)
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest("POST", youdaoTranslateURL, strings.NewReader(form.Encode()))
|
||||
|
||||
@@ -89,14 +89,15 @@ func (s *TranslationService) TranslateWith(text string, from string, to string,
|
||||
return trans.TranslateWithParams(text, params)
|
||||
}
|
||||
|
||||
// GetAvailableTranslators 获取所有可用翻译器类型
|
||||
// GetTranslators 获取所有可用翻译器类型
|
||||
// @returns {[]string} 翻译器类型列表
|
||||
func (s *TranslationService) GetAvailableTranslators() []string {
|
||||
func (s *TranslationService) GetTranslators() []string {
|
||||
return []string{
|
||||
string(translator.GoogleTranslatorType),
|
||||
string(translator.BingTranslatorType),
|
||||
string(translator.GoogleTranslatorType),
|
||||
string(translator.YoudaoTranslatorType),
|
||||
string(translator.DeeplTranslatorType),
|
||||
string(translator.TartuNLPTranslatorType),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,12 +123,3 @@ func (s *TranslationService) IsLanguageSupported(translatorType translator.Trans
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user