Compare commits

...

122 Commits

Author SHA1 Message Date
Gabe
39b3b00117 release: v1.9.2 2025-08-10 23:00:21 +08:00
Gabe
763019f0c5 doc: update custom api help 2025-08-10 22:56:36 +08:00
Gabe
d743271be8 fix: show custom api name 2025-08-10 22:32:38 +08:00
Gabe
992dad26aa doc: update readme 2025-08-10 22:01:50 +08:00
Gabe
9bd0e67474 doc: update readme 2025-08-10 21:57:26 +08:00
Gabe
5767a4afb2 doc: update readme 2025-08-10 21:54:24 +08:00
Gabe
9e4c510684 doc: update custom api help 2025-08-10 21:50:33 +08:00
Gabe
16607fb069 doc: add custom api help 2025-08-10 21:40:58 +08:00
Gabe
e047a06432 doc: update readme 2025-08-10 20:12:12 +08:00
Gabe
9e09fd898a doc: update readme 2025-08-10 17:37:15 +08:00
Gabe
799c32a871 Merge remote-tracking branch 'origin/master' 2025-08-10 17:33:14 +08:00
Gabe
483f33b5c9 Merge pull request #269 from WilliamK7/patch-1
doc: update readme
2025-08-10 17:32:23 +08:00
Gabe
d444fd4fba fix: set max-width to loading svg 2025-08-10 17:28:28 +08:00
Gabe
0aae93ba2e doc: update help text 2025-08-10 16:56:34 +08:00
Gabe
9608bea3bf doc: update warn text 2025-08-10 16:44:57 +08:00
WilliamK7
a038a1ecdc doc: update readme
Remove duplicates in Chinese README
2025-08-10 13:56:29 +08:00
Gabe
c82cdd7f8f doc: update readme 2025-08-10 12:48:40 +08:00
Gabe
0c6d5c3c61 fix: replace deault tag from span to font 2025-08-09 22:32:41 +08:00
Gabe
900426f359 Merge branch 'master' into dev 2025-08-09 22:03:52 +08:00
Gabe
1d760fc93a Merge pull request #258 from mumu-lhl/push-mlqssmoqoqko
API: Replace max_tokens with max_completion_tokens
2025-08-09 22:03:12 +08:00
Gabe
61571e0f61 fix: remove browser contextMenus (issue #262) 2025-08-09 21:36:43 +08:00
Gabe
c9eb423c89 doc: update readme 2025-08-09 21:06:18 +08:00
Gabe
45b294a121 fix: retranslate loadmore text (issue #257) 2025-08-09 20:55:04 +08:00
Gabe
3a3f1fabe1 doc: update readme 2025-08-09 11:41:24 +08:00
Mumulhl
03177a09b3 API: Replace max_tokens with max_completion_tokens 2025-08-09 11:35:47 +08:00
Gabe
cae391f62b fix: zip script 2025-07-23 21:56:54 +08:00
Gabe
2a5e9db079 v1.9.1 2025-07-23 21:03:02 +08:00
Gabe
650d6e8b41 fix: workflows 2025-07-23 20:58:50 +08:00
Gabe
1daf134b31 fix: gemini api 2025-07-23 20:03:54 +08:00
Gabe
e1dfa35c6c v1.9.0 2025-07-23 00:38:33 +08:00
Gabe
73f80692d3 feat: add format script (prettier) 2025-07-03 19:08:33 +08:00
Gabe
42c7dae495 fix: log 2025-07-03 18:08:49 +08:00
Gabe
b2a1309caa feat: add gemini2 api 2025-07-02 21:54:18 +08:00
Gabe
94bf5f9580 fix: gemini api 2025-07-02 16:37:57 +08:00
Gabe
704ebdc9d7 fix: variable name 2025-07-02 13:38:30 +08:00
Gabe
165da4e559 fix: rule page: show more button 2025-07-01 23:34:16 +08:00
Gabe
d3e3b484bf fix: remove excess code 2025-07-01 22:47:04 +08:00
Gabe
192f8faa5b feat: support keypick for customize API 2025-07-01 22:42:57 +08:00
Gabe
579d5cb0a3 fix: update userscript 2025-07-01 17:55:57 +08:00
Gabe
07fca5b9af fix: #209 2025-07-01 17:03:52 +08:00
Gabe
866a63ab6c feat: move selected translation switch from setting to rule 2025-07-01 16:44:46 +08:00
Gabe
97b4935bc4 feat: api fetch timeout 2025-07-01 12:38:06 +08:00
Gabe
30129abef3 feat: custom API name 2025-07-01 10:54:30 +08:00
Gabe
24d904b32c feat: support volcengine api 2025-06-30 21:34:37 +08:00
Gabe
5f0ce57ead feat: qq transmart 2025-06-27 20:03:58 +08:00
Gabe
51f58d095a feat: add new google translate api (issue: 225, by: Bush2021) 2025-06-27 19:29:00 +08:00
Gabe
d22e3838c4 refactor: deepModels -> tinkIgnore 2025-06-27 16:33:30 +08:00
Gabe
adbb421b7b refactor: fetch timeout 2025-06-27 12:31:32 +08:00
Gabe
eaa47af269 fix: revert old google translate api 2025-06-26 11:13:51 +08:00
Gabe
a6cb5544f8 fix: browser.menus -> browser.contextMenus 2025-06-26 10:01:43 +08:00
Gabe
9e91faa660 Merge pull request #240 from unclemcz/dev
feat:ollama接口设置新增是否禁用深度思考参数
2025-06-25 20:42:31 +08:00
mcz
8636fadc72 接口设置ollama新增是否禁用深度思考参数 2025-06-03 23:07:10 +08:00
Gabe
0621957592 chore: thunderbird 2025-05-18 00:34:20 +08:00
Gabe
8ec06b0c84 chore: defined messenger in package.json 2025-05-18 00:29:06 +08:00
Gabe
d47f8d7ee9 Merge remote-tracking branch 'origin/dev' into dev 2025-05-17 23:22:09 +08:00
Gabe
24f8959525 fix: Ignore html comment elements 2025-05-17 23:19:38 +08:00
Gabe
983740578b Merge pull request #235 from unclemcz/dev
feat:基本设置增加请求超时参数&ollama接口配置增加<think>块忽略参数
2025-05-09 14:09:18 +08:00
Gabe
b5f79ed7cd Merge pull request #232 from Bush2021/fix-workflow
build: fix build errors caused by deprecated actions
2025-05-09 14:08:47 +08:00
Gabe
bbb0e79d4e Merge pull request #231 from Bush2021/fix-google-translate
fix: update API for Google Translate
2025-05-09 14:08:16 +08:00
mcz
471dc05897 ollama接口设置增加<think>块忽略参数 2025-05-01 23:41:08 +08:00
mcz
7a772d2459 在基本设置页面增加接口请求超时时间设置 2025-05-01 20:04:58 +08:00
mcz
1d92421960 Remove the <think></think> tags in qwen3 too. 2025-04-29 19:49:14 +08:00
Bush2021
aeaaf429d7 build: fix build errors caused by deprecated actions 2025-04-16 18:45:41 -04:00
Bush2021
84432e98ae fix: update API for Google Translate 2025-04-16 18:00:54 -04:00
Gabe
77c6102de7 Merge pull request #219 from unclemcz/dev
fix:(Ollama)Remove the <think></think> tags in deepseek-r1.
2025-03-12 17:18:26 +08:00
mcz
ab5dd82169 When using the deepseek-r1 model in Ollama, remove the content between the <think></think> tags. 2025-02-23 11:54:48 +08:00
Gabe
23e7b69dc5 Merge pull request #213 from htyxyt/master
采用更好的方式支持thunderbird
2025-02-19 18:09:01 +08:00
htyxyt
3dc8f393f2 Update package.json 2025-02-18 12:56:29 +08:00
htyxyt
8a2144f263 Update background.js 2025-02-18 12:44:06 +08:00
htyxyt
c1c59caa10 Update package.json 2025-02-18 12:29:43 +08:00
htyxyt
d27ebd90b6 Update package.json 2025-02-18 12:14:02 +08:00
htyxyt
467745c1e9 Update package.json 2025-02-18 11:58:39 +08:00
htyxyt
537378a038 Update background.js 2025-02-18 11:45:57 +08:00
htyxyt
4b5ed30e5b Delete src/background.thunderbird.js 2025-02-18 11:42:57 +08:00
htyxyt
52b7f6a225 Update background.js 2025-02-18 11:42:47 +08:00
htyxyt
f31675d8a2 Update index.js 2025-02-18 11:42:05 +08:00
htyxyt
dd46a8450c Update config-overrides.js 2025-02-18 11:41:28 +08:00
htyxyt
b0843f7d66 Update package.json 2025-02-18 11:39:58 +08:00
htyxyt
daadc0195c Update background.thunderbird.js 2025-02-18 10:25:12 +08:00
Gabe
298dec6957 Merge pull request #210 from htyxyt/master
添加对Thunderbird的支持
2025-02-17 23:48:40 +08:00
Gabe
bf39d85dfa Merge pull request #212 from qonmnop/patch-1
允许用户自定义 hooks 中重写signal 参数,自定义超时时间
2025-02-17 23:30:19 +08:00
htyxyt
30a9de25a8 Update config-overrides.js 2025-02-17 14:01:17 +08:00
htyxyt
af1ecf0bd4 Update package.json 2025-02-17 14:00:35 +08:00
qonmnop
fe55a2cd3c 允许用户自定义 hooks 中重写signal 参数,自定义超时时间 2025-02-17 12:14:10 +08:00
htyxyt
5a33d4e57e Add files via upload 2025-02-17 11:57:40 +08:00
htyxyt
7f46a9023c Delete public/background.thunderbird.js 2025-02-17 11:57:12 +08:00
htyxyt
dfd943b621 Rename manifest.thunderfird.json to manifest.thunderbird.json 2025-02-17 11:10:29 +08:00
htyxyt
7007d0d922 Update package.json 2025-02-17 10:39:08 +08:00
htyxyt
601678500d Update package.json 2025-02-14 15:23:03 +08:00
htyxyt
9bfb504381 Add files via upload 2025-02-14 15:13:40 +08:00
htyxyt
8a03b0cf15 Delete src/background.thunderfird.js 2025-02-14 15:12:57 +08:00
htyxyt
11ba89de0a Update manifest.thunderfird.json 2025-02-14 14:57:38 +08:00
htyxyt
bac7f62eea Update background.thunderfird.js 2025-02-14 14:56:58 +08:00
htyxyt
eef90ea02b Delete src/popup.thunderfird.js 2025-02-14 14:56:10 +08:00
htyxyt
0650df534a Delete src/content.thunderfird.js 2025-02-14 14:56:00 +08:00
htyxyt
9ef8c8b823 Delete src/options.thunderfird.js 2025-02-14 14:55:49 +08:00
htyxyt
d7e08da0b2 Add files via upload 2025-02-13 14:53:06 +08:00
htyxyt
ef361e0798 Add files via upload 2025-02-13 14:50:33 +08:00
Gabe
6855332092 fix: modify systemPrompt & userPrompt 2024-11-30 00:41:29 +08:00
Gabe
121d523e02 Merge remote-tracking branch 'origin/dev' into dev 2024-11-30 00:09:48 +08:00
Gabe
42a375c4c7 Merge pull request #188 from unclemcz/dev
给ollama增加system message
2024-11-30 00:07:14 +08:00
Gabe
a1dd705d97 fix: update pnpm-lock file 2024-11-29 21:14:30 +08:00
mcz
71f90b36ca 给ollama增加system message 2024-09-30 16:41:58 +08:00
Gabe
37facdc3c1 Merge pull request #186 from hoilc/dev
feat: enhance openai prompt
2024-09-26 23:38:34 +08:00
hoilc
66b4f547ff feat: enhance openai prompt 2024-09-25 14:03:12 +08:00
Gabe
d27b9c7f2d Merge pull request #185 from hoilc/dev
feat: support claude api
2024-09-24 23:30:00 +08:00
hoilc
278ff9c6bc feat: support claude api 2024-09-23 18:22:19 +08:00
Gabe Yuan
d6fe1ce9d7 fix: try detect language only when fromLang is auto 2024-05-30 21:05:05 +08:00
Gabe Yuan
0bfa5256b8 fix: simplify keepSelector logic 2024-05-30 17:18:39 +08:00
Gabe Yuan
72ccfc8aec v1.8.11 2024-05-23 20:06:48 +08:00
Gabe Yuan
d117c5dc10 feat: baidu dict can be disabled 2024-05-23 00:08:10 +08:00
Gabe Yuan
9312783f44 feat: lang detector can be selected 2024-05-22 23:33:30 +08:00
Gabe Yuan
e5b16ebfd3 Merge remote-tracking branch 'origin/master' into dev 2024-05-22 10:19:04 +08:00
Gabe Yuan
5d1d65c2d3 feat: the temperature and maxTokens of the openai can be configured 2024-05-21 23:15:46 +08:00
Gabe
9ca1309cec Merge pull request #126 from kebyn/master
fix: option translation
2024-05-21 20:02:53 +08:00
kebyn
a03afc05f5 fix: option translation 2024-05-21 08:56:09 +00:00
Gabe Yuan
0198963584 fix: deeplx: replace auto to blank string 2024-05-21 11:55:17 +08:00
Gabe Yuan
58e745d967 v1.8.10 2024-05-17 10:38:11 +08:00
Gabe Yuan
377e347d68 feat: support translate hooks 2024-05-15 11:07:13 +08:00
Gabe Yuan
bac0704d3d feat: download and upload settings 2024-05-12 20:24:40 +08:00
Gabe Yuan
d2ff46edf6 fix: show full gemini url 2024-05-12 16:25:20 +08:00
Gabe Yuan
f908372b4e feat: support hook for custom api 2024-05-12 16:10:11 +08:00
42 changed files with 9569 additions and 5277 deletions

2
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译 REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.8.9 REACT_APP_VERSION=1.9.2
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -7,28 +7,28 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v4
with: with:
version: 8.7.6 version: latest
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: "18.17.0" node-version: latest
cache: "pnpm" cache: "pnpm"
- run: pnpm install - run: pnpm install
- run: pnpm build - run: pnpm build+zip
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
with: with:
name: build-artifacts name: build-artifacts
path: build path: build
deploy-web: deploy-web:
needs: build needs: build
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: build-artifacts name: build-artifacts
path: build path: build
@@ -37,7 +37,8 @@ jobs:
with: with:
folder: build/web folder: build/web
create-release: create-release:
runs-on: ubuntu-22.04 needs: build
runs-on: ubuntu-latest
outputs: outputs:
upload_url: ${{ steps.create-release.outputs.upload_url }} upload_url: ${{ steps.create-release.outputs.upload_url }}
steps: steps:
@@ -54,18 +55,14 @@ jobs:
needs: [build, create-release] needs: [build, create-release]
strategy: strategy:
matrix: matrix:
client: ["chrome", "edge", "firefox", "userscript"] client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: build-artifacts name: build-artifacts
path: build path: build
- name: Zip Release
run: |
cd build
zip -r ${{ matrix.client }}.zip ${{ matrix.client }}
- uses: actions/upload-release-asset@v1 - uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
build
public
package.json

24
.prettierrc Normal file
View File

@@ -0,0 +1,24 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"singleAttributePerLine": false,
"bracketSameLine": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"embeddedLanguageFormatting": "auto",
"vueIndentScriptAndStyle": false,
"experimentalTernaries": false,
"parser": "babel"
}

View File

@@ -11,10 +11,18 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Keep it simple, smart - [x] Keep it simple, smart
- [x] Open source - [x] Open source
- [x] Adapt to common browsers - [x] Adapt to common browsers
- [x] Chrome/Edge/Firefox/Kiwi/Orion - [x] Chrome/Edge
- [x] Firefox
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [ ] Safari - [ ] Safari
- [x] Safari (Mac)
- [x] Thunderbird
- [x] Supports multiple translation services - [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft
- [x] Baidu/Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
- [x] DeepL/DeepLX/NiuTrans
- [x] Custom translation interface - [x] Custom translation interface
- [x] Covers common translation scenarios - [x] Covers common translation scenarios
- [x] Web bilingual translation - [x] Web bilingual translation
@@ -52,6 +60,8 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN) - [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/) - [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari - [ ] Safari
- [x] Safari (Mac) Compiled by a third party, not verified, obtained by yourself: https://www.nodeloc.com/t/topic/54245
- [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
- [x] GreaseMonkey Script - [x] GreaseMonkey Script
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js) - [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator) - [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
@@ -69,16 +79,112 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy) - Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you. - If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
- Deploy and manage by yourself. - Deploy and manage by yourself.
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- A word-marking translation plug-in used with this project. ## Frequently Asked Questions
- Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections. ### How to Turn Off Automatic Translation
You can achieve this through `Rules Setting` with the following methods:
- Personal Rules: RULES-> Global Rule -> Translate Switch -> Disaabled
- Subscription Rules: SUBSCRIBE -> Select the third option `kiss-rules-off.json`
- Override Subscription Rules: OVERWRITE -> Translate Switch -> Disaabled
- Add a Personal Rule for a Specific Website: Translate Switch -> Disaabled
### How to Set Keyboard Shortcuts
Set this in the extension management page, for example:
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
- firefox [about:addons](about:addons)
### How to Turn Off Selection Translation
Set this in the `Rules Setting`: RULES -> Global Rule -> If translate selected -> Disable
### How to Set it to Show Only the Translation
Set this in the `Rules Setting`: RULES -> Global Rule -> Show Only Translations -> Enable
### How to Set Mouse Hover Translation
Set this in the `Rules Setting`: RULES -> Global Rule -> TTrigger Mode
### Why are some web pages not fully translated?
This extension's webpage translation is based on CSS selectors. Generic rules cannot adapt to all websites, and sometimes you need to manually add site-specific rules. If you don't know how to write rules, you can seek help here:
https://github.com/fishjar/kiss-rules/issues
### What is the priority order of rule settings?
Personal Rules > Override Subscription Rules > Subscription Rules > Global Rules
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
### Why are YouTube subtitles translated in broken sentences?
This extension has no special development for video content. Support for YouTube is also treated as regular webpage translation. Auto-generated subtitles are streamed and output progressively, resulting in poorer support.
To disable this extension's subtitle translation, add a rule. Reference: https://github.com/fishjar/kiss-translator/issues/62
### Local Ollama interface cannot be used
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
### Custom API doesn't work in Tampermonkey scripts
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
### How to Set Up Hook Functions for Custom Interfaces
The custom interface feature is highly flexible and can theoretically integrate with any translation interface.
Example of a Request Hook function:
```js
/**
* Request Hook
* @param {string} text Text to be translated
* @param {string} from Source language
* @param {string} to Target language
* @param {string} url Translation interface URL
* @param {string} key Translation interface API key
* @returns {Array[string, object]} [Interface URL, request object]
*/
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
"Authorization": `Bearer ${key}`
},
method: "POST",
body: { text, to },
}]
```
Example of a Response Hook function:
```js
* Response Hook
* @param {string} res JSON data returned by the interface
* @param {string} text Text to be translated
* @param {string} from Source language
* @param {string} to Target language
* @returns {Array[string, boolean]} [Translated text, whether target language is same as source]
* Note: If the second return value is true (target language same as source),
* the translation will not be displayed on the page,
* If the parameters are incomplete, it is recommended to return false directly
*/
(res, text, from, to) => [res.text, to === res.src]
```
For more custom interface examples, refer to: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
## Development Guidelines ## Development Guidelines
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator cd kiss-translator
git checkout dev # Submit a PR suggestion to push to the dev branch
pnpm install pnpm install
pnpm build pnpm build
``` ```

117
README.md
View File

@@ -11,10 +11,18 @@
- [x] 保持简约 - [x] 保持简约
- [x] 开放源代码 - [x] 开放源代码
- [x] 适配常见浏览器 - [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi/Orion - [x] Chrome/Edge
- [x] Firefox
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [ ] Safari - [ ] Safari
- [x] Safari (Mac)
- [x] Thunderbird
- [x] 支持多种翻译服务 - [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft
- [x] Baidu/Tencent/Volcengine
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
- [x] DeepL/DeepLX/NiuTrans
- [x] 自定义翻译接口 - [x] 自定义翻译接口
- [x] 覆盖常见翻译场景 - [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译 - [x] 网页双语对照翻译
@@ -52,6 +60,8 @@
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN) - [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/) - [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari - [ ] Safari
- [x] Safari (Mac) 第三方编译,未作验证,自行获取: https://www.nodeloc.com/t/topic/54245
- [x] Thunderbird [下载地址](https://github.com/fishjar/kiss-translator/releases)
- [x] 油猴脚本 - [x] 油猴脚本
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js) - [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator) - [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
@@ -69,16 +79,111 @@
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy) - 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。 - 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
- 自己部署,自己管理。 - 自己部署,自己管理。
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- 搭配本项目一起使用的划词翻译插件。 ## 常见问题
- 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。 ### 如何关闭自动翻译
通过规则设置,以下方法均可实现:
- 个人规则:全局规则 -> 开启翻译 -> 默认关闭
- 订阅规则:选择第三个 `kiss-rules-off.json`
- 覆写订阅规则:开启翻译 -> 默认关闭
- 添加一条针对某个网站的个人规则:开启翻译 -> 默认关闭
### 如何设置快捷键
在插件管理那里设置,例如:
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
- firefox [about:addons](about:addons)
### 如何关闭划词翻译
通过规则设置:个人规则 -> 全局规则 -> 是否启用划词翻译 -> 禁用
### 如何设置仅显示译文
通过规则设置:个人规则 -> 全局规则 -> 仅显示译文 -> 启用
### 如何设置鼠标悬停翻译
通过规则设置:个人规则 -> 全局规则 -> 触发方式
### 为什么有些网页翻译不全
本插件的网页翻译是基于CSS选择器的通用规则不能适配所有网页有时需要自行添加相应网站的单独规则。如果不会写规则可以到这里求助 https://github.com/fishjar/kiss-rules/issues
### 规则设置的优先级是如何的
个人规则 > 覆写订阅规则 > 订阅规则 > 全局规则
其中全局规则优先级最低,但非常重要,相当于默认规则。
### 为什么油管字幕一句话会断开翻译
本插件目前没有针对视频做特殊开发,对油管的支持也是当做网页翻译看待,自动生成字幕是流式生成并输出的,所以支持较差。
如果需要关闭本插件的字幕翻译增加一条规则即可参考https://github.com/fishjar/kiss-translator/issues/62
### 本地的Ollama接口不能使用
如果出现403的情况参考https://github.com/fishjar/kiss-translator/issues/174
### 填写的接口在油猴脚本不能使用
油猴脚本需要增加域名白名单,否则不能发出请求。
### 如何设置自定义接口的hook函数
自定义接口功能非常灵活,理论可以接入任何翻译接口。
Request Hook 函数示例如下:
```js
/**
* Request Hook
* @param {string} text 需要翻译的原文
* @param {string} from 原文语言
* @param {string} to 译文语言
* @param {string} url 翻译接口地址
* @param {string} key 翻译接口密钥
* @returns {Array[string, object]} [接口地址, 请求参数对象]
*/
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
"Authorization": `Bearer ${key}`
},
method: "POST",
body: { text, to },
}]
```
Response Hook 函数示例如下:
```js
/**
* Request Hook
* @param {string} res 接口返回的json数据
* @param {string} text 需要翻译的原文
* @param {string} from 原文语言
* @param {string} to 译文语言
* @returns {Array[string, boolean]} [译文, 译文语言与原文语言是否相同]
* 注如果返回值第二个值为true译文语言与原文语言相同则译文不会在页面显示
* 参数不全的情况建议直接返回false
*/
(res, text, from, to) => [res.text, to === res.src]
```
更多的自定义接口示例,请参考: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
## 开发指引 ## 开发指引
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator cd kiss-translator
git checkout dev # 提交PR建议推送到dev分支
pnpm install pnpm install
pnpm build pnpm build
``` ```

View File

@@ -92,6 +92,7 @@ const userscriptWebpack = (config, env) => {
// @grant GM.info // @grant GM.info
// @grant unsafeWindow // @grant unsafeWindow
// @connect translate.googleapis.com // @connect translate.googleapis.com
// @connect translate-pa.googleapis.com
// @connect api-edge.cognitive.microsofttranslator.com // @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com // @connect edge.microsoft.com
// @connect api-free.deepl.com // @connect api-free.deepl.com
@@ -108,12 +109,10 @@ const userscriptWebpack = (config, env) => {
// @connect dav.jianguoyun.com // @connect dav.jianguoyun.com
// @connect fanyi.baidu.com // @connect fanyi.baidu.com
// @connect transmart.qq.com // @connect transmart.qq.com
// @connect localhost:3000 // @connect niutrans.com
// @connect 127.0.0.1:3000 // @connect translate.volcengine.com
// @connect localhost:1188 // @connect localhost
// @connect 127.0.0.1:1188 // @connect 127.0.0.1
// @connect localhost:11434
// @connect 127.0.0.1:11434
// @run-at document-end // @run-at document-end
// ==/UserScript== // ==/UserScript==

346
custom-api.md Normal file
View File

@@ -0,0 +1,346 @@
# 自定义接口示例
以下示例为网友提供,仅供学习参考。
## 本地运行 Seed-X-PPO-7B 量化模型
> 由网友 emptyghost6 提供来源https://linux.do/t/topic/828257
URL
```sh
http://localhost:8000/v1/completions
```
Request Hook
```js
(text, from, to, url, key) => {
// 模型支持的语言代码到完整名称的映射
const langFullNameMap = {
ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
};
// 将 Hook 系统的语言代码转换为模型 API 支持的代码
const getModelLangCode = (lang) => {
if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
return lang;
};
const sourceLangCode = getModelLangCode(from);
const targetLangCode = getModelLangCode(to);
const sourceLangName = langFullNameMap[sourceLangCode] || from;
const targetLangName = langFullNameMap[targetLangCode] || to;
const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;
// 构建请求体对象
const bodyObject = {
model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
prompt: prompt,
max_tokens: 2048,
temperature: 0.0,
};
// 返回最终的请求配置
return [url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// 关键改动:将 JavaScript 对象转换为 JSON 字符串
body: JSON.stringify(bodyObject),
}];
}
```
Response Hook
```js
(res, text, from, to) => {
// 检查返回是否有效
if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {
// 提取译文并去除可能存在的前后空格
const translatedText = res.choices[0].text.trim();
// 比较原文与译文,相同为 true否则为 false。
const areTextsIdentical = text.trim() === translatedText;
// 返回数组:[翻译后的文本, 是否与原文相同]
return [translatedText, areTextsIdentical];
}
// 如果响应格式不正确或没有结果,则抛出错误
throw new Error("Invalid API response format or no translation found.");
}
```
## 接入 openrouter
> 由网友 Rick Sanchez 提供
URL
```sh
https://openrouter.ai/api/v1/chat/completions
```
Request Hook
```js
(text, from, to, url, key) => [url, {
method: "POST",
headers: {
"Authorization": `Bearer ${key}`,
"Content-type": "application/json",
},
body: JSON.stringify({
"model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
"messages": [
{
"role": "user",
"content": //可自定义你的提示词
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.
## Translation Rules
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
6. Maintain the same level of formality or informality as the original.
Source Text: ${text}
Translated Text:`
}
]
})
}]
```
Response Hook
```js
(res, text, from, to) => [
res.choices?.[0]?.message?.content ?? "",
false
]
```
## 接入 gemini-2.5-flash, 关闭思考模式, 去审查
> 由网友 Rick Sanchez 提供
URL
```sh
https://generativelanguage.googleapis.com/v1beta/models
```
Request Hook
```js
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
"generationConfig": {
"temperature": 0.8,
"thinkingConfig": {
"thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
},
},
"safetySettings": [
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE",
}
],
"contents": [{
"parts": [{
"text": `自定义提示词`
}]
}],
}),
}]
```
Response Hook
```js
(res, text, from, to) => [
res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
false
]
```
## 接入 Qwen-MT
> 由网友 atom 提供
URL
```sh
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
```
Request Hook
```js
(text, from, to, url, key) => {
const mapLanguageCode = (lang) => ({
'zh-CN': 'zh',
'zh-TW': 'zh_tw',
})[lang] || lang;
const targetLang = mapLanguageCode(to);
return [
url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${key}`
},
body: JSON.stringify({
"model": "qwen-mt-turbo",
"messages": [
{
"role": "user",
"content": text
}
],
"translation_options": {
"source_lang": "auto",
"target_lang": targetLang
}
})
}
];
}
```
Response Hook
```js
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
```
## 接入 deepl 接口
> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236
Request Hook
```js
(text, from, to, url, key) => [
url,
{
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
text,
target_lang: "ZH",
source_lang: "auto",
}),
},
]
```
Response Hook
```js
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
```
## 接入智谱AI大模型
> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679
Request Hook
```js
(text, from, to, url, key) => [url, {
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": key
},
"body": JSON.stringify({
"model": "glm-4-flash",
"messages": [
{
"role":"system",
"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
},
{
"role": "user",
"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
}
]
})
}]
```
## 接入谷歌新接口
> 由网友 Bush2021 提供来源https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717
URL
```sh
https://translate-pa.googleapis.com/v1/translateHtml
```
KEY
```sh
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
```
Request Hook
```js
(text, from, to, url, key) => [url, {
method: "POST",
headers: {
"Content-Type": "application/json+protobuf",
"X-Goog-API-Key": key
},
body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
}]
```
Response Hook
```js
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
```

View File

@@ -1,7 +1,7 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.8.9", "version": "1.9.2",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -17,6 +17,7 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sval": "^0.5.2",
"webdav": "^5.3.0", "webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
}, },
@@ -25,13 +26,16 @@
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start", "start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html", "build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge", "build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
"build:thunderbird": "rm -rf build/thunderbird && BUILD_PATH=./build/thunderbird REACT_APP_CLIENT=thunderbird react-app-rewired build && rm build/thunderbird/content.html && cp ./build/thunderbird/manifest.thunderbird.json ./build/thunderbird/manifest.json && rm build/*/manifest.thunderbird.json",
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json", "build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build", "build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2", "build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/", "build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js", "build:rules": "babel-node src/rules.js",
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules", "build": "pnpm format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
"pack": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .", "zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && zip -r userscript.zip userscript && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
"build+zip": "pnpm build && pnpm zip",
"format": "prettier --write \"**/*.{js,json,html}\"",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
@@ -43,7 +47,8 @@
"globals": { "globals": {
"GM": true, "GM": true,
"unsafeWindow": true, "unsafeWindow": true,
"globalThis": true "globalThis": true,
"messenger": true
} }
}, },
"browserslist": { "browserslist": {
@@ -63,6 +68,7 @@
"@babel/node": "^7.22.19", "@babel/node": "^7.22.19",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.20", "@babel/preset-env": "^7.22.20",
"prettier": "3.6.2",
"react-app-rewired": "^2.2.1" "react-app-rewired": "^2.2.1"
} }
} }

12138
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@@ -16,7 +16,7 @@
} }
</style> </style>
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener('DOMContentLoaded', function () {
// (() => { // (() => {
// var shadow = document.querySelector("#shadow1"); // var shadow = document.querySelector("#shadow1");
// var root = shadow.attachShadow({ mode: "open" }); // var root = shadow.attachShadow({ mode: "open" });
@@ -54,8 +54,8 @@
// }, 1000); // }, 1000);
setTimeout(function () { setTimeout(function () {
var el = document.querySelector("h2>p>span"); var el = document.querySelector('h2>p>span');
el.innerText = "hello world"; el.innerText = 'hello world';
}, 1000); }, 1000);
}); });
</script> </script>
@@ -105,28 +105,26 @@
<br /> <br />
<div id="content"> <div id="content">
<p>You need to enable JavaScript to run <span>this app.</span></p> <p>You need to enable JavaScript to run <span>this app.</span></p>
The <span>embargo</span> has just lifted to confirm that AmpereOne is The <span>embargo</span> has just lifted to confirm that AmpereOne is coming to
coming to Google Cloud with the C3A instances. Google Cloud with the C3A instances.
<br /> <br />
But these upcoming instances for now are only in private preview form. But these upcoming instances for now are only in private preview form.
<br /> <br />
<br /> <br />
Needless to say I also haven't had any AmpereOne access to check out the Needless to say I also haven't had any AmpereOne access to check out the
performance and power efficiency of these new Arm server processors from performance and power efficiency of these new Arm server processors from Ampere
Ampere Computing. Computing.
<br /> <br />
</div> </div>
<h2> <h2>
<p> <p>
<span <span>React is a JavaScript library for building user interfaces.</span>
>React is a JavaScript library for building user interfaces.</span
>
</p> </p>
</h2> </h2>
<hr /> <hr />
<input id="input1" style="width: 80%;" /> <input id="input1" style="width: 80%" />
<hr /> <hr />
<textarea id="textarea1" style="width: 80%;">test</textarea> <textarea id="textarea1" style="width: 80%">test</textarea>
<hr /> <hr />
<div id="addtitle"></div> <div id="addtitle"></div>
<h2>Shadow 1</h2> <h2>Shadow 1</h2>
@@ -166,15 +164,47 @@
<br /> <br />
<br /> <br />
<h2> <h2>
React Server Components (or RSC) is a new application architecture React Server Components (or RSC) is a new application architecture designed by the
designed by the React team. React team.
</h2> </h2>
<iframe <iframe
id="iframe1" id="iframe1"
width="800px" width="800px"
height="600px" height="600px"
src="http://localhost:3000/index.html" src="http://localhost:3000/index.html"></iframe>
></iframe> <br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<h2>Weve first shared our research on RSC in an introductory talk and an RFC.</h2>
<br /> <br />
<br /> <br />
<br /> <br />
@@ -208,52 +238,10 @@
<br /> <br />
<br /> <br />
<h2> <h2>
Weve first shared our research on RSC in an introductory talk and an To recap them, we are introducing a new kind of component—Server Components—that
RFC. run ahead of time and are excluded from your JavaScript bundle.
</h2> </h2>
<br /> <iframe id="iframe2" width="800px" height="600px" src="https://react.dev/"></iframe>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<h2>
To recap them, we are introducing a new kind of component—Server
Components—that run ahead of time and are excluded from your JavaScript
bundle.
</h2>
<iframe
id="iframe2"
width="800px"
height="600px"
src="https://react.dev/"
></iframe>
<br /> <br />
<br /> <br />
<br /> <br />
@@ -288,15 +276,14 @@
<br /> <br />
<div class="cont cont1"> <div class="cont cont1">
<h2> <h2>
Server Components can run during the build, letting you read from the Server Components can run during the build, letting you read from the filesystem
filesystem or fetch static content. or fetch static content.
</h2> </h2>
<ul> <ul>
<li> <li>
They can also run on the server, letting you access your data layer They can also run on the server, letting you access your data layer without
without having to build an API. You can pass data by props from having to build an API. You can pass data by props from Server Components to
Server Components to the interactive Client Components in the the interactive Client Components in the browser.
browser.
</li> </li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li> <li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul> </ul>
@@ -315,14 +302,14 @@
<br /> <br />
<div class="cont cont2"> <div class="cont cont2">
<h2> <h2>
Since our last update, we have merged the React Server Components RFC Since our last update, we have merged the React Server Components RFC to ratify
to ratify the proposal. the proposal.
</h2> </h2>
<ul> <ul>
<li> <li>
RSC combines the simple “request/response” mental model of RSC combines the simple “request/response” mental model of server-centric
server-centric Multi-Page Apps with the seamless interactivity of Multi-Page Apps with the seamless interactivity of client-centric Single-Page
client-centric Single-Page Apps, giving you the best of both worlds. Apps, giving you the best of both worlds.
</li> </li>
<li> <li>
React 使创建交互式 UI React 使创建交互式 UI

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.8.9", "version": "1.9.2",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.8.9", "version": "1.9.2",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -45,12 +45,7 @@
"description": "__MSG_open_options__" "description": "__MSG_open_options__"
} }
}, },
"permissions": [ "permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest"],
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",

View File

@@ -0,0 +1,78 @@
{
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.9.2",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
"browser_specific_settings": {
"gecko": {
"id": "yugang2002@gmail.com",
"strict_min_version": "78.0"
}
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"js": ["content.js"],
"matches": ["<all_urls>"],
"all_frames": true
}
],
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"
},
"description": "__MSG_toggle_translate__"
},
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": {
"suggested_key": {
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"description": "__MSG_open_options__"
}
},
"permissions": [
"<all_urls>",
"storage",
"menus",
"messagesModify",
"scripting",
"declarativeNetRequest"
],
"icons": {
"16": "images/logo16.png",
"32": "images/logo32.png",
"48": "images/logo48.png",
"128": "images/logo128.png"
},
"browser_action": {
"default_icon": {
"128": "images/logo128.png"
},
"default_title": "__MSG_app_name__",
"default_popup": "popup.html"
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
}
}

View File

@@ -2,6 +2,7 @@ import queryString from "query-string";
import { fetchData } from "../libs/fetch"; import { fetchData } from "../libs/fetch";
import { import {
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
@@ -9,10 +10,13 @@ import {
OPT_TRANS_NIUTRANS, OPT_TRANS_NIUTRANS,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2, OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3, OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI, OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA, OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2, OPT_TRANS_OLLAMA_2,
@@ -24,6 +28,8 @@ import {
OPT_TRANS_CUSTOMIZE_5, OPT_TRANS_CUSTOMIZE_5,
URL_CACHE_TRAN, URL_CACHE_TRAN,
KV_SALT_SYNC, KV_SALT_SYNC,
URL_GOOGLE_TRAN,
URL_MICROSOFT_LANGDETECT,
URL_BAIDU_LANGDETECT, URL_BAIDU_LANGDETECT,
URL_BAIDU_SUGGEST, URL_BAIDU_SUGGEST,
URL_BAIDU_TTS, URL_BAIDU_TTS,
@@ -31,8 +37,12 @@ import {
URL_TENCENT_TRANSMART, URL_TENCENT_TRANSMART,
OPT_LANGS_TENCENT, OPT_LANGS_TENCENT,
OPT_LANGS_SPECIAL, OPT_LANGS_SPECIAL,
OPT_LANGS_MICROSOFT,
} from "../config"; } from "../config";
import { sha256 } from "../libs/utils"; import { sha256 } from "../libs/utils";
import interpreter from "../libs/interpreter";
import { msAuth } from "../libs/auth";
import { kissLog } from "../libs/log";
/** /**
* 同步数据 * 同步数据
@@ -58,6 +68,52 @@ export const apiSyncData = async (url, key, data) =>
*/ */
export const apiFetch = (url) => fetchData(url); export const apiFetch = (url) => fetchData(url);
/**
* Google语言识别
* @param {*} text
* @returns
*/
export const apiGoogleLangdetect = async (text) => {
const params = {
client: "gtx",
dt: "t",
dj: 1,
ie: "UTF-8",
sl: "auto",
tl: "zh-CN",
q: text,
};
const input = `${URL_GOOGLE_TRAN}?${queryString.stringify(params)}`;
const res = await fetchData(input, {
headers: {
"Content-type": "application/json",
},
useCache: true,
});
return res.src;
};
/**
* Microsoft语言识别
* @param {*} text
* @returns
*/
export const apiMicrosoftLangdetect = async (text) => {
const [token] = await msAuth();
const res = await fetchData(URL_MICROSOFT_LANGDETECT, {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
useCache: true,
});
return OPT_LANGS_MICROSOFT.get(res[0].language) ?? res[0].language;
};
/** /**
* 百度语言识别 * 百度语言识别
* @param {*} text * @param {*} text
@@ -171,7 +227,7 @@ export const apiTranslate = async ({
OPT_LANGS_SPECIAL[translator].get("auto"); OPT_LANGS_SPECIAL[translator].get("auto");
const to = OPT_LANGS_SPECIAL[translator].get(toLang); const to = OPT_LANGS_SPECIAL[translator].get(toLang);
if (!to) { if (!to) {
console.log(`[trans] target lang: ${toLang} not support`); kissLog(`target lang: ${toLang} not support`, "translate");
return [trText, isSame]; return [trText, isSame];
} }
@@ -207,6 +263,10 @@ export const apiTranslate = async ({
trText = res.sentences.map((item) => item.trans).join(" "); trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src; isSame = to === res.src;
break; break;
case OPT_TRANS_GOOGLE_2:
trText = res?.[0]?.[0] || "";
isSame = to === res.src;
break;
case OPT_TRANS_MICROSOFT: case OPT_TRANS_MICROSOFT:
trText = res trText = res
.map((item) => item.translations.map((item) => item.text).join(" ")) .map((item) => item.translations.map((item) => item.text).join(" "))
@@ -245,12 +305,17 @@ export const apiTranslate = async ({
} }
break; break;
case OPT_TRANS_TENCENT: case OPT_TRANS_TENCENT:
trText = res.auto_translation; trText = res?.auto_translation?.[0];
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_VOLCENGINE:
trText = res?.translation || "";
isSame = to === res?.detected_language;
break;
case OPT_TRANS_OPENAI: case OPT_TRANS_OPENAI:
case OPT_TRANS_OPENAI_2: case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3: case OPT_TRANS_OPENAI_3:
case OPT_TRANS_GEMINI_2:
trText = res?.choices?.map((item) => item.message.content).join(" "); trText = res?.choices?.map((item) => item.message.content).join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
@@ -260,6 +325,10 @@ export const apiTranslate = async ({
.join(" "); .join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_CLAUDE:
trText = res?.content?.map((item) => item.text).join(" ");
isSame = text === trText;
break;
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:
trText = res?.result?.translated_text; trText = res?.result?.translated_text;
isSame = text === trText; isSame = text === trText;
@@ -267,7 +336,13 @@ export const apiTranslate = async ({
case OPT_TRANS_OLLAMA: case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2: case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3: case OPT_TRANS_OLLAMA_3:
trText = res?.response; const { thinkIgnore = "" } = apiSetting;
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
if (deepModels.some((model) => res?.model?.startsWith(model))) {
trText = res?.response.replace(/<think>[\s\S]*<\/think>/i, "");
} else {
trText = res?.response;
}
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_CUSTOMIZE: case OPT_TRANS_CUSTOMIZE:
@@ -275,27 +350,14 @@ export const apiTranslate = async ({
case OPT_TRANS_CUSTOMIZE_3: case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4: case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5: case OPT_TRANS_CUSTOMIZE_5:
trText = res.text; const { resHook } = apiSetting;
isSame = to === res.from; if (resHook?.trim()) {
interpreter.run(`exports.resHook = ${resHook}`);
const { customOption } = apiSetting; [trText, isSame] = interpreter.exports.resHook(res, text, from, to);
if (customOption?.trim()) { } else {
try { trText = res.text;
const opt = JSON.parse(customOption); isSame = to === res.from;
const textPattern = opt.resPattern?.text;
const fromPattern = opt.resPattern?.from;
if (textPattern) {
trText = textPattern.split(".").reduce((pre, cur) => pre[cur], res);
}
if (fromPattern) {
isSame =
to === fromPattern.split(".").reduce((pre, cur) => pre[cur], res);
}
} catch (err) {
throw new Error(`custom option parse err: ${err}`);
}
} }
break; break;
default: default:
} }

View File

@@ -1,6 +1,7 @@
import queryString from "query-string"; import queryString from "query-string";
import { import {
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
@@ -8,10 +9,13 @@ import {
OPT_TRANS_NIUTRANS, OPT_TRANS_NIUTRANS,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2, OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3, OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI, OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA, OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2, OPT_TRANS_OLLAMA_2,
@@ -23,15 +27,18 @@ import {
OPT_TRANS_CUSTOMIZE_5, OPT_TRANS_CUSTOMIZE_5,
URL_MICROSOFT_TRAN, URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART, URL_TENCENT_TRANSMART,
URL_VOLCENGINE_TRAN,
INPUT_PLACE_URL, INPUT_PLACE_URL,
INPUT_PLACE_FROM, INPUT_PLACE_FROM,
INPUT_PLACE_TO, INPUT_PLACE_TO,
INPUT_PLACE_TEXT, INPUT_PLACE_TEXT,
INPUT_PLACE_KEY, INPUT_PLACE_KEY,
INPUT_PLACE_MODEL,
} from "../config"; } from "../config";
import { msAuth } from "../libs/auth"; import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl"; import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu"; import { genBaidu } from "./baidu";
import interpreter from "../libs/interpreter";
const keyMap = new Map(); const keyMap = new Map();
const urlMap = new Map(); const urlMap = new Map();
@@ -77,6 +84,20 @@ const genGoogle = ({ text, from, to, url, key }) => {
return [input, init]; return [input, init];
}; };
const genGoogle2 = ({ text, from, to, url, key }) => {
const body = JSON.stringify([[[text], from, to], "wt_lib"]);
const init = {
method: "POST",
headers: {
"Content-Type": "application/json+protobuf",
"X-Goog-API-Key": key,
},
body,
};
return [url, init];
};
const genMicrosoft = async ({ text, from, to }) => { const genMicrosoft = async ({ text, from, to }) => {
const [token] = await msAuth(); const [token] = await msAuth();
const params = { const params = {
@@ -161,10 +182,14 @@ const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
const genTencent = ({ text, from, to }) => { const genTencent = ({ text, from, to }) => {
const data = { const data = {
header: { header: {
fn: "auto_translation_block", fn: "auto_translation",
client_key:
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
}, },
type: "plain",
model_category: "normal",
source: { source: {
text_block: text, text_list: [text],
lang: from, lang: from,
}, },
target: { target: {
@@ -175,6 +200,9 @@ const genTencent = ({ text, from, to }) => {
const init = { const init = {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
referer: "https://transmart.qq.com/zh-CN/index",
}, },
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -183,25 +211,63 @@ const genTencent = ({ text, from, to }) => {
return [URL_TENCENT_TRANSMART, init]; return [URL_TENCENT_TRANSMART, init];
}; };
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => { const genVolcengine = ({ text, from, to }) => {
prompt = prompt const data = {
source_language: from,
target_language: to,
text: text,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [URL_VOLCENGINE_TRAN, init];
};
const genOpenAI = ({
text,
from,
to,
url,
key,
systemPrompt,
userPrompt,
model,
temperature,
maxTokens,
}) => {
// 兼容历史上作为systemPrompt的prompt如果prompt中不包含带翻译文本则添加文本到prompt末尾
// if (!prompt.includes(INPUT_PLACE_TEXT)) {
// prompt += `\nSource Text: ${INPUT_PLACE_TEXT}`;
// }
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to); .replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = { const data = {
model, model,
messages: [ messages: [
{ {
role: "system", role: "system",
content: prompt, content: systemPrompt,
}, },
{ {
role: "user", role: "user",
content: text, content: userPrompt,
}, },
], ],
temperature: 0, temperature,
max_tokens: 256, max_completion_tokens: maxTokens,
}; };
const init = { const init = {
@@ -217,26 +283,50 @@ const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
return [url, init]; return [url, init];
}; };
const genGemini = ({ text, from, to, url, key, prompt, model }) => { const genGemini = ({
prompt = prompt text,
from,
to,
url,
key,
systemPrompt,
userPrompt,
model,
temperature,
maxTokens,
}) => {
url = url
.replaceAll(INPUT_PLACE_MODEL, model)
.replaceAll(INPUT_PLACE_KEY, key);
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to) .replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text); .replaceAll(INPUT_PLACE_TEXT, text);
const data = { const data = {
contents: [ system_instruction: {
{ parts: {
// role: "user", text: systemPrompt,
parts: [
{
text: prompt,
},
],
}, },
], },
contents: {
role: "user",
parts: {
text: userPrompt,
},
},
generationConfig: {
maxOutputTokens: maxTokens,
temperature,
// topP: 0.8,
// topK: 10,
},
}; };
const input = `${url}/${model}:generateContent?key=${key}`;
const init = { const init = {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
@@ -245,18 +335,130 @@ const genGemini = ({ text, from, to, url, key, prompt, model }) => {
body: JSON.stringify(data), body: JSON.stringify(data),
}; };
return [input, init]; return [url, init];
}; };
const genOllama = ({ text, from, to, url, key, prompt, model }) => { const genGemini2 = ({
prompt = prompt text,
from,
to,
url,
key,
systemPrompt,
userPrompt,
model,
temperature,
maxTokens,
}) => {
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to) .replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text); .replaceAll(INPUT_PLACE_TEXT, text);
const data = { const data = {
model, model,
prompt, messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: userPrompt,
},
],
temperature,
max_tokens: maxTokens,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genClaude = ({
text,
from,
to,
url,
key,
systemPrompt,
userPrompt,
model,
temperature,
maxTokens,
}) => {
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
model,
system: systemPrompt,
messages: [
{
role: "user",
content: userPrompt,
},
],
temperature,
max_tokens: maxTokens,
};
const init = {
headers: {
"Content-type": "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": key,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genOllama = ({
text,
from,
to,
think,
url,
key,
systemPrompt,
userPrompt,
model,
}) => {
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
model,
system: systemPrompt,
prompt: userPrompt,
think: think,
stream: false, stream: false,
}; };
@@ -293,20 +495,27 @@ const genCloudflareAI = ({ text, from, to, url, key }) => {
return [url, init]; return [url, init];
}; };
const genCustom = ({ text, from, to, url, key, customOption }) => { const genCustom = ({ text, from, to, url, key, reqHook }) => {
const replaceInput = (str) => url = url
str .replaceAll(INPUT_PLACE_URL, url)
.replaceAll(INPUT_PLACE_URL, url) .replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TO, to) .replaceAll(INPUT_PLACE_TEXT, text)
.replaceAll(INPUT_PLACE_TEXT, text.replaceAll(`"`, `\n`)) .replaceAll(INPUT_PLACE_KEY, key);
.replaceAll(INPUT_PLACE_KEY, key); let init = {};
if (reqHook?.trim()) {
interpreter.run(`exports.reqHook = ${reqHook}`);
[url, init] = interpreter.exports.reqHook(text, from, to, url, key);
return [url, init];
}
const data = { const data = {
text, text,
from, from,
to, to,
}; };
const init = { init = {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
@@ -316,23 +525,6 @@ const genCustom = ({ text, from, to, url, key, customOption }) => {
if (key) { if (key) {
init.headers.Authorization = `Bearer ${key}`; init.headers.Authorization = `Bearer ${key}`;
} }
url = replaceInput(url);
if (customOption?.trim()) {
try {
const opt = JSON.parse(replaceInput(customOption));
opt.url && (url = opt.url);
opt.headers && (init.headers = opt.headers);
opt.method && (init.method = opt.method);
if (init.method === "GET") {
delete init.body;
} else {
opt.body && (init.body = JSON.stringify(opt.body));
}
} catch (err) {
throw new Error(`custom option parse err: ${err}`);
}
}
return [url, init]; return [url, init];
}; };
@@ -351,11 +543,18 @@ export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
case OPT_TRANS_OPENAI_2: case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3: case OPT_TRANS_OPENAI_3:
case OPT_TRANS_GEMINI: case OPT_TRANS_GEMINI:
case OPT_TRANS_GEMINI_2:
case OPT_TRANS_CLAUDE:
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:
case OPT_TRANS_OLLAMA: case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2: case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3: case OPT_TRANS_OLLAMA_3:
case OPT_TRANS_NIUTRANS: case OPT_TRANS_NIUTRANS:
case OPT_TRANS_CUSTOMIZE:
case OPT_TRANS_CUSTOMIZE_2:
case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5:
args.key = keyPick(translator, args.key, keyMap); args.key = keyPick(translator, args.key, keyMap);
break; break;
case OPT_TRANS_DEEPLX: case OPT_TRANS_DEEPLX:
@@ -367,6 +566,8 @@ export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
switch (translator) { switch (translator) {
case OPT_TRANS_GOOGLE: case OPT_TRANS_GOOGLE:
return genGoogle(args); return genGoogle(args);
case OPT_TRANS_GOOGLE_2:
return genGoogle2(args);
case OPT_TRANS_MICROSOFT: case OPT_TRANS_MICROSOFT:
return genMicrosoft(args); return genMicrosoft(args);
case OPT_TRANS_DEEPL: case OPT_TRANS_DEEPL:
@@ -381,12 +582,18 @@ export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
return genBaidu(args); return genBaidu(args);
case OPT_TRANS_TENCENT: case OPT_TRANS_TENCENT:
return genTencent(args); return genTencent(args);
case OPT_TRANS_VOLCENGINE:
return genVolcengine(args);
case OPT_TRANS_OPENAI: case OPT_TRANS_OPENAI:
case OPT_TRANS_OPENAI_2: case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3: case OPT_TRANS_OPENAI_3:
return genOpenAI(args); return genOpenAI(args);
case OPT_TRANS_GEMINI: case OPT_TRANS_GEMINI:
return genGemini(args); return genGemini(args);
case OPT_TRANS_GEMINI_2:
return genGemini2(args);
case OPT_TRANS_CLAUDE:
return genClaude(args);
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args); return genCloudflareAI(args);
case OPT_TRANS_OLLAMA: case OPT_TRANS_OLLAMA:

View File

@@ -17,6 +17,7 @@ import {
CMD_TOGGLE_STYLE, CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS, CMD_OPEN_OPTIONS,
CMD_OPEN_TRANBOX, CMD_OPEN_TRANBOX,
CLIENT_THUNDERBIRD,
} from "./config"; } from "./config";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage"; import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync"; import { trySyncSettingAndRules } from "./libs/sync";
@@ -46,7 +47,7 @@ async function addContextMenus(contextMenuType = 1) {
try { try {
await browser.contextMenus.removeAll(); await browser.contextMenus.removeAll();
} catch (err) { } catch (err) {
// kissLog(err, "remove contextMenus");
} }
switch (contextMenuType) { switch (contextMenuType) {
@@ -123,12 +124,26 @@ async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
} }
} }
/**
* 注册邮件显示脚本
*/
async function registerMsgDisplayScript() {
await messenger.messageDisplayScripts.register({
js: [{ file: "/content.js" }],
});
}
/** /**
* 插件安装 * 插件安装
*/ */
browser.runtime.onInstalled.addListener(() => { browser.runtime.onInstalled.addListener(() => {
tryInitDefaultData(); tryInitDefaultData();
//在thunderbird中注册脚本
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
registerMsgDisplayScript();
}
// 右键菜单 // 右键菜单
addContextMenus(); addContextMenus();
@@ -151,6 +166,11 @@ browser.runtime.onStartup.addListener(async () => {
tryClearCaches(); tryClearCaches();
} }
//在thunderbird中注册脚本
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
registerMsgDisplayScript();
}
// 右键菜单 // 右键菜单
// firefox重启后菜单会消失,故重复添加 // firefox重启后菜单会消失,故重复添加
addContextMenus(contextMenuType); addContextMenus(contextMenuType);

View File

@@ -72,7 +72,7 @@ function runtimeListener(translator) {
default: default:
return { error: `message action is unavailable: ${action}` }; return { error: `message action is unavailable: ${action}` };
} }
return { data: translator.rule }; return { rule: translator.rule, setting: translator.setting };
}); });
} }
@@ -135,14 +135,18 @@ async function showFab(translator) {
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
function showTransbox({ function showTransbox(
contextMenuType, {
tranboxSetting = DEFAULT_TRANBOX_SETTING, contextMenuType,
transApis, tranboxSetting = DEFAULT_TRANBOX_SETTING,
darkMode, transApis,
uiLang, darkMode,
}) { uiLang,
if (!tranboxSetting?.transOpen) { langDetector,
},
{ transSelected }
) {
if (transSelected === "false") {
return; return;
} }
@@ -172,6 +176,7 @@ function showTransbox({
tranboxSetting={tranboxSetting} tranboxSetting={tranboxSetting}
transApis={transApis} transApis={transApis}
uiLang={uiLang} uiLang={uiLang}
langDetector={langDetector}
/> />
</CacheProvider> </CacheProvider>
</React.StrictMode> </React.StrictMode>
@@ -249,7 +254,7 @@ export async function run(isUserscript = false) {
inputTranslate(setting); inputTranslate(setting);
// 划词翻译 // 划词翻译
showTransbox(setting); showTransbox(setting, rule);
// 浮球按钮 // 浮球按钮
await showFab(translator); await showFab(translator);

View File

@@ -42,7 +42,8 @@ const customApiLangs = `["en", "English - English"],
["vi", "Vietnamese - Tiếng Việt"], ["vi", "Vietnamese - Tiếng Việt"],
`; `;
const customDefaultOption = `{ const customApiHelpZH = `// 请求数据默认格式
{
"url": "{{url}}", "url": "{{url}}",
"method": "POST", "method": "POST",
"headers": { "headers": {
@@ -50,18 +51,12 @@ const customDefaultOption = `{
"Authorization": "Bearer {{key}}" "Authorization": "Bearer {{key}}"
}, },
"body": { "body": {
"text": "{{text}}", "text": "{{text}}", // 待翻译文字
"from": "{{from}}", "from": "{{from}}", // 文字的语言(可能为空)
"to": "{{to}}" "to": "{{to}}", // 目标语言
}, },
"resPattern": { }
"text": "text",
"from": "from"
}
}`;
const customApiHelpZH = `// 自定义选项范例
${customDefaultOption}
// 返回数据默认格式 // 返回数据默认格式
{ {
@@ -70,20 +65,72 @@ ${customDefaultOption}
to: "", // 目标语言(可选) to: "", // 目标语言(可选)
} }
// Hook 范例
// URL
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
},
method: "GET",
body: null,
}]
// Response Hook
// 其中返回数组第一个值表示译文字符串,第二个值为布尔值,表示原文语言与目标语言是否相同
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
// 支持的语言代码如下 // 支持的语言代码如下
${customApiLangs} ${customApiLangs}
`; `;
const customApiHelpEN = `// Example of custom options const customApiHelpEN = `// Default request
${customDefaultOption} {
"url": "{{url}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // Text to be translated
"from": "{{from}}", // The language of the text (may be empty)
"to": "{{to}}", // Target language
},
}
// Return data default format
// Default response
{ {
text: "", // translated text text: "", // translated text
from: "", // Recognized source language from: "", // Recognized source language
to: "", // Target language (optional) to: "", // Target language (optional)
} }
/// Hook Example
// URL
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
},
method: "GET",
body: null,
}]
// Response Hook
// In the returned array, the first value is the translated string, while the second value is a boolean
// that indicates whether the source language is the same as the target language.
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
// The supported language codes are as follows // The supported language codes are as follows
${customApiLangs} ${customApiLangs}
`; `;
@@ -153,6 +200,22 @@ export const I18N = {
zh: `最大并发请求数量 (1-100)`, zh: `最大并发请求数量 (1-100)`,
en: `Maximum Number Of Concurrent Requests (1-100)`, en: `Maximum Number Of Concurrent Requests (1-100)`,
}, },
if_think: {
zh: `启用或禁用模型的深度思考能力`,
en: `Enable or disable the models thinking behavior `,
},
think: {
zh: `启用深度思考`,
en: `enable thinking`,
},
nothink: {
zh: `禁用深度思考`,
en: `disable thinking`,
},
think_ignore: {
zh: `忽略以下模型的<think>输出,逗号(,)分割,当模型支持思考但ollama不支持时需要填写本参数`,
en: `Ignore the <think> block for the following models, comma (,) separated`,
},
fetch_interval: { fetch_interval: {
zh: `每次请求间隔时间 (0-5000ms)`, zh: `每次请求间隔时间 (0-5000ms)`,
en: `Time Between Requests (0-5000ms)`, en: `Time Between Requests (0-5000ms)`,
@@ -161,6 +224,10 @@ export const I18N = {
zh: `重新翻译间隔时间 (100-5000ms)`, zh: `重新翻译间隔时间 (100-5000ms)`,
en: `Retranslation Interval (100-5000ms)`, en: `Retranslation Interval (100-5000ms)`,
}, },
http_timeout: {
zh: `请求超时时间 (5000-30000ms)`,
en: `Request Timeout Time (5000-30000ms)`,
},
min_translate_length: { min_translate_length: {
zh: `最小翻译字符数 (1-100)`, zh: `最小翻译字符数 (1-100)`,
en: `Minimum number Of Translated Characters (1-100)`, en: `Minimum number Of Translated Characters (1-100)`,
@@ -306,12 +373,20 @@ export const I18N = {
en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`, en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`,
}, },
sync_warn: { sync_warn: {
zh: `涉及隐私数据的同步请谨慎选择第三方同步服务,建议自行搭建 kiss-worker 或 WebDAV 服务。`,
en: `When synchronizing data that involves privacy, please be cautious about choosing third-party sync services. It is recommended to set up your own sync service using kiss-worker or WebDAV.`,
},
sync_warn_2: {
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`, zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`, en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
}, },
about_sync_api: { about_sync_api: {
zh: `查看关于数据同步接口部署`, zh: `自建kiss-wroker数据同步服务`,
en: `View About Data Synchronization Interface Deployment`, en: `Self-hosting a Kiss-worker data sync service`,
},
about_api: {
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`,
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
}, },
about_api_proxy: { about_api_proxy: {
zh: `查看自建一个翻译接口代理`, zh: `查看自建一个翻译接口代理`,
@@ -394,8 +469,8 @@ export const I18N = {
en: `Keep unchanged selector`, en: `Keep unchanged selector`,
}, },
keep_selector_helper: { keep_selector_helper: {
zh: `1、遵循CSS选择器语法。2、子元素选择器用“>>>”隔开。`, zh: `1、遵循CSS选择器语法。`,
en: `1. Follow CSS selector syntax. 2. Sub-element selectors are separated by ">>>".`, en: `1. Follow CSS selector syntax.`,
}, },
terms: { terms: {
zh: `专业术语`, zh: `专业术语`,
@@ -623,7 +698,7 @@ export const I18N = {
}, },
use_simple_style: { use_simple_style: {
zh: `使用简洁界面`, zh: `使用简洁界面`,
en: `Click outside to close the pop-up window`, en: `Use a simple interface`,
}, },
show: { show: {
zh: `显示`, zh: `显示`,
@@ -821,6 +896,10 @@ export const I18N = {
zh: `更多`, zh: `更多`,
en: `More`, en: `More`,
}, },
less: {
zh: `更少`,
en: `Less`,
},
fixer_selector: { fixer_selector: {
zh: `网页修复选择器`, zh: `网页修复选择器`,
en: `Fixer Selector`, en: `Fixer Selector`,
@@ -873,4 +952,44 @@ export const I18N = {
zh: `翻译框跟随选中文本`, zh: `翻译框跟随选中文本`,
en: `Transbox Follow Selection`, en: `Transbox Follow Selection`,
}, },
translate_start_hook: {
zh: `翻译开始钩子函数`,
en: `Translate Start Hook`,
},
translate_start_hook_helper: {
zh: `翻译开始时运行,入参为: 翻译节点,原文文本。`,
en: `Run when translation starts, the input parameters are: translation node, original text.`,
},
translate_end_hook: {
zh: `翻译完成钩子函数`,
en: `Translate End Hook`,
},
translate_end_hook_helper: {
zh: `翻译完成时运行,入参为: 翻译节点,原文文本,译文文本,保留元素。`,
en: `Run when the translation is completed, the input parameters are: translation node, original text, translation text, retained elements.`,
},
translate_remove_hook: {
zh: `翻译移除钩子函数`,
en: `Translate Removed Hook`,
},
translate_remove_hook_helper: {
zh: `翻译移除时运行,入参为: 翻译节点。`,
en: `Run when translation is removed, the input parameters are: translation node.`,
},
english_dict: {
zh: `英文词典`,
en: `English Dictionary`,
},
api_name: {
zh: `接口名称`,
en: `API Name`,
},
is_disabled: {
zh: `是否禁用`,
en: `Is Disabled`,
},
translate_selected: {
zh: `是否启用划词翻译`,
en: `If translate selected`,
},
}; };

View File

@@ -39,7 +39,13 @@ export const CLIENT_CHROME = "chrome";
export const CLIENT_EDGE = "edge"; export const CLIENT_EDGE = "edge";
export const CLIENT_FIREFOX = "firefox"; export const CLIENT_FIREFOX = "firefox";
export const CLIENT_USERSCRIPT = "userscript"; export const CLIENT_USERSCRIPT = "userscript";
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX]; export const CLIENT_THUNDERBIRD = "thunderbird";
export const CLIENT_EXTS = [
CLIENT_CHROME,
CLIENT_EDGE,
CLIENT_FIREFOX,
CLIENT_THUNDERBIRD,
];
export const KV_RULES_KEY = "kiss-rules.json"; export const KV_RULES_KEY = "kiss-rules.json";
export const KV_WORDS_KEY = "kiss-words.json"; export const KV_WORDS_KEY = "kiss-words.json";
@@ -78,9 +84,20 @@ export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master"; "https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`; export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
// api.cognitive.microsofttranslator.com
export const URL_MICROSOFT_TRAN = export const URL_MICROSOFT_TRAN =
"https://api-edge.cognitive.microsofttranslator.com/translate"; "https://api-edge.cognitive.microsofttranslator.com/translate";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth"; export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_LANGDETECT =
"https://api-edge.cognitive.microsofttranslator.com/detect?api-version=3.0";
export const URL_GOOGLE_TRAN =
"https://translate.googleapis.com/translate_a/single";
export const URL_GOOGLE_TRAN2 =
"https://translate-pa.googleapis.com/v1/translateHtml";
export const DEFAULT_GOOGLE_API_KEY = "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520";
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect"; export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
export const URL_BAIDU_SUGGEST = "https://fanyi.baidu.com/sug"; export const URL_BAIDU_SUGGEST = "https://fanyi.baidu.com/sug";
export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts"; export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts";
@@ -89,13 +106,18 @@ export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi"; export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc"; export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt"; export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
export const URL_VOLCENGINE_TRAN =
"https://translate.volcengine.com/crx/translate/v1";
export const URL_NIUTRANS_REG = export const URL_NIUTRANS_REG =
"https://niutrans.com/login?active=3&userSource=kiss-translator"; "https://niutrans.com/login?active=3&userSource=kiss-translator";
export const DEFAULT_USER_AGENT = export const DEFAULT_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"; "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
export const OPT_DICT_BAIDU = "Baidu";
export const OPT_TRANS_GOOGLE = "Google"; export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2";
export const OPT_TRANS_MICROSOFT = "Microsoft"; export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL"; export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_DEEPLX = "DeepLX"; export const OPT_TRANS_DEEPLX = "DeepLX";
@@ -103,10 +125,13 @@ export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_NIUTRANS = "NiuTrans"; export const OPT_TRANS_NIUTRANS = "NiuTrans";
export const OPT_TRANS_BAIDU = "Baidu"; export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent"; export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_VOLCENGINE = "Volcengine";
export const OPT_TRANS_OPENAI = "OpenAI"; export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_OPENAI_2 = "OpenAI2"; export const OPT_TRANS_OPENAI_2 = "OpenAI2";
export const OPT_TRANS_OPENAI_3 = "OpenAI3"; export const OPT_TRANS_OPENAI_3 = "OpenAI3";
export const OPT_TRANS_GEMINI = "Gemini"; export const OPT_TRANS_GEMINI = "Gemini";
export const OPT_TRANS_GEMINI_2 = "Gemini2";
export const OPT_TRANS_CLAUDE = "Claude";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI"; export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_OLLAMA = "Ollama"; export const OPT_TRANS_OLLAMA = "Ollama";
export const OPT_TRANS_OLLAMA_2 = "Ollama2"; export const OPT_TRANS_OLLAMA_2 = "Ollama2";
@@ -118,9 +143,11 @@ export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5"; export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
export const OPT_TRANS_ALL = [ export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX, OPT_TRANS_DEEPLX,
@@ -129,6 +156,8 @@ export const OPT_TRANS_ALL = [
OPT_TRANS_OPENAI_2, OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3, OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI, OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA, OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2, OPT_TRANS_OLLAMA_2,
@@ -140,6 +169,13 @@ export const OPT_TRANS_ALL = [
OPT_TRANS_CUSTOMIZE_5, OPT_TRANS_CUSTOMIZE_5,
]; ];
export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
];
export const OPT_LANGS_TO = [ export const OPT_LANGS_TO = [
["en", "English - English"], ["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"], ["zh-CN", "Simplified Chinese - 简体中文"],
@@ -182,6 +218,7 @@ export const OPT_LANGS_TO = [
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO]; export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
export const OPT_LANGS_SPECIAL = { export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])), [OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
[OPT_TRANS_GOOGLE_2]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
[OPT_TRANS_MICROSOFT]: new Map([ [OPT_TRANS_MICROSOFT]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]), ...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""], ["auto", ""],
@@ -202,7 +239,7 @@ export const OPT_LANGS_SPECIAL = {
]), ]),
[OPT_TRANS_DEEPLX]: new Map([ [OPT_TRANS_DEEPLX]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]), ...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""], ["auto", "auto"],
["zh-CN", "ZH"], ["zh-CN", "ZH"],
["zh-TW", "ZH"], ["zh-TW", "ZH"],
]), ]),
@@ -212,6 +249,12 @@ export const OPT_LANGS_SPECIAL = {
["zh-CN", "zh"], ["zh-CN", "zh"],
["zh-TW", "cht"], ["zh-TW", "cht"],
]), ]),
[OPT_TRANS_VOLCENGINE]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", "auto"],
["zh-CN", "zh"],
["zh-TW", "zh-Hant"],
]),
[OPT_TRANS_BAIDU]: new Map([ [OPT_TRANS_BAIDU]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]), ...OPT_LANGS_FROM.map(([key]) => [key, key]),
["zh-CN", "zh"], ["zh-CN", "zh"],
@@ -273,6 +316,12 @@ export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_GEMINI]: new Map( [OPT_TRANS_GEMINI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
), ),
[OPT_TRANS_GEMINI_2]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CLAUDE]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OLLAMA]: new Map( [OPT_TRANS_OLLAMA]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
), ),
@@ -318,6 +367,12 @@ export const OPT_LANGS_SPECIAL = {
]), ]),
}; };
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang); export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
export const OPT_LANGS_MICROSOFT = new Map(
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_MICROSOFT].entries()).map(([k, v]) => [
v,
k,
])
);
export const OPT_LANGS_BAIDU = new Map( export const OPT_LANGS_BAIDU = new Map(
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [ Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
v, v,
@@ -384,10 +439,11 @@ export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
export const INPUT_PLACE_TO = "{{to}}"; // 占位符 export const INPUT_PLACE_TO = "{{to}}"; // 占位符
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符 export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符 export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色 export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const DEFAULT_TRANS_TAG = "span"; export const DEFAULT_TRANS_TAG = "font";
export const DEFAULT_SELECT_STYLE = export const DEFAULT_SELECT_STYLE =
"-webkit-line-clamp: unset; max-height: none; height: auto;"; "-webkit-line-clamp: unset; max-height: none; height: auto;";
@@ -412,10 +468,14 @@ export const GLOBLA_RULE = {
transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译
transTag: DEFAULT_TRANS_TAG, // 译文元素标签 transTag: DEFAULT_TRANS_TAG, // 译文元素标签
transTitle: "false", // 是否同时翻译页面标题 transTitle: "false", // 是否同时翻译页面标题
transSelected: "true", // 是否启用划词翻译
detectRemote: "false", // 是否使用远程语言检测 detectRemote: "false", // 是否使用远程语言检测
skipLangs: [], // 不翻译的语言 skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器 fixerSelector: "", // 修复函数选择器
fixerFunc: "-", // 修复函数 fixerFunc: "-", // 修复函数
transStartHook: "", // 钩子函数
transEndHook: "", // 钩子函数
transRemoveHook: "", // 钩子函数
}; };
// 输入框翻译 // 输入框翻译
@@ -447,7 +507,7 @@ export const OPT_TRANBOX_TRIGGER_ALL = [
]; ];
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"]; export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
export const DEFAULT_TRANBOX_SETTING = { export const DEFAULT_TRANBOX_SETTING = {
transOpen: true, // transOpen: true, // 是否启用划词翻译作废移至rule
translator: OPT_TRANS_MICROSOFT, translator: OPT_TRANS_MICROSOFT,
fromLang: "auto", fromLang: "auto",
toLang: "zh-CN", toLang: "zh-CN",
@@ -463,6 +523,7 @@ export const DEFAULT_TRANBOX_SETTING = {
followSelection: false, // 翻译框是否跟随选中文本 followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式 triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
extStyles: "", // 附加样式 extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
}; };
// 订阅列表 // 订阅列表
@@ -481,64 +542,120 @@ export const DEFAULT_SUBRULES_LIST = [
}, },
]; ];
export const DEFAULT_HTTP_TIMEOUT = 5000; // 调用超时时间
// 翻译接口 // 翻译接口
const defaultCustomApi = { const defaultCustomApi = {
url: "", url: "",
key: "", key: "",
customOption: "", customOption: "", // (作废)
reqHook: "", // request 钩子函数
resHook: "", // response 钩子函数
fetchLimit: DEFAULT_FETCH_LIMIT, fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL, fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: "",
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}; };
const defaultOpenaiApi = { const defaultOpenaiApi = {
url: "https://api.openai.com/v1/chat/completions", url: "https://api.openai.com/v1/chat/completions",
key: "", key: "",
model: "gpt-4", model: "gpt-4",
prompt: `You will be provided with a sentence in ${INPUT_PLACE_FROM}, and your task is to translate it into ${INPUT_PLACE_TO}.`, systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 256,
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: "",
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
}; };
const defaultOllamaApi = { const defaultOllamaApi = {
url: "http://localhost:11434/api/generate", url: "http://localhost:11434/api/generate",
key: "", key: "",
model: "llama3", model: "llama3.1",
prompt: `Translate the following text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}:\n\n${INPUT_PLACE_TEXT}`, systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
think: false,
thinkIgnore: `qwen3,deepseek-r1`,
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: "",
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
}; };
export const DEFAULT_TRANS_APIS = { export const DEFAULT_TRANS_APIS = {
[OPT_TRANS_GOOGLE]: { [OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single", url: URL_GOOGLE_TRAN,
key: "", key: "",
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量 fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间 fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
apiName: OPT_TRANS_GOOGLE, // 接口自定义名称
isDisabled: false, // 是否禁用
httpTimeout: DEFAULT_HTTP_TIMEOUT, // 超时时间
},
[OPT_TRANS_GOOGLE_2]: {
url: URL_GOOGLE_TRAN2,
key: DEFAULT_GOOGLE_API_KEY,
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_GOOGLE_2,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_MICROSOFT]: { [OPT_TRANS_MICROSOFT]: {
fetchLimit: DEFAULT_FETCH_LIMIT, fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL, fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_MICROSOFT,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_BAIDU]: { [OPT_TRANS_BAIDU]: {
fetchLimit: DEFAULT_FETCH_LIMIT, fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL, fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_BAIDU,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_TENCENT]: { [OPT_TRANS_TENCENT]: {
fetchLimit: DEFAULT_FETCH_LIMIT, fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL, fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_TENCENT,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_VOLCENGINE]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_VOLCENGINE,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_DEEPL]: { [OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate", url: "https://api-free.deepl.com/v2/translate",
key: "", key: "",
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: OPT_TRANS_DEEPL,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_DEEPLFREE]: { [OPT_TRANS_DEEPLFREE]: {
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: OPT_TRANS_DEEPLFREE,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_DEEPLX]: { [OPT_TRANS_DEEPLX]: {
url: "http://localhost:1188/translate", url: "http://localhost:1188/translate",
key: "", key: "",
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: OPT_TRANS_DEEPLX,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_NIUTRANS]: { [OPT_TRANS_NIUTRANS]: {
url: "https://api.niutrans.com/NiuTransServer/translation", url: "https://api.niutrans.com/NiuTransServer/translation",
@@ -547,23 +664,63 @@ export const DEFAULT_TRANS_APIS = {
memoryNo: "", memoryNo: "",
fetchLimit: DEFAULT_FETCH_LIMIT, fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL, fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_NIUTRANS,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
}, },
[OPT_TRANS_OPENAI]: defaultOpenaiApi, [OPT_TRANS_OPENAI]: defaultOpenaiApi,
[OPT_TRANS_OPENAI_2]: defaultOpenaiApi, [OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
[OPT_TRANS_OPENAI_3]: defaultOpenaiApi, [OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
[OPT_TRANS_GEMINI]: { [OPT_TRANS_GEMINI]: {
url: "https://generativelanguage.googleapis.com/v1/models", url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
key: "", key: "",
model: "gemini-pro", model: "gemini-2.5-flash",
prompt: `Translate the following text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}:\n\n${INPUT_PLACE_TEXT}`, systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 2048,
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: OPT_TRANS_GEMINI,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
},
[OPT_TRANS_GEMINI_2]: {
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
key: "",
model: "gemini-2.0-flash",
systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 2048,
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_GEMINI_2,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
},
[OPT_TRANS_CLAUDE]: {
url: "https://api.anthropic.com/v1/messages",
key: "",
model: "claude-3-haiku-20240307",
systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 1024,
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_CLAUDE,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
}, },
[OPT_TRANS_CLOUDFLAREAI]: { [OPT_TRANS_CLOUDFLAREAI]: {
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b", url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
key: "", key: "",
fetchLimit: 1, fetchLimit: 1,
fetchInterval: 500, fetchInterval: 500,
apiName: OPT_TRANS_CLOUDFLAREAI,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
}, },
[OPT_TRANS_OLLAMA]: defaultOllamaApi, [OPT_TRANS_OLLAMA]: defaultOllamaApi,
[OPT_TRANS_OLLAMA_2]: defaultOllamaApi, [OPT_TRANS_OLLAMA_2]: defaultOllamaApi,
@@ -607,6 +764,7 @@ export const DEFAULT_SETTING = {
minLength: TRANS_MIN_LENGTH, minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH, maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH, newlineLength: TRANS_NEWLINE_LENGTH,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
clearCache: false, // 是否在浏览器下次启动时清除缓存 clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则 injectRules: true, // 是否注入订阅规则
// injectWebfix: true, // 是否注入修复补丁(作废) // injectWebfix: true, // 是否注入修复补丁(作废)
@@ -628,6 +786,7 @@ export const DEFAULT_SETTING = {
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单 csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
// disableLangs: [], // 不翻译的语言(移至rule作废) // disableLangs: [], // 不翻译的语言(移至rule作废)
transInterval: 500, // 翻译间隔时间 transInterval: 500, // 翻译间隔时间
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -26,10 +26,14 @@ export const DEFAULT_RULE = {
transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译
transTag: GLOBAL_KEY, // 译文元素标签 transTag: GLOBAL_KEY, // 译文元素标签
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题 transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
transSelected: GLOBAL_KEY, // 是否启用划词翻译
detectRemote: GLOBAL_KEY, // 是否使用远程语言检测 detectRemote: GLOBAL_KEY, // 是否使用远程语言检测
skipLangs: [], // 不翻译的语言 skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器 fixerSelector: "", // 修复函数选择器
fixerFunc: GLOBAL_KEY, // 修复函数 fixerFunc: GLOBAL_KEY, // 修复函数
transStartHook: "", // 钩子函数
transEndHook: "", // 钩子函数
transRemoveHook: "", // 钩子函数
}; };
const DEFAULT_DIY_STYLE = `color: #666; const DEFAULT_DIY_STYLE = `color: #666;

View File

@@ -30,7 +30,14 @@ export function useTranslate(q, rule, setting) {
return; return;
} }
const deLang = await tryDetectLang(q, detectRemote === "true"); let deLang = "";
if (fromLang === "auto") {
deLang = await tryDetectLang(
q,
detectRemote === "true",
setting.langDetector
);
}
if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) { if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
setSamelang(true); setSamelang(true);
} else { } else {

View File

@@ -1,20 +1,21 @@
import { isExt, isGm } from "./client"; import { isExt, isGm } from "./client";
import { sendBgMsg } from "./msg"; import { sendBgMsg } from "./msg";
import { taskPool } from "./pool"; import { taskPool } from "./pool";
import { getSettingWithDefault } from "./storage";
import { import {
MSG_FETCH, MSG_FETCH,
MSG_GET_HTTPCACHE, MSG_GET_HTTPCACHE,
CACHE_NAME, CACHE_NAME,
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT,
DEFAULT_HTTP_TIMEOUT,
} from "../config"; } from "../config";
import { isBg } from "./browser"; import { isBg } from "./browser";
import { genTransReq } from "../apis/trans"; import { genTransReq } from "../apis/trans";
import { kissLog } from "./log"; import { kissLog } from "./log";
import { blobToBase64 } from "./utils"; import { blobToBase64 } from "./utils";
const TIMEOUT = 5000;
/** /**
* 构造缓存 request * 构造缓存 request
* @param {*} input * @param {*} input
@@ -39,7 +40,10 @@ const newCacheReq = async (input, init) => {
* @param {*} init * @param {*} init
* @returns * @returns
*/ */
export const fetchGM = async (input, { method = "GET", headers, body } = {}) => export const fetchGM = async (
input,
{ method = "GET", headers, body, timeout } = {}
) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
GM.xmlHttpRequest({ GM.xmlHttpRequest({
method, method,
@@ -47,7 +51,7 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
headers, headers,
data: body, data: body,
// withCredentials: true, // withCredentials: true,
timeout: TIMEOUT, timeout,
onload: ({ response, responseHeaders, status, statusText }) => { onload: ({ response, responseHeaders, status, statusText }) => {
const headers = {}; const headers = {};
responseHeaders.split("\n").forEach((line) => { responseHeaders.split("\n").forEach((line) => {
@@ -81,35 +85,60 @@ export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
throw new Error("url is empty"); throw new Error("url is empty");
} }
if (isGm) { let timeout = apiSetting?.httpTimeout || DEFAULT_HTTP_TIMEOUT;
let info; if (!apiSetting) {
if (window.KISS_GM) { try {
info = await window.KISS_GM.getInfo(); timeout = (await getSettingWithDefault()).httpTimeout;
} else { } catch (err) {
info = GM.info; //
}
// Tampermonkey --> .connects
// Violentmonkey --> .connect
const connects = info?.script?.connects || info?.script?.connect || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
const { body, headers, status, statusText } = window.KISS_GM
? await window.KISS_GM.fetch(input, init)
: await fetchGM(input, init);
return new Response(body, {
headers: new Headers(headers),
status,
statusText,
});
} }
} }
if (AbortSignal?.timeout) { if (isGm) {
Object.assign(init, { signal: AbortSignal.timeout(TIMEOUT) }); // let info;
// if (window.KISS_GM) {
// info = await window.KISS_GM.getInfo();
// } else {
// info = GM.info;
// }
// Tampermonkey --> .connects
// Violentmonkey --> .connect
// const connects = info?.script?.connects || info?.script?.connect || [];
// const url = new URL(input);
// const isSafe = connects.find((item) => url.hostname.endsWith(item));
// if (isSafe) {
// // todo: 自定义接口 init 可能包含了 signal
// Object.assign(init, { timeout });
// const { body, headers, status, statusText } = window.KISS_GM
// ? await window.KISS_GM.fetch(input, init)
// : await fetchGM(input, init);
// return new Response(body, {
// headers: new Headers(headers),
// status,
// statusText,
// });
// }
// todo: 自定义接口 init 可能包含了 signal
Object.assign(init, { timeout });
const { body, headers, status, statusText } = window.KISS_GM
? await window.KISS_GM.fetch(input, init)
: await fetchGM(input, init);
return new Response(body, {
headers: new Headers(headers),
status,
statusText,
});
}
if (AbortSignal?.timeout && !init.signal) {
Object.assign(init, { signal: AbortSignal.timeout(timeout) });
} }
return fetch(input, init); return fetch(input, init);

View File

@@ -1,8 +1,26 @@
import { CACHE_NAME } from "../config"; import {
CACHE_NAME,
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
} from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { apiBaiduLangdetect } from "../apis"; import {
apiGoogleLangdetect,
apiMicrosoftLangdetect,
apiBaiduLangdetect,
apiTencentLangdetect,
} from "../apis";
import { kissLog } from "./log"; import { kissLog } from "./log";
const langdetectMap = {
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
};
/** /**
* 清除缓存数据 * 清除缓存数据
*/ */
@@ -19,12 +37,16 @@ export const tryClearCaches = async () => {
* @param {*} q * @param {*} q
* @returns * @returns
*/ */
export const tryDetectLang = async (q, useRemote = false) => { export const tryDetectLang = async (
q,
useRemote = false,
langDetector = OPT_TRANS_MICROSOFT
) => {
let lang = ""; let lang = "";
if (useRemote) { if (useRemote) {
try { try {
lang = await apiBaiduLangdetect(q); lang = await langdetectMap[langDetector](q);
} catch (err) { } catch (err) {
kissLog(err, "detect lang remote"); kissLog(err, "detect lang remote");
} }

16
src/libs/interpreter.js Normal file
View File

@@ -0,0 +1,16 @@
import Sval from "sval";
const interpreter = new Sval({
// ECMA Version of the code
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
// or "latest"
ecmaVer: "latest",
// Code source type
// "script" or "module"
sourceType: "script",
// Whether the code runs in a sandbox
sandBox: true,
});
export default interpreter;

View File

@@ -74,6 +74,9 @@ export const matchRule = async (
"injectJs", "injectJs",
"injectCss", "injectCss",
"fixerSelector", "fixerSelector",
"transStartHook",
"transEndHook",
"transRemoveHook",
].forEach((key) => { ].forEach((key) => {
if (!rule[key]?.trim()) { if (!rule[key]?.trim()) {
rule[key] = globalRule[key]; rule[key] = globalRule[key];
@@ -89,6 +92,7 @@ export const matchRule = async (
"transTiming", "transTiming",
"transTag", "transTag",
"transTitle", "transTitle",
"transSelected",
"detectRemote", "detectRemote",
"fixerFunc", "fixerFunc",
].forEach((key) => { ].forEach((key) => {
@@ -158,10 +162,14 @@ export const checkRules = (rules) => {
transTiming, transTiming,
transTag, transTag,
transTitle, transTitle,
transSelected,
detectRemote, detectRemote,
skipLangs, skipLangs,
fixerSelector, fixerSelector,
fixerFunc, fixerFunc,
transStartHook,
transEndHook,
transRemoveHook,
}) => ({ }) => ({
pattern: pattern.trim(), pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "", selector: type(selector) === "string" ? selector : "",
@@ -182,9 +190,14 @@ export const checkRules = (rules) => {
transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming), transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag), transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle), transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected),
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote), detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
skipLangs: type(skipLangs) === "array" ? skipLangs : [], skipLangs: type(skipLangs) === "array" ? skipLangs : [],
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "", fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
transRemoveHook:
type(transRemoveHook) === "string" ? transRemoveHook : "",
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc), fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
}) })
); );

View File

@@ -1,5 +1,5 @@
export const loadingSvg = ` export const loadingSvg = `
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;"> <svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%; max-width: 24; max-height: 24;">
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6"> <circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
<animateTransform <animateTransform
attributeName="transform" attributeName="transform"

View File

@@ -18,13 +18,14 @@ import {
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch"; import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils"; import { debounce, genEventName, getHtmlText } from "./utils";
import { runFixer } from "./webfix"; import { runFixer } from "./webfix";
import { apiTranslate } from "../apis"; import { apiTranslate } from "../apis";
import { sendBgMsg } from "./msg"; import { sendBgMsg } from "./msg";
import { isExt } from "./client"; import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector"; import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log"; import { kissLog } from "./log";
import interpreter from "./interpreter";
/** /**
* 翻译类 * 翻译类
@@ -52,7 +53,7 @@ export class Translator {
]; ];
_eventName = genEventName(); _eventName = genEventName();
_mouseoverNode = null; _mouseoverNode = null;
_keepSelector = [null, null]; _keepSelector = "";
_terms = []; _terms = [];
_docTitle = ""; _docTitle = "";
@@ -124,9 +125,7 @@ export class Translator {
this._setting = setting; this._setting = setting;
this._rule = rule; this._rule = rule;
this._keepSelector = (rule.keepSelector || "") this._keepSelector = rule.keepSelector || "";
.split(SHADOW_KEY)
.map((item) => item.trim());
this._terms = (rule.terms || "") this._terms = (rule.terms || "")
.split(/\n|;/) .split(/\n|;/)
.map((item) => item.split(",").map((item) => item.trim())) .map((item) => item.split(",").map((item) => item.trim()))
@@ -405,6 +404,7 @@ export class Translator {
// 移除键盘监听 // 移除键盘监听
window.removeEventListener("keydown", this._handleKeydown); window.removeEventListener("keydown", this._handleKeydown);
const { transRemoveHook } = this._rule;
this._tranNodes.forEach((innerHTML, node) => { this._tranNodes.forEach((innerHTML, node) => {
if ( if (
!this._rule.transTiming || !this._rule.transTiming ||
@@ -420,10 +420,17 @@ export class Translator {
} }
// 移除/恢复元素 // 移除/恢复元素
if (innerHTML && this._rule.transOnly === "true") { if (innerHTML) {
node.innerHTML = innerHTML; if (this._rule.transOnly === "true") {
} else { node.innerHTML = innerHTML;
node.querySelector(APP_LCNAME)?.remove(); } else {
node.querySelector(APP_LCNAME)?.remove();
}
// 钩子函数
if (transRemoveHook?.trim()) {
interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`);
interpreter.exports.transRemoveHook(node);
}
} }
}); });
@@ -468,45 +475,41 @@ export class Translator {
return; return;
} }
const preText = this._tranNodes.get(el); const preText = getHtmlText(this._tranNodes.get(el));
const curText = el.innerText.trim(); const curText = getHtmlText(el.innerHTML, APP_LCNAME);
// const traText = traEl.innerText.trim(); if (preText === curText) {
// todo
// 1. traText when loading
// 2. replace startsWith
if (curText.startsWith(preText)) {
return; return;
} }
traEl.remove(); traEl.remove();
} }
// 缓存已翻译元素
this._tranNodes.set(el, el.innerHTML);
let q = el.innerText.trim(); let q = el.innerText.trim();
if (this._rule.transOnly === "true") {
this._tranNodes.set(el, el.innerHTML);
} else {
this._tranNodes.set(el, q);
}
const keeps = []; const keeps = [];
// 翻译开始钩子函数
const { transStartHook } = this._rule;
if (transStartHook?.trim()) {
interpreter.run(`exports.transStartHook = ${transStartHook}`);
interpreter.exports.transStartHook(el, q);
}
// 保留元素 // 保留元素
const [matchSelector, subSelector] = this._keepSelector; const keepSelector = this._keepSelector.trim();
if (matchSelector || subSelector) { if (keepSelector) {
let text = ""; let text = "";
el.childNodes.forEach((child) => { el.childNodes.forEach((child) => {
if ( if (child.nodeType === 1 && child.matches(keepSelector)) {
child.nodeType === 1 &&
((matchSelector && child.matches(matchSelector)) ||
(subSelector && child.querySelector(subSelector)))
) {
if (child.nodeName === "IMG") { if (child.nodeName === "IMG") {
child.style.cssText += `width: ${child.width}px;`; child.style.cssText += `width: ${child.width}px;`;
child.style.cssText += `height: ${child.height}px;`; child.style.cssText += `height: ${child.height}px;`;
} }
text += `[${keeps.length}]`; text += `[${keeps.length}]`;
keeps.push(child.outerHTML); keeps.push(child.outerHTML);
} else { } else if (child.nodeType === 1 || child.nodeType === 3) {
text += child.textContent; text += child.textContent;
} }
}); });
@@ -538,18 +541,22 @@ export class Translator {
} }
} }
traEl = document.createElement(APP_LCNAME); // 附加样式
traEl.style.visibility = "visible";
// if (this._rule.transOnly === "true") {
// el.innerHTML = "";
// }
const { selectStyle, parentStyle } = this._rule; const { selectStyle, parentStyle } = this._rule;
el.appendChild(traEl);
el.style.cssText += selectStyle; el.style.cssText += selectStyle;
if (el.parentElement) { if (el.parentElement) {
el.parentElement.style.cssText += parentStyle; el.parentElement.style.cssText += parentStyle;
} }
// 插入译文节点
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
// if (this._rule.transOnly === "true") {
// el.innerHTML = "";
// }
el.appendChild(traEl);
// 渲染译文节点
const root = createRoot(traEl); const root = createRoot(traEl);
root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />); root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
}; };

View File

@@ -15,6 +15,16 @@ export const limitNumber = (num, min = 0, max = 100) => {
return number; return number;
}; };
export const limitFloat = (num, min = 0, max = 100) => {
const number = parseFloat(num);
if (Number.isNaN(number) || number < min) {
return min;
} else if (number > max) {
return max;
}
return number;
};
/** /**
* 匹配是否为数组中的值 * 匹配是否为数组中的值
* @param {*} arr * @param {*} arr
@@ -240,3 +250,20 @@ export const blobToBase64 = (blob) => {
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
}; };
/**
* 获取html内的文本
* @param {*} htmlStr
* @param {*} skipTag
* @returns
*/
export const getHtmlText = (htmlStr, skipTag = "") => {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlStr, "text/html");
if (skipTag) {
doc.querySelectorAll(skipTag).forEach((el) => el.remove());
}
return doc.body.innerText.trim();
};

View File

@@ -15,6 +15,7 @@ import {
import { useTranslate } from "../../hooks/Translate"; import { useTranslate } from "../../hooks/Translate";
import { styled, css } from "@mui/material/styles"; import { styled, css } from "@mui/material/styles";
import { APP_LCNAME } from "../../config"; import { APP_LCNAME } from "../../config";
import interpreter from "../../libs/interpreter";
const LINE_STYLES = { const LINE_STYLES = {
[OPT_STYLE_LINE]: "solid", [OPT_STYLE_LINE]: "solid",
@@ -85,8 +86,15 @@ const StyledSpan = styled("span")`
export default function Content({ q, keeps, translator, $el }) { export default function Content({ q, keeps, translator, $el }) {
const [rule, setRule] = useState(translator.rule); const [rule, setRule] = useState(translator.rule);
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting); const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { transOpen, textStyle, bgColor, textDiyStyle, transOnly, transTag } = const {
rule; transOpen,
textStyle,
bgColor,
textDiyStyle,
transOnly,
transTag,
transEndHook,
} = rule;
const { newlineLength } = translator.setting; const { newlineLength } = translator.setting;
@@ -107,6 +115,14 @@ export default function Content({ q, keeps, translator, $el }) {
}; };
}, [translator.eventName]); }, [translator.eventName]);
useEffect(() => {
// 运行钩子函数
if (text && transEndHook?.trim()) {
interpreter.run(`exports.transEndHook = ${transEndHook}`);
interpreter.exports.transEndHook($el, q, text, keeps);
}
}, [$el, q, text, keeps, transEndHook]);
const gap = useMemo(() => { const gap = useMemo(() => {
if (transOnly === "true") { if (transOnly === "true") {
return ""; return "";

View File

@@ -2,6 +2,9 @@ import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import LoadingButton from "@mui/lab/LoadingButton"; import LoadingButton from "@mui/lab/LoadingButton";
import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { import {
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
@@ -10,20 +13,27 @@ import {
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2, OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3, OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI, OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA, OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2, OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3, OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
OPT_TRANS_NIUTRANS, OPT_TRANS_NIUTRANS,
URL_KISS_PROXY,
URL_NIUTRANS_REG, URL_NIUTRANS_REG,
DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT,
} from "../../config"; } from "../../config";
import { useState } from "react"; import { useState } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -38,7 +48,7 @@ import { useApi } from "../../hooks/Api";
import { apiTranslate } from "../../apis"; import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { limitNumber } from "../../libs/utils"; import { limitNumber, limitFloat } from "../../libs/utils";
function TestButton({ translator, api }) { function TestButton({ translator, api }) {
const i18n = useI18n(); const i18n = useI18n();
@@ -56,7 +66,7 @@ function TestButton({ translator, api }) {
useCache: false, useCache: false,
}); });
if (!text) { if (!text) {
throw new Error("empty reault"); throw new Error("empty result");
} }
alert.success(i18n("test_success")); alert.success(i18n("test_success"));
} catch (err) { } catch (err) {
@@ -107,19 +117,27 @@ function TestButton({ translator, api }) {
); );
} }
function ApiFields({ translator }) { function ApiFields({ translator, api, updateApi, resetApi }) {
const i18n = useI18n(); const i18n = useI18n();
const { api, updateApi, resetApi } = useApi(translator);
const { const {
url = "", url = "",
key = "", key = "",
model = "", model = "",
prompt = "", systemPrompt = "",
userPrompt = "",
think = false,
thinkIgnore = "",
fetchLimit = DEFAULT_FETCH_LIMIT, fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL, fetchInterval = DEFAULT_FETCH_INTERVAL,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
dictNo = "", dictNo = "",
memoryNo = "", memoryNo = "",
customOption = "", reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
apiName = "",
isDisabled = false,
} = api; } = api;
const handleChange = (e) => { const handleChange = (e) => {
@@ -131,6 +149,15 @@ function ApiFields({ translator }) {
case "fetchInterval": case "fetchInterval":
value = limitNumber(value, 0, 5000); value = limitNumber(value, 0, 5000);
break; break;
case "httpTimeout":
value = limitNumber(value, 5000, 30000);
break;
case "temperature":
value = limitFloat(value, 0, 2);
break;
case "maxTokens":
value = limitNumber(value, 0, 2 ** 15);
break;
default: default:
} }
updateApi({ updateApi({
@@ -143,6 +170,7 @@ function ApiFields({ translator }) {
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
]; ];
const mulkeysTranslators = [ const mulkeysTranslators = [
@@ -151,11 +179,18 @@ function ApiFields({ translator }) {
OPT_TRANS_OPENAI_2, OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3, OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI, OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA, OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2, OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3, OPT_TRANS_OLLAMA_3,
OPT_TRANS_NIUTRANS, OPT_TRANS_NIUTRANS,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
]; ];
const keyHelper = const keyHelper =
@@ -174,6 +209,14 @@ function ApiFields({ translator }) {
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<TextField
size="small"
label={i18n("api_name")}
name="apiName"
value={apiName}
onChange={handleChange}
/>
{!builtinTranslators.includes(translator) && ( {!builtinTranslators.includes(translator) && (
<> <>
<TextField <TextField
@@ -203,7 +246,8 @@ function ApiFields({ translator }) {
{(translator.startsWith(OPT_TRANS_OPENAI) || {(translator.startsWith(OPT_TRANS_OPENAI) ||
translator.startsWith(OPT_TRANS_OLLAMA) || translator.startsWith(OPT_TRANS_OLLAMA) ||
translator === OPT_TRANS_GEMINI) && ( translator === OPT_TRANS_CLAUDE ||
translator.startsWith(OPT_TRANS_GEMINI)) && (
<> <>
<TextField <TextField
size="small" size="small"
@@ -214,13 +258,69 @@ function ApiFields({ translator }) {
/> />
<TextField <TextField
size="small" size="small"
label={"PROMPT"} label={"SYSTEM PROMPT"}
name="prompt" name="systemPrompt"
value={prompt} value={systemPrompt}
onChange={handleChange} onChange={handleChange}
multiline multiline
maxRows={10} maxRows={10}
/> />
<TextField
size="small"
label={"USER PROMPT"}
name="userPrompt"
value={userPrompt}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)}
{translator.startsWith(OPT_TRANS_OLLAMA) && (
<>
<TextField
select
size="small"
name="think"
value={think}
label={i18n("if_think")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("nothink")}</MenuItem>
<MenuItem value={true}>{i18n("think")}</MenuItem>
</TextField>
<TextField
size="small"
label={i18n("think_ignore")}
name="thinkIgnore"
value={thinkIgnore}
onChange={handleChange}
/>
</>
)}
{(translator.startsWith(OPT_TRANS_OPENAI) ||
translator === OPT_TRANS_CLAUDE ||
translator === OPT_TRANS_GEMINI ||
translator === OPT_TRANS_GEMINI_2) && (
<>
<TextField
size="small"
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
<TextField
size="small"
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</> </>
)} )}
@@ -244,15 +344,26 @@ function ApiFields({ translator }) {
)} )}
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && ( {translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<TextField <>
size="small" <TextField
label={i18n("custom_option")} size="small"
name="customOption" label={"Request Hook"}
value={customOption} name="reqHook"
onChange={handleChange} value={reqHook}
multiline onChange={handleChange}
maxRows={10} multiline
/> maxRows={10}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)} )}
<TextField <TextField
@@ -273,6 +384,29 @@ function ApiFields({ translator }) {
onChange={handleChange} onChange={handleChange}
/> />
<TextField
size="small"
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
defaultValue={httpTimeout}
onChange={handleChange}
/>
<FormControlLabel
control={
<Switch
size="small"
name="isDisabled"
checked={isDisabled}
onChange={() => {
updateApi({ isDisabled: !isDisabled });
}}
/>
}
label={i18n("is_disabled")}
/>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} /> <TestButton translator={translator} api={api} />
<Button <Button
@@ -295,6 +429,7 @@ function ApiFields({ translator }) {
function ApiAccordion({ translator }) { function ApiAccordion({ translator }) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const { api, updateApi, resetApi } = useApi(translator);
const handleChange = (e) => { const handleChange = (e) => {
setExpanded((pre) => !pre); setExpanded((pre) => !pre);
@@ -303,10 +438,19 @@ function ApiAccordion({ translator }) {
return ( return (
<Accordion expanded={expanded} onChange={handleChange}> <Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{translator}</Typography> <Typography>
{api.apiName ? `${translator} (${api.apiName})` : translator}
</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{expanded && <ApiFields translator={translator} />} {expanded && (
<ApiFields
translator={translator}
api={api}
updateApi={updateApi}
resetApi={resetApi}
/>
)}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );
@@ -317,11 +461,7 @@ export default function Apis() {
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Alert severity="info"> <Alert severity="info">{i18n("about_api")}</Alert>
<Link href={URL_KISS_PROXY} target="_blank">
{i18n("about_api_proxy")}
</Link>
</Alert>
<Box> <Box>
{OPT_TRANS_ALL.map((translator) => ( {OPT_TRANS_ALL.map((translator) => (

View File

@@ -96,9 +96,7 @@ export default function FavWords() {
.join(" "), .join(" "),
dictResult.content[0].mean dictResult.content[0].mean
.map(({ pre, cont }) => { .map(({ pre, cont }) => {
return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join( return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
"; "
)}`;
}) })
.join("\n"), .join("\n"),
].join("\n\n") ].join("\n\n")

View File

@@ -27,6 +27,7 @@ import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { useRules } from "../../hooks/Rules"; import { useRules } from "../../hooks/Rules";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
@@ -93,10 +94,14 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
transTiming = OPT_TIMING_PAGESCROLL, transTiming = OPT_TIMING_PAGESCROLL,
transTag = DEFAULT_TRANS_TAG, transTag = DEFAULT_TRANS_TAG,
transTitle = "false", transTitle = "false",
transSelected = "true",
detectRemote = "false", detectRemote = "false",
skipLangs = [], skipLangs = [],
fixerSelector = "", fixerSelector = "",
fixerFunc = "-", fixerFunc = "-",
transStartHook = "",
transEndHook = "",
transRemoveHook = "",
} = formValues; } = formValues;
const hasSamePattern = (str) => { const hasSamePattern = (str) => {
@@ -177,6 +182,30 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</MenuItem> </MenuItem>
); );
const ShowMoreButton = showMore ? (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(false);
}}
startIcon={<ExpandLessIcon />}
>
{i18n("less")}
</Button>
) : (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Stack spacing={2}> <Stack spacing={2}>
@@ -339,95 +368,139 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
/> />
)} )}
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transOnly"
value={transOnly}
label={i18n("show_only_translations")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTiming"
value={transTiming}
label={i18n("trigger_mode")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_TIMING_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTag"
value={transTag}
label={i18n("translation_element_tag")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"span"}>{`<span>`}</MenuItem>
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTitle"
value={transTitle}
label={i18n("translate_page_title")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transSelected"
value={transSelected}
label={i18n("translate_selected")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
</Grid>
</Box>
{showMore && ( {showMore && (
<> <>
<Box> <TextField
<Grid container spacing={2} columns={12}> size="small"
<Grid item xs={12} sm={6} md={3} lg={2}> label={i18n("fixer_selector")}
<TextField name="fixerSelector"
select value={fixerSelector}
size="small" disabled={disabled}
fullWidth onChange={handleChange}
name="transOnly" multiline
value={transOnly} maxRows={10}
label={i18n("show_only_translations")} />
disabled={disabled} <TextField
onChange={handleChange} select
> size="small"
{GlobalItem} name="fixerFunc"
<MenuItem value={"false"}>{i18n("disable")}</MenuItem> value={fixerFunc}
<MenuItem value={"true"}>{i18n("enable")}</MenuItem> label={i18n("fixer_function")}
</TextField> helperText={i18n("fixer_function_helper")}
</Grid> disabled={disabled}
<Grid item xs={12} sm={6} md={3} lg={2}> onChange={handleChange}
<TextField >
select {GlobalItem}
size="small" {FIXER_ALL.map((item) => (
fullWidth <MenuItem key={item} value={item}>
name="transTiming" {item}
value={transTiming} </MenuItem>
label={i18n("trigger_mode")} ))}
disabled={disabled} </TextField>
onChange={handleChange}
>
{GlobalItem}
{OPT_TIMING_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTag"
value={transTag}
label={i18n("translation_element_tag")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"span"}>{`<span>`}</MenuItem>
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTitle"
value={transTitle}
label={i18n("translate_page_title")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
</Grid>
</Box>
<TextField <TextField
select select
@@ -458,34 +531,42 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
multiline multiline
maxRows={10}
/> />
<TextField <TextField
size="small" size="small"
label={i18n("fixer_selector")} label={i18n("translate_start_hook")}
name="fixerSelector" helperText={i18n("translate_start_hook_helper")}
value={fixerSelector} name="transStartHook"
value={transStartHook}
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
multiline multiline
maxRows={10}
/> />
<TextField <TextField
select
size="small" size="small"
name="fixerFunc" label={i18n("translate_end_hook")}
value={fixerFunc} helperText={i18n("translate_end_hook_helper")}
label={i18n("fixer_function")} name="transEndHook"
helperText={i18n("fixer_function_helper")} value={transEndHook}
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
> multiline
{GlobalItem} maxRows={10}
{FIXER_ALL.map((item) => ( />
<MenuItem key={item} value={item}> <TextField
{item} size="small"
</MenuItem> label={i18n("translate_remove_hook")}
))} helperText={i18n("translate_remove_hook_helper")}
</TextField> name="transRemoveHook"
value={transRemoveHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField <TextField
size="small" size="small"
@@ -564,18 +645,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
{i18n("delete")} {i18n("delete")}
</Button> </Button>
)} )}
{!showMore && ( {ShowMoreButton}
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
</> </>
) : ( ) : (
<> <>
@@ -595,18 +665,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
> >
{i18n("cancel")} {i18n("cancel")}
</Button> </Button>
{!showMore && ( {ShowMoreButton}
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
</> </>
)} )}
</Stack> </Stack>
@@ -629,17 +688,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
> >
{i18n("cancel")} {i18n("cancel")}
</Button> </Button>
{!showMore && ( {ShowMoreButton}
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
>
{i18n("more")}
</Button>
)}
</Stack> </Stack>
))} ))}
</Stack> </Stack>

View File

@@ -17,6 +17,8 @@ import {
UI_LANGS, UI_LANGS,
TRANS_NEWLINE_LENGTH, TRANS_NEWLINE_LENGTH,
CACHE_NAME, CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_LANGDETECTOR_ALL,
OPT_SHORTCUT_TRANSLATE, OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE, OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP, OPT_SHORTCUT_POPUP,
@@ -25,12 +27,15 @@ import {
DEFAULT_CSPLIST, DEFAULT_CSPLIST,
MSG_CONTEXT_MENUS, MSG_CONTEXT_MENUS,
MSG_UPDATE_CSP, MSG_UPDATE_CSP,
DEFAULT_HTTP_TIMEOUT,
} from "../../config"; } from "../../config";
import { useShortcut } from "../../hooks/Shortcut"; import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab"; import { useFab } from "../../hooks/Fab";
import { sendBgMsg } from "../../libs/msg"; import { sendBgMsg } from "../../libs/msg";
import { kissLog } from "../../libs/log"; import { kissLog } from "../../libs/log";
import UploadButton from "./UploadButton";
import DownloadButton from "./DownloadButton";
function ShortcutItem({ action, label }) { function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action); const { shortcut, setShortcut } = useShortcut(action);
@@ -67,6 +72,9 @@ export default function Settings() {
case "newlineLength": case "newlineLength":
value = limitNumber(value, 1, 1000); value = limitNumber(value, 1, 1000);
break; break;
case "httpTimeout":
value = limitNumber(value, 5000, 30000);
break;
case "touchTranslate": case "touchTranslate":
value = limitNumber(value, 0, 4); value = limitNumber(value, 0, 4);
break; break;
@@ -92,23 +100,48 @@ export default function Settings() {
} }
}; };
const handleImport = async (data) => {
try {
await updateSetting(JSON.parse(data));
} catch (err) {
kissLog(err, "import setting");
}
};
const { const {
uiLang, uiLang,
minLength, minLength,
maxLength, maxLength,
clearCache, clearCache,
newlineLength = TRANS_NEWLINE_LENGTH, newlineLength = TRANS_NEWLINE_LENGTH,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
contextMenuType = 1, contextMenuType = 1,
touchTranslate = 2, touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"), blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"), csplist = DEFAULT_CSPLIST.join(",\n"),
transInterval = 500, transInterval = 500,
langDetector = OPT_TRANS_MICROSOFT,
} = setting; } = setting;
const { isHide = false } = fab || {}; const { isHide = false } = fab || {};
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<UploadButton text={i18n("import")} handleImport={handleImport} />
<DownloadButton
handleData={() => JSON.stringify(setting, null, 2)}
text={i18n("export")}
fileName={`kiss-setting_${Date.now()}.json`}
/>
</Stack>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("ui_lang")}</InputLabel> <InputLabel>{i18n("ui_lang")}</InputLabel>
<Select <Select
@@ -160,7 +193,14 @@ export default function Settings() {
defaultValue={transInterval} defaultValue={transInterval}
onChange={handleChange} onChange={handleChange}
/> />
<TextField
size="small"
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
defaultValue={httpTimeout}
onChange={handleChange}
/>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel> <InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
<Select <Select
@@ -206,6 +246,22 @@ export default function Settings() {
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small">
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
<Select
name="langDetector"
value={langDetector}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
{OPT_LANGDETECTOR_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</Select>
</FormControl>
{isExt ? ( {isExt ? (
<> <>
<FormControl size="small"> <FormControl size="small">

View File

@@ -65,6 +65,7 @@ export default function SyncSetting() {
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Alert severity="warning">{i18n("sync_warn")}</Alert> <Alert severity="warning">{i18n("sync_warn")}</Alert>
<Alert severity="warning">{i18n("sync_warn_2")}</Alert>
<TextField <TextField
select select

View File

@@ -9,10 +9,9 @@ import {
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_TRANBOX_TRIGGER_CLICK, OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL, OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU,
} from "../../config"; } from "../../config";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useCallback } from "react"; import { useCallback } from "react";
import { limitNumber } from "../../libs/utils"; import { limitNumber } from "../../libs/utils";
import { useTranbox } from "../../hooks/Tranbox"; import { useTranbox } from "../../hooks/Tranbox";
@@ -47,7 +46,6 @@ export default function Tranbox() {
); );
const { const {
transOpen,
translator, translator,
fromLang, fromLang,
toLang, toLang,
@@ -63,25 +61,12 @@ export default function Tranbox() {
followSelection = false, followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles = "", extStyles = "",
enDict = OPT_DICT_BAIDU,
} = tranboxSetting; } = tranboxSetting;
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateTranbox({ transOpen: !transOpen });
}}
/>
}
label={i18n("toggle_selection_translate")}
/>
<TextField <TextField
select select
size="small" size="small"
@@ -143,6 +128,18 @@ export default function Tranbox() {
))} ))}
</TextField> </TextField>
<TextField
select
size="small"
name="enDict"
value={enDict}
label={i18n("english_dict")}
onChange={handleChange}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
<MenuItem value={OPT_DICT_BAIDU}>{OPT_DICT_BAIDU}</MenuItem>
</TextField>
<TextField <TextField
size="small" size="small"
label={i18n("tranbtn_offset_x")} label={i18n("tranbtn_offset_x")}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
@@ -23,6 +23,7 @@ import {
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_STYLE_ALL, OPT_STYLE_ALL,
DEFAULT_TRANS_APIS,
} from "../../config"; } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules"; import { saveRule } from "../../libs/rules";
@@ -32,6 +33,7 @@ import { kissLog } from "../../libs/log";
export default function Popup({ setShowPopup, translator: tran }) { export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n(); const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule); const [rule, setRule] = useState(tran?.rule);
const [transApis, setTransApis] = useState(tran?.setting?.transApis || []);
const [commands, setCommands] = useState({}); const [commands, setCommands] = useState({});
const handleOpenSetting = () => { const handleOpenSetting = () => {
@@ -106,7 +108,8 @@ export default function Popup({ setShowPopup, translator: tran }) {
try { try {
const res = await sendTabMsg(MSG_TRANS_GETRULE); const res = await sendTabMsg(MSG_TRANS_GETRULE);
if (!res.error) { if (!res.error) {
setRule(res.data); setRule(res.rule);
setTransApis(res.setting.transApis);
} }
} catch (err) { } catch (err) {
kissLog(err, "query rule"); kissLog(err, "query rule");
@@ -138,6 +141,20 @@ export default function Popup({ setShowPopup, translator: tran }) {
})(); })();
}, [tran]); }, [tran]);
const optApis = useMemo(
() =>
OPT_TRANS_ALL.map((key) => ({
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
apiKey: key,
}))
.filter((item) => !item.isDisabled)
.map(({ apiKey, apiName }) => ({
key: apiKey,
name: apiName?.trim() || apiKey,
})),
[transApis]
);
if (!rule) { if (!rule) {
return ( return (
<Box minWidth={300}> <Box minWidth={300}>
@@ -197,9 +214,9 @@ export default function Popup({ setShowPopup, translator: tran }) {
label={i18n("translate_service")} label={i18n("translate_service")}
onChange={handleChange} onChange={handleChange}
> >
{OPT_TRANS_ALL.map((item) => ( {optApis.map(({ key, name }) => (
<MenuItem key={item} value={item}> <MenuItem key={key} value={key}>
{item} {name}
</MenuItem> </MenuItem>
))} ))}
</TextField> </TextField>

View File

@@ -93,9 +93,7 @@ export default function DictCont({ text }) {
key={key} key={key}
style={{ display: "inline-block" }} style={{ display: "inline-block" }}
> >
<Typography component="span">{`${ <Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
PHONIC_MAP[key]?.[0] || key
} ${val}`}</Typography>
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} /> <AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
</Typography> </Typography>
))} ))}

View File

@@ -18,8 +18,13 @@ import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen"; import LockOpenIcon from "@mui/icons-material/LockOpen";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config"; import {
import { useState, useRef } from "react"; OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
DEFAULT_TRANS_APIS,
} from "../../config";
import { useState, useRef, useMemo } from "react";
import TranCont from "./TranCont"; import TranCont from "./TranCont";
import DictCont from "./DictCont"; import DictCont from "./DictCont";
import SugCont from "./SugCont"; import SugCont from "./SugCont";
@@ -101,7 +106,15 @@ function Header({
); );
} }
function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) { function TranForm({
text,
setText,
tranboxSetting,
transApis,
simpleStyle,
langDetector,
enDict,
}) {
const i18n = useI18n(); const i18n = useI18n();
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
@@ -111,6 +124,20 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
const [toLang, setToLang] = useState(tranboxSetting.toLang); const [toLang, setToLang] = useState(tranboxSetting.toLang);
const inputRef = useRef(null); const inputRef = useRef(null);
const optApis = useMemo(
() =>
OPT_TRANS_ALL.map((key) => ({
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
apiKey: key,
}))
.filter((item) => !item.isDisabled)
.map(({ apiKey, apiName }) => ({
key: apiKey,
name: apiName?.trim() || apiKey,
})),
[transApis]
);
return ( return (
<Stack <Stack
className="KT-transbox-container" className="KT-transbox-container"
@@ -174,9 +201,9 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
setTranslator(e.target.value); setTranslator(e.target.value);
}} }}
> >
{OPT_TRANS_ALL.map((item) => ( {optApis.map(({ key, name }) => (
<MenuItem key={item} value={item}> <MenuItem key={key} value={key}>
{item} {name}
</MenuItem> </MenuItem>
))} ))}
</TextField> </TextField>
@@ -233,7 +260,10 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
</> </>
)} )}
{(!simpleStyle || !isValidWord(text) || !toLang.startsWith("zh")) && ( {(!simpleStyle ||
!isValidWord(text) ||
!toLang.startsWith("zh") ||
enDict === "-") && (
<TranCont <TranCont
text={text} text={text}
translator={translator} translator={translator}
@@ -242,11 +272,16 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
toLang2={tranboxSetting.toLang2} toLang2={tranboxSetting.toLang2}
transApis={transApis} transApis={transApis}
simpleStyle={simpleStyle} simpleStyle={simpleStyle}
langDetector={langDetector}
/> />
)} )}
<DictCont text={text} /> {enDict !== "-" && (
<SugCont text={text} /> <>
<DictCont text={text} />
<SugCont text={text} />
</>
)}
</Stack> </Stack>
); );
} }
@@ -268,6 +303,8 @@ export default function TranBox({
followSelection, followSelection,
setFollowSelection, setFollowSelection,
extStyles, extStyles,
langDetector,
enDict,
}) { }) {
const [mouseHover, setMouseHover] = useState(false); const [mouseHover, setMouseHover] = useState(false);
return ( return (
@@ -300,6 +337,8 @@ export default function TranBox({
tranboxSetting={tranboxSetting} tranboxSetting={tranboxSetting}
transApis={transApis} transApis={transApis}
simpleStyle={simpleStyle} simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
/> />
</DraggableResizable> </DraggableResizable>
</ThemeProvider> </ThemeProvider>

View File

@@ -5,10 +5,11 @@ import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS } from "../../config"; import { DEFAULT_TRANS_APIS } from "../../config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { apiTranslate, apiBaiduLangdetect } from "../../apis"; import { apiTranslate } from "../../apis";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import { tryDetectLang } from "../../libs";
export default function TranCont({ export default function TranCont({
text, text,
@@ -18,6 +19,7 @@ export default function TranCont({
toLang2 = "en", toLang2 = "en",
transApis, transApis,
simpleStyle, simpleStyle,
langDetector,
}) { }) {
const i18n = useI18n(); const i18n = useI18n();
const [trText, setTrText] = useState(""); const [trText, setTrText] = useState("");
@@ -32,8 +34,8 @@ export default function TranCont({
setError(""); setError("");
let to = toLang; let to = toLang;
if (toLang !== toLang2 && toLang2 !== "none") { if (fromLang === "auto" && toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await apiBaiduLangdetect(text); const detectLang = await tryDetectLang(text, true, langDetector);
if (detectLang === toLang) { if (detectLang === toLang) {
to = toLang2; to = toLang2;
} }
@@ -55,7 +57,7 @@ export default function TranCont({
setLoading(false); setLoading(false);
} }
})(); })();
}, [text, translator, fromLang, toLang, toLang2, transApis]); }, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
if (simpleStyle) { if (simpleStyle) {
return ( return (

View File

@@ -10,6 +10,7 @@ import {
OPT_TRANBOX_TRIGGER_CLICK, OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER, OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT, OPT_TRANBOX_TRIGGER_SELECT,
OPT_DICT_BAIDU,
} from "../../config"; } from "../../config";
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log"; import { kissLog } from "../../libs/log";
@@ -20,6 +21,7 @@ export default function Slection({
tranboxSetting, tranboxSetting,
transApis, transApis,
uiLang, uiLang,
langDetector,
}) { }) {
const { const {
hideTranBtn = false, hideTranBtn = false,
@@ -33,6 +35,7 @@ export default function Slection({
btnOffsetY, btnOffsetY,
boxOffsetX = 0, boxOffsetX = 0,
boxOffsetY = 10, boxOffsetY = 10,
enDict = OPT_DICT_BAIDU,
} = tranboxSetting; } = tranboxSetting;
const boxWidth = const boxWidth =
@@ -234,6 +237,8 @@ export default function Slection({
followSelection={followSelection} followSelection={followSelection}
setFollowSelection={setFollowSelection} setFollowSelection={setFollowSelection}
extStyles={extStyles} extStyles={extStyles}
langDetector={langDetector}
enDict={enDict}
/> />
)} )}