Compare commits

...

109 Commits

Author SHA1 Message Date
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
Gabe Yuan
5d44ff4913 v1.8.9 2024-04-28 22:23:53 +08:00
Gabe Yuan
4c9aa66048 feat: support ollama api 2024-04-28 21:45:20 +08:00
Gabe Yuan
b6a09b99ab feat: support ollama api 2024-04-28 21:43:20 +08:00
Gabe Yuan
3a0dcb1a52 feat: add more openai translator 2024-04-28 16:58:09 +08:00
Gabe Yuan
5015503b4c feat: hide transbox header when mouseleave 2024-04-28 14:56:49 +08:00
Gabe Yuan
16423feea4 fix: update readme 2024-04-21 22:21:29 +08:00
Gabe Yuan
9703514698 v1.8.8 2024-04-21 19:25:00 +08:00
Gabe Yuan
de7a97fb76 feat: tranbox offset 2024-04-21 19:19:06 +08:00
Gabe Yuan
319aaf8132 fix: move taskpool from background to content 2024-04-21 13:16:44 +08:00
Gabe Yuan
74bc58ba91 feat: transbox follow selection 2024-04-20 18:07:16 +08:00
Gabe Yuan
d622db0d7c fix: i18n menu command for userscript 2024-04-20 15:54:41 +08:00
Gabe Yuan
de1ddf2362 fix: stopPropagation when close tranbox 2024-04-20 15:12:25 +08:00
Gabe Yuan
32c0fc860b fix: update readme 2024-04-20 14:11:32 +08:00
Gabe Yuan
1938f432dd feat: support multi url for DEEPLX 2024-04-20 14:01:34 +08:00
Gabe Yuan
a5cfb0ca1d fix: fetch pool retry 2024-04-20 11:52:16 +08:00
Gabe Yuan
a172234fb0 fix: niutrans i18n text 2024-04-18 12:31:16 +08:00
47 changed files with 9762 additions and 5963 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.7 REACT_APP_VERSION=1.9.0
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
- 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:
@@ -55,10 +56,10 @@ jobs:
strategy: strategy:
matrix: matrix:
client: ["chrome", "edge", "firefox", "userscript"] client: ["chrome", "edge", "firefox", "userscript"]
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

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

@@ -1,5 +1,7 @@
# KISS Translator # KISS Translator
English | [简体中文](README.md)
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator). A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
@@ -12,7 +14,7 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Chrome/Edge/Firefox/Kiwi/Orion - [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari - [ ] Safari
- [x] Supports multiple translation services - [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [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

View File

@@ -1,5 +1,7 @@
# 简约翻译 # 简约翻译
[English](README.en.md) | 简体中文
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。 一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
@@ -12,7 +14,7 @@
- [x] Chrome/Edge/Firefox/Kiwi/Orion - [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari - [ ] Safari
- [x] 支持多种翻译服务 - [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] 自定义翻译接口 - [x] 自定义翻译接口
- [x] 覆盖常见翻译场景 - [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译 - [x] 网页双语对照翻译

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,10 +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
// @run-at document-end // @run-at document-end
// ==/UserScript== // ==/UserScript==

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.7", "version": "1.9.0",
"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,15 @@
"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 && zip -r chrome.zip chrome && zip -r edge.zip edge && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.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 +46,8 @@
"globals": { "globals": {
"GM": true, "GM": true,
"unsafeWindow": true, "unsafeWindow": true,
"globalThis": true "globalThis": true,
"messenger": true
} }
}, },
"browserslist": { "browserslist": {
@@ -63,6 +67,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.7", "version": "1.9.0",
"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.7", "version": "1.9.0",
"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.0",
"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

@@ -1,243 +1,5 @@
import queryString from "query-string"; import queryString from "query-string";
import { getBdauth, setBdauth } from "../libs/storage"; import { URL_BAIDU_TRANSAPI, DEFAULT_USER_AGENT } from "../config";
import {
URL_BAIDU_WEB,
URL_BAIDU_TRANSAPI_V2,
URL_BAIDU_TRANSAPI,
DEFAULT_USER_AGENT,
} from "../config";
import { fetchApi } from "../libs/fetch";
/* eslint-disable */
function n(t, e) {
for (var n = 0; n < e.length - 2; n += 3) {
var r = e.charAt(n + 2);
(r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r)),
(r = "+" === e.charAt(n + 1) ? t >>> r : t << r),
(t = "+" === e.charAt(n) ? (t + r) & 4294967295 : t ^ r);
}
return t;
}
function e(t, e) {
(null == e || e > t.length) && (e = t.length);
for (var n = 0, r = new Array(e); n < e; n++) r[n] = t[n];
return r;
}
/* eslint-disable */
function getSign(t, gtk, r = null) {
var o,
i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === i) {
var a = t.length;
a > 30 &&
(t = ""
.concat(t.substr(0, 10))
.concat(t.substr(Math.floor(a / 2) - 5, 10))
.concat(t.substr(-10, 10)));
} else {
for (
var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),
c = 0,
u = s.length,
l = [];
c < u;
c++
)
"" !== s[c] &&
l.push.apply(
l,
(function (t) {
if (Array.isArray(t)) return e(t);
})((o = s[c].split(""))) ||
(function (t) {
if (
("undefined" != typeof Symbol && null != t[Symbol.iterator]) ||
null != t["@@iterator"]
)
return Array.from(t);
})(o) ||
(function (t, n) {
if (t) {
if ("string" == typeof t) return e(t, n);
var r = Object.prototype.toString.call(t).slice(8, -1);
return (
"Object" === r && t.constructor && (r = t.constructor.name),
"Map" === r || "Set" === r
? Array.from(t)
: "Arguments" === r ||
/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)
? e(t, n)
: void 0
);
}
})(o) ||
(function () {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
);
})()
),
c !== u - 1 && l.push(i[c]);
var p = l.length;
p > 30 &&
(t =
l.slice(0, 10).join("") +
l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") +
l.slice(-10).join(""));
}
for (
var d = ""
.concat(String.fromCharCode(103))
.concat(String.fromCharCode(116))
.concat(String.fromCharCode(107)),
h = (null !== r ? r : (r = gtk || "") || "").split("."),
f = Number(h[0]) || 0,
m = Number(h[1]) || 0,
g = [],
y = 0,
v = 0;
v < t.length;
v++
) {
var _ = t.charCodeAt(v);
_ < 128
? (g[y++] = _)
: (_ < 2048
? (g[y++] = (_ >> 6) | 192)
: (55296 == (64512 & _) &&
v + 1 < t.length &&
56320 == (64512 & t.charCodeAt(v + 1))
? ((_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v))),
(g[y++] = (_ >> 18) | 240),
(g[y++] = ((_ >> 12) & 63) | 128))
: (g[y++] = (_ >> 12) | 224),
(g[y++] = ((_ >> 6) & 63) | 128)),
(g[y++] = (63 & _) | 128));
}
for (
var b = f,
w =
""
.concat(String.fromCharCode(43))
.concat(String.fromCharCode(45))
.concat(String.fromCharCode(97)) +
""
.concat(String.fromCharCode(94))
.concat(String.fromCharCode(43))
.concat(String.fromCharCode(54)),
k =
""
.concat(String.fromCharCode(43))
.concat(String.fromCharCode(45))
.concat(String.fromCharCode(51)) +
""
.concat(String.fromCharCode(94))
.concat(String.fromCharCode(43))
.concat(String.fromCharCode(98)) +
""
.concat(String.fromCharCode(43))
.concat(String.fromCharCode(45))
.concat(String.fromCharCode(102)),
x = 0;
x < g.length;
x++
)
b = n((b += g[x]), w);
return (
(b = n(b, k)),
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
);
}
const getToken = async () => {
const res = await fetchApi({
input: URL_BAIDU_WEB,
init: {
headers: {
"Content-type": "text/html; charset=utf-8",
},
},
});
if (!res.ok) {
throw new Error(res.statusText);
}
const text = await res.text();
const token = text.match(/token: '(.*)',/)[1];
const gtk = text.match(/gtk = "(.*)";/)[1];
const exp = Date.now() + 8 * 60 * 60 * 1000;
if (!token || !gtk) {
throw new Error("[baidu] get token error");
}
return { token, gtk, exp };
};
/**
* 闭包缓存token减少对storage查询
* @returns
*/
const _bdAuth = () => {
let store;
return async () => {
const now = Date.now();
// 查询内存缓存
if (store && store.exp > now) {
return store;
}
// 查询storage缓存
store = await getBdauth();
if (store && store.exp > now) {
return store;
}
// 缓存没有或失效,查询接口
store = await getToken();
await setBdauth(store);
return store;
};
};
const bdAuth = _bdAuth();
/**
* 失效作废
* @param {*} param0
* @returns
*/
export const genBaiduV2 = async ({ text, from, to }) => {
const { token, gtk } = await bdAuth();
const sign = getSign(text, gtk);
const data = {
from,
to,
query: text,
simple_means_flag: 3,
sign,
token,
domain: "common",
ts: Date.now(),
};
const input = `${URL_BAIDU_TRANSAPI_V2}?from=${from}&to=${to}`;
const init = {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: queryString.stringify(data),
};
return [input, init];
};
export const genBaidu = async ({ text, from, to }) => { export const genBaidu = async ({ text, from, to }) => {
const data = { const data = {

View File

@@ -1,7 +1,8 @@
import queryString from "query-string"; import queryString from "query-string";
import { fetchPolyfill } 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,9 +10,17 @@ 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_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_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2, OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3, OPT_TRANS_CUSTOMIZE_3,
@@ -19,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,
@@ -26,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";
/** /**
* 同步数据 * 同步数据
@@ -37,7 +52,7 @@ import { sha256 } from "../libs/utils";
* @returns * @returns
*/ */
export const apiSyncData = async (url, key, data) => export const apiSyncData = async (url, key, data) =>
fetchPolyfill(url, { fetchData(url, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`, Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
@@ -51,7 +66,53 @@ export const apiSyncData = async (url, key, data) =>
* @param {*} url * @param {*} url
* @returns * @returns
*/ */
export const apiFetch = (url) => fetchPolyfill(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;
};
/** /**
* 百度语言识别 * 百度语言识别
@@ -59,7 +120,7 @@ export const apiFetch = (url) => fetchPolyfill(url);
* @returns * @returns
*/ */
export const apiBaiduLangdetect = async (text) => { export const apiBaiduLangdetect = async (text) => {
const res = await fetchPolyfill(URL_BAIDU_LANGDETECT, { const res = await fetchData(URL_BAIDU_LANGDETECT, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
@@ -83,7 +144,7 @@ export const apiBaiduLangdetect = async (text) => {
* @returns * @returns
*/ */
export const apiBaiduSuggest = async (text) => { export const apiBaiduSuggest = async (text) => {
const res = await fetchPolyfill(URL_BAIDU_SUGGEST, { const res = await fetchData(URL_BAIDU_SUGGEST, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
@@ -110,7 +171,7 @@ export const apiBaiduSuggest = async (text) => {
*/ */
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => { export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`; const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`;
return fetchPolyfill(url, { return fetchData(url, {
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据 useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
}); });
}; };
@@ -128,7 +189,7 @@ export const apiTencentLangdetect = async (text) => {
text, text,
}); });
const res = await fetchPolyfill(URL_TENCENT_TRANSMART, { const res = await fetchData(URL_TENCENT_TRANSMART, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
@@ -166,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];
} }
@@ -187,7 +248,7 @@ export const apiTranslate = async ({
to, to,
}; };
const res = await fetchPolyfill( const res = await fetchData(
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`, `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
{ {
useCache, useCache,
@@ -202,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(" "))
@@ -240,10 +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_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;
@@ -253,36 +325,39 @@ 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;
break; break;
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3:
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;
break;
case OPT_TRANS_CUSTOMIZE: case OPT_TRANS_CUSTOMIZE:
case OPT_TRANS_CUSTOMIZE_2: case OPT_TRANS_CUSTOMIZE_2:
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:
} }

612
src/apis/trans.js Normal file
View File

@@ -0,0 +1,612 @@
import queryString from "query-string";
import {
OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_GEMINI_2,
OPT_TRANS_CLAUDE,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART,
URL_VOLCENGINE_TRAN,
INPUT_PLACE_URL,
INPUT_PLACE_FROM,
INPUT_PLACE_TO,
INPUT_PLACE_TEXT,
INPUT_PLACE_KEY,
INPUT_PLACE_MODEL,
} from "../config";
import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu";
import interpreter from "../libs/interpreter";
const keyMap = new Map();
const urlMap = new Map();
// 轮询key/url
const keyPick = (translator, key = "", cacheMap) => {
const keys = key
.split(/\n|,/)
.map((item) => item.trim())
.filter(Boolean);
if (keys.length === 0) {
return "";
}
const preIndex = cacheMap.get(translator) ?? -1;
const curIndex = (preIndex + 1) % keys.length;
cacheMap.set(translator, curIndex);
return keys[curIndex];
};
const genGoogle = ({ text, from, to, url, key }) => {
const params = {
client: "gtx",
dt: "t",
dj: 1,
ie: "UTF-8",
sl: from,
tl: to,
q: text,
};
const input = `${url}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
},
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
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 [token] = await msAuth();
const params = {
from,
to,
"api-version": "3.0",
};
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
};
return [input, init];
};
const genDeepl = ({ text, from, to, url, key }) => {
const data = {
text: [text],
target_lang: to,
source_lang: from,
// split_sentences: "0",
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `DeepL-Auth-Key ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genDeeplX = ({ text, from, to, url, key }) => {
const data = {
text,
target_lang: to,
source_lang: from,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
const data = {
from,
to,
apikey: key,
src_text: text,
dictNo,
memoryNo,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genTencent = ({ text, from, to }) => {
const data = {
header: {
fn: "auto_translation",
client_key:
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
},
type: "plain",
model_category: "normal",
source: {
text_list: [text],
lang: from,
},
target: {
lang: to,
},
};
const init = {
headers: {
"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",
body: JSON.stringify(data),
};
return [URL_TENCENT_TRANSMART, init];
};
const genVolcengine = ({ text, from, to }) => {
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_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,
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: userPrompt,
},
],
temperature,
max_tokens: maxTokens,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`, // OpenAI
"api-key": key, // Azure OpenAI
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genGemini = ({
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_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
system_instruction: {
parts: {
text: systemPrompt,
},
},
contents: {
role: "user",
parts: {
text: userPrompt,
},
},
generationConfig: {
maxOutputTokens: maxTokens,
temperature,
// topP: 0.8,
// topK: 10,
},
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genGemini2 = ({
text,
from,
to,
url,
key,
systemPrompt,
userPrompt,
model,
temperature,
reasoningEffort,
}) => {
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,
reasoning_effort: reasoningEffort,
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: userPrompt,
},
],
temperature,
};
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,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
const genCloudflareAI = ({ text, from, to, url, key }) => {
const data = {
text,
source_lang: from,
target_lang: to,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genCustom = ({ text, from, to, url, key, reqHook }) => {
url = url
.replaceAll(INPUT_PLACE_URL, url)
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text)
.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 = {
text,
from,
to,
};
init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
/**
* 构造翻译接口请求参数
* @param {*}
* @returns
*/
export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
const args = { text, from, to, ...apiSetting };
switch (translator) {
case OPT_TRANS_DEEPL:
case OPT_TRANS_OPENAI:
case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3:
case OPT_TRANS_GEMINI:
case OPT_TRANS_GEMINI_2:
case OPT_TRANS_CLAUDE:
case OPT_TRANS_CLOUDFLAREAI:
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3:
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);
break;
case OPT_TRANS_DEEPLX:
args.url = keyPick(translator, args.url, urlMap);
break;
default:
}
switch (translator) {
case OPT_TRANS_GOOGLE:
return genGoogle(args);
case OPT_TRANS_GOOGLE_2:
return genGoogle2(args);
case OPT_TRANS_MICROSOFT:
return genMicrosoft(args);
case OPT_TRANS_DEEPL:
return genDeepl(args);
case OPT_TRANS_DEEPLFREE:
return genDeeplFree(args);
case OPT_TRANS_DEEPLX:
return genDeeplX(args);
case OPT_TRANS_NIUTRANS:
return genNiuTrans(args);
case OPT_TRANS_BAIDU:
return genBaidu(args);
case OPT_TRANS_TENCENT:
return genTencent(args);
case OPT_TRANS_VOLCENGINE:
return genVolcengine(args);
case OPT_TRANS_OPENAI:
case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3:
return genOpenAI(args);
case OPT_TRANS_GEMINI:
return genGemini(args);
case OPT_TRANS_GEMINI_2:
return genGemini2(args);
case OPT_TRANS_CLAUDE:
return genClaude(args);
case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args);
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3:
return genOllama(args);
case OPT_TRANS_CUSTOMIZE:
case OPT_TRANS_CUSTOMIZE_2:
case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5:
return genCustom(args);
default:
throw new Error(`[trans] translator: ${translator} not support`);
}
};

View File

@@ -1,8 +1,7 @@
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import { import {
MSG_FETCH, MSG_FETCH,
MSG_FETCH_LIMIT, MSG_GET_HTTPCACHE,
MSG_FETCH_CLEAR,
MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE,
MSG_OPEN_OPTIONS, MSG_OPEN_OPTIONS,
MSG_SAVE_RULE, MSG_SAVE_RULE,
@@ -18,10 +17,11 @@ 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";
import { fetchData, fetchPool } from "./libs/fetch"; import { fetchHandle, getHttpCache } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg"; import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules"; import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs"; import { tryClearCaches } from "./libs";
@@ -45,7 +45,7 @@ const REMOVE_HEADERS = [
async function addContextMenus(contextMenuType = 1) { async function addContextMenus(contextMenuType = 1) {
// 添加前先删除,避免重复ID的错误 // 添加前先删除,避免重复ID的错误
try { try {
await browser.contextMenus.removeAll(); await browser.menus.removeAll();
} catch (err) { } catch (err) {
// //
} }
@@ -124,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();
@@ -152,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);
@@ -169,13 +188,10 @@ browser.runtime.onStartup.addListener(async () => {
browser.runtime.onMessage.addListener(async ({ action, args }) => { browser.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) { switch (action) {
case MSG_FETCH: case MSG_FETCH:
const { input, opts } = args; return await fetchHandle(args);
return await fetchData(input, opts); case MSG_GET_HTTPCACHE:
case MSG_FETCH_LIMIT: const { input, init } = args;
const { interval, limit } = args; return await getHttpCache(input, init);
return fetchPool.update(interval, limit);
case MSG_FETCH_CLEAR:
return fetchPool.clear();
case MSG_OPEN_OPTIONS: case MSG_OPEN_OPTIONS:
return await browser.runtime.openOptionsPage(); return await browser.runtime.openOptionsPage();
case MSG_SAVE_RULE: case MSG_SAVE_RULE:

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,13 +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,
}) { darkMode,
if (!tranboxSetting?.transOpen) { uiLang,
langDetector,
},
{ transSelected }
) {
if (transSelected === "false") {
return; return;
} }
@@ -170,6 +175,8 @@ function showTransbox({
contextMenuType={contextMenuType} contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting} tranboxSetting={tranboxSetting}
transApis={transApis} transApis={transApis}
uiLang={uiLang}
langDetector={langDetector}
/> />
</CacheProvider> </CacheProvider>
</React.StrictMode> </React.StrictMode>
@@ -247,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,23 @@ const customApiLangs = `["en", "English - English"],
["vi", "Vietnamese - Tiếng Việt"], ["vi", "Vietnamese - Tiếng Việt"],
`; `;
const customDefaultOption = `{ const hookExample = `// 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]`;
const customApiHelpZH = `// 请求数据默认格式
{
"url": "{{url}}", "url": "{{url}}",
"method": "POST", "method": "POST",
"headers": { "headers": {
@@ -50,18 +66,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 +80,43 @@ ${customDefaultOption}
to: "", // 目标语言(可选) to: "", // 目标语言(可选)
} }
// Hook 范例
${hookExample}
// 支持的语言代码如下 // 支持的语言代码如下
${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
${hookExample}
// The supported language codes are as follows // The supported language codes are as follows
${customApiLangs} ${customApiLangs}
`; `;
@@ -153,6 +186,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 +210,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)`,
@@ -394,8 +447,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 +676,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: `显示`,
@@ -710,12 +763,20 @@ export const I18N = {
en: `Open Translate Popup/Translate Selected Shortcut`, en: `Open Translate Popup/Translate Selected Shortcut`,
}, },
tranbtn_offset_x: { tranbtn_offset_x: {
zh: `翻译按钮偏移X0-100`, zh: `翻译按钮偏移X±200`,
en: `Translate Button Offset X (0-100)`, en: `Translate Button Offset X (±200)`,
}, },
tranbtn_offset_y: { tranbtn_offset_y: {
zh: `翻译按钮偏移Y0-100`, zh: `翻译按钮偏移Y±200`,
en: `Translate Button Offset Y (0-100)`, en: `Translate Button Offset Y (±200)`,
},
tranbox_offset_x: {
zh: `翻译框偏移X±200`,
en: `Translate Box Offset X (±200)`,
},
tranbox_offset_y: {
zh: `翻译框偏移Y±200`,
en: `Translate Box Offset Y (±200)`,
}, },
translated_text: { translated_text: {
zh: `译文`, zh: `译文`,
@@ -790,8 +851,8 @@ export const I18N = {
en: `Secondary Context Menus`, en: `Secondary Context Menus`,
}, },
mulkeys_help: { mulkeys_help: {
zh: `支持用换行或英文逗号“,”分隔多个KEY轮询调用。`, zh: `支持用换行或英文逗号“,”分隔轮询调用。`,
en: `Supports multiple KEY polling calls separated by newlines or English commas ",".`, en: `Supports polling calls separated by newlines or English commas ",".`,
}, },
translation_element_tag: { translation_element_tag: {
zh: `译文元素标签`, zh: `译文元素标签`,
@@ -813,13 +874,17 @@ 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`,
}, },
reg_niutrans: { reg_niutrans: {
zh: `获取小牛翻译密钥`, zh: `获取小牛翻译密钥【简约翻译专属新用户注册赠送300万字符】`,
en: `Get NiuTrans APIKey`, en: `Get NiuTrans APIKey [KISS Translator Exclusive New User Registration Free 3 Million Characters]`,
}, },
trigger_mode: { trigger_mode: {
zh: `触发方式`, zh: `触发方式`,
@@ -845,4 +910,64 @@ export const I18N = {
zh: `自定义选项`, zh: `自定义选项`,
en: `Custom Option`, en: `Custom Option`,
}, },
translate_selected_text: {
zh: `翻译选中文字`,
en: `Translate Selected Text`,
},
toggle_style: {
zh: `切换样式`,
en: `Toggle Style`,
},
open_menu: {
zh: `打开弹窗菜单`,
en: `Open Popup Menu`,
},
open_setting: {
zh: `打开设置`,
en: `Open Setting`,
},
follow_selection: {
zh: `翻译框跟随选中文本`,
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";
@@ -51,8 +57,7 @@ export const KV_SALT_SHARE = "KISS-Translator-SHARE";
export const CACHE_NAME = `${APP_NAME}_cache`; export const CACHE_NAME = `${APP_NAME}_cache`;
export const MSG_FETCH = "fetch"; export const MSG_FETCH = "fetch";
export const MSG_FETCH_LIMIT = "fetch_limit"; export const MSG_GET_HTTPCACHE = "get_httpcache";
export const MSG_FETCH_CLEAR = "fetch_clear";
export const MSG_OPEN_OPTIONS = "open_options"; export const MSG_OPEN_OPTIONS = "open_options";
export const MSG_SAVE_RULE = "save_rule"; export const MSG_SAVE_RULE = "save_rule";
export const MSG_TRANS_TOGGLE = "trans_toggle"; export const MSG_TRANS_TOGGLE = "trans_toggle";
@@ -79,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";
@@ -90,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";
@@ -104,9 +125,17 @@ 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_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_2 = "Ollama2";
export const OPT_TRANS_OLLAMA_3 = "Ollama3";
export const OPT_TRANS_CUSTOMIZE = "Custom"; export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_CUSTOMIZE_2 = "Custom2"; export const OPT_TRANS_CUSTOMIZE_2 = "Custom2";
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3"; export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
@@ -114,16 +143,25 @@ 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,
OPT_TRANS_NIUTRANS, OPT_TRANS_NIUTRANS,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
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_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2, OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3, OPT_TRANS_CUSTOMIZE_3,
@@ -131,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 - 简体中文"],
@@ -173,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", ""],
@@ -193,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"],
]), ]),
@@ -203,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"],
@@ -255,9 +307,30 @@ export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_OPENAI]: new Map( [OPT_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
), ),
[OPT_TRANS_OPENAI_2]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OPENAI_3]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[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_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OLLAMA_2]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OLLAMA_3]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CLOUDFLAREAI]: new Map([ [OPT_TRANS_CLOUDFLAREAI]: new Map([
["auto", ""], ["auto", ""],
["zh-CN", "chinese"], ["zh-CN", "chinese"],
@@ -294,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,
@@ -360,6 +439,7 @@ 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"; // 默认高亮背景色/线条颜色
@@ -388,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: "", // 钩子函数
}; };
// 输入框翻译 // 输入框翻译
@@ -423,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",
@@ -431,11 +515,15 @@ export const DEFAULT_TRANBOX_SETTING = {
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT, tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10, btnOffsetX: 10,
btnOffsetY: 10, btnOffsetY: 10,
boxOffsetX: 0,
boxOffsetY: 10,
hideTranBtn: false, // 是否隐藏翻译按钮 hideTranBtn: false, // 是否隐藏翻译按钮
hideClickAway: false, // 是否点击外部关闭弹窗 hideClickAway: false, // 是否点击外部关闭弹窗
simpleStyle: false, // 是否简洁界面 simpleStyle: false, // 是否简洁界面
followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式 triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
extStyles: "", // 附加样式 extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
}; };
// 订阅列表 // 订阅列表
@@ -454,48 +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 = {
url: "https://api.openai.com/v1/chat/completions",
key: "",
model: "gpt-4",
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,
fetchInterval: 500,
apiName: "",
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
};
const defaultOllamaApi = {
url: "http://localhost:11434/api/generate",
key: "",
model: "llama3.1",
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,
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",
@@ -504,29 +664,67 @@ 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]: { [OPT_TRANS_OPENAI]: defaultOpenaiApi,
url: "https://api.openai.com/v1/chat/completions", [OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
key: "", [OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
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}.`,
fetchLimit: 1,
fetchInterval: 500,
},
[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,
reasoningEffort: "low",
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_2]: defaultOllamaApi,
[OPT_TRANS_OLLAMA_3]: defaultOllamaApi,
[OPT_TRANS_CUSTOMIZE]: defaultCustomApi, [OPT_TRANS_CUSTOMIZE]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi, [OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi, [OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
@@ -566,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, // 是否注入修复补丁(作废)
@@ -587,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

@@ -2,6 +2,14 @@ import { useSetting } from "./Setting";
import { I18N, URL_RAW_PREFIX } from "../config"; import { I18N, URL_RAW_PREFIX } from "../config";
import { useFetch } from "./Fetch"; import { useFetch } from "./Fetch";
export const getI18n = (uiLang, key, defaultText = "") => {
return I18N?.[key]?.[uiLang] ?? defaultText;
};
export const useLangMap = (uiLang) => {
return (key, defaultText = "") => getI18n(uiLang, key, defaultText);
};
/** /**
* 多语言 hook * 多语言 hook
* @returns * @returns
@@ -10,7 +18,7 @@ export const useI18n = () => {
const { const {
setting: { uiLang }, setting: { uiLang },
} = useSetting(); } = useSetting();
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText; return useLangMap(uiLang);
}; };
export const useI18nMd = (key) => { export const useI18nMd = (key) => {

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,6 +1,6 @@
import { getMsauth, setMsauth } from "./storage"; import { getMsauth, setMsauth } from "./storage";
import { URL_MICROSOFT_AUTH } from "../config"; import { URL_MICROSOFT_AUTH } from "../config";
import { fetchData } from "./fetch"; import { fetchHandle } from "./fetch";
import { kissLog } from "./log"; import { kissLog } from "./log";
const parseMSToken = (token) => { const parseMSToken = (token) => {
@@ -35,7 +35,7 @@ const _msAuth = () => {
} }
// 缓存没有或失效,查询接口 // 缓存没有或失效,查询接口
token = await fetchData(URL_MICROSOFT_AUTH); token = await fetchHandle({ input: URL_MICROSOFT_AUTH });
exp = parseMSToken(token); exp = parseMSToken(token);
await setMsauth({ token, exp }); await setMsauth({ token, exp });
return [token, exp]; return [token, exp];

View File

@@ -1,20 +1,38 @@
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_FETCH_LIMIT, MSG_GET_HTTPCACHE,
MSG_FETCH_CLEAR,
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 { newCacheReq, newTransReq } from "./req"; 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
* @param {*} input
* @param {*} init
* @returns
*/
const newCacheReq = async (input, init) => {
let request = new Request(input, init);
if (request.method !== "GET") {
const body = await request.text();
const cacheUrl = new URL(request.url);
cacheUrl.pathname += body;
request = new Request(cacheUrl.toString(), { method: "GET" });
}
return request;
};
/** /**
* 油猴脚本的请求封装 * 油猴脚本的请求封装
@@ -22,7 +40,10 @@ const TIMEOUT = 5000;
* @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,
@@ -30,8 +51,8 @@ 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, ...opts }) => { onload: ({ response, responseHeaders, status, statusText }) => {
const headers = {}; const headers = {};
responseHeaders.split("\n").forEach((line) => { responseHeaders.split("\n").forEach((line) => {
const [name, value] = line.split(":").map((item) => item.trim()); const [name, value] = line.split(":").map((item) => item.trim());
@@ -55,112 +76,82 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const fetchApi = async ({ input, init, transOpts, apiSetting }) => { export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
if (transOpts?.translator) { if (transOpts?.translator) {
[input, init] = await newTransReq(transOpts, apiSetting); [input, init] = await genTransReq(transOpts, apiSetting);
} }
if (!input) { if (!input) {
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);
}; };
/** /**
* 请求池实例 * 解析 response
*/ * @param {*} res
export const fetchPool = taskPool(
fetchApi,
null,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT
);
/**
* 请求数据统一接口
* @param {*} input
* @param {*} opts
* @returns * @returns
*/ */
export const fetchData = async ( const parseResponse = async (res) => {
input,
{ useCache, usePool, transOpts, apiSetting, ...init } = {}
) => {
const cacheReq = await newCacheReq(input, init);
let res;
// 查询缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
res = await cache.match(cacheReq);
} catch (err) {
kissLog(err, "cache match");
}
}
if (!res) { if (!res) {
// 发送请求 return null;
if (usePool) {
res = await fetchPool.push({ input, init, transOpts, apiSetting });
} else {
res = await fetchApi({ input, init, transOpts, apiSetting });
}
if (!res) {
throw new Error("Unknow error");
} else if (!res.ok) {
const msg = {
url: res.url,
status: res.status,
};
if (res.headers.get("Content-Type")?.includes("json")) {
msg.response = await res.json();
}
throw new Error(JSON.stringify(msg));
}
// 插入缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
await cache.put(cacheReq, res.clone());
} catch (err) {
kissLog(err, "cache put");
}
}
} }
const contentType = res.headers.get("Content-Type"); const contentType = res.headers.get("Content-Type");
@@ -174,23 +165,141 @@ export const fetchData = async (
}; };
/** /**
* fetch 兼容性封装 * 查询 caches
* @param {*} input * @param {*} input
* @param {*} opts * @param {*} param1
* @returns * @returns
*/ */
export const fetchPolyfill = async (input, opts) => { export const getHttpCache = async (input, { method, headers, body }) => {
try {
const req = await newCacheReq(input, { method, headers, body });
const cache = await caches.open(CACHE_NAME);
const res = await cache.match(req);
return parseResponse(res);
} catch (err) {
kissLog(err, "get cache");
}
return null;
};
/**
* 插入 caches
* @param {*} input
* @param {*} param1
* @param {*} res
*/
export const putHttpCache = async (input, { method, headers, body }, res) => {
try {
const req = await newCacheReq(input, { method, headers, body });
const cache = await caches.open(CACHE_NAME);
await cache.put(req, res);
} catch (err) {
kissLog(err, "put cache");
}
};
/**
* 处理请求
* @param {*} param0
* @returns
*/
export const fetchHandle = async ({
input,
useCache,
transOpts,
apiSetting,
...init
}) => {
// 发送请求
const res = await fetchPatcher(input, init, transOpts, apiSetting);
if (!res) {
throw new Error("Unknow error");
} else if (!res.ok) {
const msg = {
url: res.url,
status: res.status,
};
if (res.headers.get("Content-Type")?.includes("json")) {
msg.response = await res.json();
}
throw new Error(JSON.stringify(msg));
}
// 插入缓存
if (useCache) {
await putHttpCache(input, init, res.clone());
}
return parseResponse(res);
};
/**
* fetch 兼容性封装
* @param {*} args
* @returns
*/
export const fetchPolyfill = (args) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_FETCH, args);
}
// 油猴/网页/BackgroundPage
return fetchHandle(args);
};
/**
* getHttpCache 兼容性封装
* @param {*} input
* @param {*} init
* @returns
*/
export const getHttpCachePolyfill = (input, init) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_GET_HTTPCACHE, { input, init });
}
// 油猴/网页/BackgroundPage
return getHttpCache(input, init);
};
/**
* 请求池实例
*/
export const fetchPool = taskPool(
fetchPolyfill,
null,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT
);
/**
* 数据请求
* @param {*} input
* @param {*} param1
* @returns
*/
export const fetchData = async (input, { useCache, usePool, ...args } = {}) => {
if (!input?.trim()) { if (!input?.trim()) {
throw new Error("URL is empty"); throw new Error("URL is empty");
} }
// 插件 // 查询缓存
if (isExt && !isBg()) { if (useCache) {
return await sendBgMsg(MSG_FETCH, { input, opts }); const cache = await getHttpCachePolyfill(input, args);
if (cache) {
return cache;
}
} }
// 油猴/网页/BackgroundPage // 通过任务池发送请求
return await fetchData(input, opts); if (usePool) {
return fetchPool.push({ input, useCache, ...args });
}
// 直接请求
return fetchPolyfill({ input, useCache, ...args });
}; };
/** /**
@@ -198,21 +307,13 @@ export const fetchPolyfill = async (input, opts) => {
* @param {*} interval * @param {*} interval
* @param {*} limit * @param {*} limit
*/ */
export const updateFetchPool = async (interval, limit) => { export const updateFetchPool = (interval, limit) => {
if (isExt) { fetchPool.update(interval, limit);
await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
} else {
fetchPool.update(interval, limit);
}
}; };
/** /**
* 清空任务池 * 清空任务池
*/ */
export const clearFetchPool = async () => { export const clearFetchPool = () => {
if (isExt) { fetchPool.clear();
await sendBgMsg(MSG_FETCH_CLEAR);
} else {
fetchPool.clear();
}
}; };

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

@@ -1,376 +0,0 @@
import queryString from "query-string";
import {
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART,
INPUT_PLACE_URL,
INPUT_PLACE_FROM,
INPUT_PLACE_TO,
INPUT_PLACE_TEXT,
INPUT_PLACE_KEY,
} from "../config";
import { msAuth } from "./auth";
import { genDeeplFree } from "../apis/deepl";
import { genBaidu } from "../apis/baidu";
const keyMap = new Map();
// 轮询key
const keyPick = (translator, key = "") => {
const keys = key
.split(/\n|,/)
.map((item) => item.trim())
.filter(Boolean);
if (keys.length === 0) {
return "";
}
const preIndex = keyMap.get(translator) ?? -1;
const curIndex = (preIndex + 1) % keys.length;
keyMap.set(translator, curIndex);
return keys[curIndex];
};
/**
* 构造缓存 request
* @param {*} request
* @returns
*/
export const newCacheReq = async (input, init) => {
let request = new Request(input, init);
if (request.method !== "GET") {
const body = await request.text();
const cacheUrl = new URL(request.url);
cacheUrl.pathname += body;
request = new Request(cacheUrl.toString(), { method: "GET" });
}
return request;
};
const genGoogle = ({ text, from, to, url, key }) => {
const params = {
client: "gtx",
dt: "t",
dj: 1,
ie: "UTF-8",
sl: from,
tl: to,
q: text,
};
const input = `${url}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
},
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [input, init];
};
const genMicrosoft = async ({ text, from, to }) => {
const [token] = await msAuth();
const params = {
from,
to,
"api-version": "3.0",
};
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
};
return [input, init];
};
const genDeepl = ({ text, from, to, url, key }) => {
const data = {
text: [text],
target_lang: to,
source_lang: from,
// split_sentences: "0",
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `DeepL-Auth-Key ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genDeeplX = ({ text, from, to, url, key }) => {
const data = {
text,
target_lang: to,
source_lang: from,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
const data = {
from,
to,
apikey: key,
src_text: text,
dictNo,
memoryNo,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genTencent = ({ text, from, to }) => {
const data = {
header: {
fn: "auto_translation_block",
},
source: {
text_block: text,
lang: from,
},
target: {
lang: to,
},
};
const init = {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [URL_TENCENT_TRANSMART, init];
};
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to);
const data = {
model,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`, // OpenAI
"api-key": key, // Azure OpenAI
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genGemini = ({ text, from, to, url, key, prompt, model }) => {
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
contents: [
{
// role: "user",
parts: [
{
text: prompt,
},
],
},
],
};
const input = `${url}/${model}:generateContent?key=${key}`;
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [input, init];
};
const genCloudflareAI = ({ text, from, to, url, key }) => {
const data = {
text,
source_lang: from,
target_lang: to,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genCustom = ({ text, from, to, url, key, customOption }) => {
const replaceInput = (str) =>
str
.replaceAll(INPUT_PLACE_URL, url)
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text.replaceAll(`"`, `\n`))
.replaceAll(INPUT_PLACE_KEY, key);
const data = {
text,
from,
to,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (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];
};
/**
* 构造翻译接口 request
* @param {*}
* @returns
*/
export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
const args = { text, from, to, ...apiSetting };
switch (translator) {
case OPT_TRANS_DEEPL:
case OPT_TRANS_OPENAI:
case OPT_TRANS_GEMINI:
case OPT_TRANS_CLOUDFLAREAI:
case OPT_TRANS_NIUTRANS:
args.key = keyPick(translator, args.key);
break;
default:
}
switch (translator) {
case OPT_TRANS_GOOGLE:
return genGoogle(args);
case OPT_TRANS_MICROSOFT:
return genMicrosoft(args);
case OPT_TRANS_DEEPL:
return genDeepl(args);
case OPT_TRANS_DEEPLFREE:
return genDeeplFree(args);
case OPT_TRANS_DEEPLX:
return genDeeplX(args);
case OPT_TRANS_NIUTRANS:
return genNiuTrans(args);
case OPT_TRANS_BAIDU:
return genBaidu(args);
case OPT_TRANS_TENCENT:
return genTencent(args);
case OPT_TRANS_OPENAI:
return genOpenAI(args);
case OPT_TRANS_GEMINI:
return genGemini(args);
case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args);
case OPT_TRANS_CUSTOMIZE:
case OPT_TRANS_CUSTOMIZE_2:
case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5:
return genCustom(args);
default:
throw new Error(`[trans] translator: ${translator} not support`);
}
};

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

@@ -20,13 +20,14 @@ import {
import { apiSyncData } from "../apis"; import { apiSyncData } from "../apis";
import { sha256, removeEndchar } from "./utils"; import { sha256, removeEndchar } from "./utils";
import { createClient, getPatcher } from "webdav"; import { createClient, getPatcher } from "webdav";
import { fetchApi } from "./fetch"; import { fetchPatcher } from "./fetch";
import { kissLog } from "./log"; import { kissLog } from "./log";
getPatcher().patch("request", (opts) => { getPatcher().patch("request", (opts) => {
return fetchApi({ return fetchPatcher(opts.url, {
input: opts.url, method: opts.method,
init: { method: opts.method, headers: opts.headers, body: opts.data }, headers: opts.headers,
body: opts.data,
}); });
}); });

View File

@@ -25,6 +25,7 @@ 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);
}
} }
}); });
@@ -490,23 +497,26 @@ export class Translator {
} }
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 +548,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

View File

@@ -22,6 +22,7 @@ import {
import { shortcutRegister } from "../../libs/shortcut"; import { shortcutRegister } from "../../libs/shortcut";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { kissLog } from "../../libs/log"; import { kissLog } from "../../libs/log";
import { getI18n } from "../../hooks/I18n";
export default function Action({ translator, fab }) { export default function Action({ translator, fab }) {
const fabWidth = 40; const fabWidth = 40;
@@ -96,11 +97,11 @@ export default function Action({ translator, fab }) {
// 注册菜单 // 注册菜单
try { try {
const menuCommandIds = []; const menuCommandIds = [];
const { contextMenuType } = translator.setting; const { contextMenuType, uiLang } = translator.setting;
contextMenuType !== 0 && contextMenuType !== 0 &&
menuCommandIds.push( menuCommandIds.push(
GM.registerMenuCommand( GM.registerMenuCommand(
"Toggle Translate", getI18n(uiLang, "translate_switch"),
(event) => { (event) => {
translator.toggle(); translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE); sendIframeMsg(MSG_TRANS_TOGGLE);
@@ -109,7 +110,7 @@ export default function Action({ translator, fab }) {
"Q" "Q"
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
"Toggle Style", getI18n(uiLang, "toggle_style"),
(event) => { (event) => {
translator.toggleStyle(); translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE); sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
@@ -118,14 +119,14 @@ export default function Action({ translator, fab }) {
"C" "C"
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
"Open Menu", getI18n(uiLang, "open_menu"),
(event) => { (event) => {
setShowPopup((pre) => !pre); setShowPopup((pre) => !pre);
}, },
"K" "K"
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
"Open Setting", getI18n(uiLang, "open_setting"),
(event) => { (event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"); window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}, },

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,22 +2,39 @@ 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,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLX,
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_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_2,
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_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";
@@ -32,7 +49,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();
@@ -50,7 +67,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) {
@@ -108,12 +125,22 @@ function ApiFields({ translator }) {
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,
reasoningEffort = "low",
} = api; } = api;
const handleChange = (e) => { const handleChange = (e) => {
@@ -125,6 +152,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({
@@ -132,19 +168,32 @@ function ApiFields({ translator }) {
}); });
}; };
const buildinTranslators = [ const builtinTranslators = [
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
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 = [
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
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_2,
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 =
@@ -163,7 +212,15 @@ function ApiFields({ translator }) {
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
{!buildinTranslators.includes(translator) && ( <TextField
size="small"
label={i18n("api_name")}
name="apiName"
value={apiName}
onChange={handleChange}
/>
{!builtinTranslators.includes(translator) && (
<> <>
<TextField <TextField
size="small" size="small"
@@ -171,6 +228,11 @@ function ApiFields({ translator }) {
name="url" name="url"
value={url} value={url}
onChange={handleChange} onChange={handleChange}
multiline={translator === OPT_TRANS_DEEPLX}
maxRows={10}
helperText={
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
}
/> />
<TextField <TextField
size="small" size="small"
@@ -179,12 +241,16 @@ function ApiFields({ translator }) {
value={key} value={key}
onChange={handleChange} onChange={handleChange}
multiline={mulkeysTranslators.includes(translator)} multiline={mulkeysTranslators.includes(translator)}
maxRows={10}
helperText={keyHelper} helperText={keyHelper}
/> />
</> </>
)} )}
{(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && ( {(translator.startsWith(OPT_TRANS_OPENAI) ||
translator.startsWith(OPT_TRANS_OLLAMA) ||
translator === OPT_TRANS_CLAUDE ||
translator.startsWith(OPT_TRANS_GEMINI)) && (
<> <>
<TextField <TextField
size="small" size="small"
@@ -195,12 +261,94 @@ 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}
/> />
<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) && (
<>
<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}
/>
</>
)}
{translator === OPT_TRANS_GEMINI_2 && (
<>
<TextField
size="small"
label="Temperature"
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
<TextField
select
size="small"
name="reasoningEffort"
value={reasoningEffort}
label="Reasoning Effort"
onChange={handleChange}
>
<MenuItem value={"none"}>none</MenuItem>
<MenuItem value={"low"}>low</MenuItem>
<MenuItem value={"medium"}>medium</MenuItem>
<MenuItem value={"high"}>high</MenuItem>
</TextField>
</> </>
)} )}
@@ -224,14 +372,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}
/> multiline
maxRows={10}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)} )}
<TextField <TextField
@@ -252,6 +412,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

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

@@ -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";
@@ -27,10 +26,10 @@ export default function Tranbox() {
let { name, value } = e.target; let { name, value } = e.target;
switch (name) { switch (name) {
case "btnOffsetX": case "btnOffsetX":
value = limitNumber(value, 0, 100);
break;
case "btnOffsetY": case "btnOffsetY":
value = limitNumber(value, 0, 100); case "boxOffsetX":
case "boxOffsetY":
value = limitNumber(value, -200, 200);
break; break;
default: default:
} }
@@ -47,7 +46,6 @@ export default function Tranbox() {
); );
const { const {
transOpen,
translator, translator,
fromLang, fromLang,
toLang, toLang,
@@ -55,30 +53,20 @@ export default function Tranbox() {
tranboxShortcut, tranboxShortcut,
btnOffsetX, btnOffsetX,
btnOffsetY, btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
hideTranBtn = false, hideTranBtn = false,
hideClickAway = false, hideClickAway = false,
simpleStyle = false, simpleStyle = 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"
@@ -140,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")}
@@ -158,6 +158,24 @@ export default function Tranbox() {
onChange={handleChange} onChange={handleChange}
/> />
<TextField
size="small"
label={i18n("tranbox_offset_x")}
type="number"
name="boxOffsetX"
defaultValue={boxOffsetX}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("tranbox_offset_y")}
type="number"
name="boxOffsetY"
defaultValue={boxOffsetY}
onChange={handleChange}
/>
<TextField <TextField
select select
size="small" size="small"
@@ -194,6 +212,18 @@ export default function Tranbox() {
<MenuItem value={true}>{i18n("enable")}</MenuItem> <MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField> </TextField>
<TextField
select
size="small"
name="followSelection"
value={followSelection}
label={i18n("follow_selection")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
<TextField <TextField
select select
size="small" size="small"

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

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useState } from "react";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
@@ -130,11 +130,11 @@ function Pointer({
export default function DraggableResizable({ export default function DraggableResizable({
header, header,
children, children,
defaultPosition = { position = {
x: 0, x: 0,
y: 0, y: 0,
}, },
defaultSize = { size = {
w: 600, w: 600,
h: 400, h: 400,
}, },
@@ -146,14 +146,13 @@ export default function DraggableResizable({
w: 1200, w: 1200,
h: 1200, h: 1200,
}, },
setSize,
setPosition,
onChangeSize, onChangeSize,
onChangePosition, onChangePosition,
...props ...props
}) { }) {
const lineWidth = 4; const lineWidth = 4;
const [position, setPosition] = useState(defaultPosition);
const [size, setSize] = useState(defaultSize);
const opts = { const opts = {
size, size,
setSize, setSize,
@@ -163,14 +162,6 @@ export default function DraggableResizable({
maxSize, maxSize,
}; };
useEffect(() => {
onChangeSize && onChangeSize(size);
}, [size, onChangeSize]);
useEffect(() => {
onChangePosition && onChangePosition(position);
}, [position, onChangePosition]);
return ( return (
<Box <Box
className="KT-draggable" className="KT-draggable"

View File

@@ -14,15 +14,23 @@ import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import PushPinIcon from "@mui/icons-material/PushPin"; import PushPinIcon from "@mui/icons-material/PushPin";
import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
import LockIcon from "@mui/icons-material/Lock";
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";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import { isMobile } from "../../libs/mobile";
function Header({ function Header({
setShowPopup, setShowPopup,
@@ -30,9 +38,20 @@ function Header({
setSimpleStyle, setSimpleStyle,
hideClickAway, hideClickAway,
setHideClickAway, setHideClickAway,
followSelection,
setFollowSelection,
mouseHover,
}) { }) {
if (!isMobile && simpleStyle && !mouseHover) {
return;
}
return ( return (
<Box className="KT-transbox-header"> <Box
className="KT-transbox-header"
onMouseUp={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack direction="row" justifyContent="space-between" alignItems="center">
<DragIndicatorIcon fontSize="small" /> <DragIndicatorIcon fontSize="small" />
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
@@ -43,6 +62,18 @@ function Header({
}} }}
> >
{hideClickAway ? ( {hideClickAway ? (
<LockOpenIcon fontSize="small" />
) : (
<LockIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setFollowSelection((pre) => !pre);
}}
>
{followSelection ? (
<PushPinOutlinedIcon fontSize="small" /> <PushPinOutlinedIcon fontSize="small" />
) : ( ) : (
<PushPinIcon fontSize="small" /> <PushPinIcon fontSize="small" />
@@ -75,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);
@@ -85,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"
@@ -148,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>
@@ -207,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}
@@ -216,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>
); );
} }
@@ -239,14 +300,21 @@ export default function TranBox({
setSimpleStyle, setSimpleStyle,
hideClickAway, hideClickAway,
setHideClickAway, setHideClickAway,
followSelection,
setFollowSelection,
extStyles, extStyles,
langDetector,
enDict,
}) { }) {
const [mouseHover, setMouseHover] = useState(false);
return ( return (
<SettingProvider> <SettingProvider>
<ThemeProvider styles={extStyles}> <ThemeProvider styles={extStyles}>
<DraggableResizable <DraggableResizable
defaultPosition={boxPosition} position={boxPosition}
defaultSize={boxSize} size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
header={ header={
<Header <Header
setShowPopup={setShowBox} setShowPopup={setShowBox}
@@ -254,11 +322,14 @@ export default function TranBox({
setSimpleStyle={setSimpleStyle} setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway} hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway} setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
mouseHover={mouseHover}
/> />
} }
onChangeSize={setBoxSize}
onChangePosition={setBoxPosition}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)}
> >
<TranForm <TranForm
text={text} text={text}
@@ -266,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,18 +5,11 @@ export default function TranBtn({
onTrigger, onTrigger,
btnEvent, btnEvent,
position, position,
tranboxSetting, btnOffsetX,
btnOffsetY,
}) { }) {
const left = limitNumber( const left = limitNumber(position.x + btnOffsetX, 0, window.innerWidth - 32);
position.x + tranboxSetting.btnOffsetX, const top = limitNumber(position.y + btnOffsetY, 0, window.innerHeight - 32);
0,
window.innerWidth - 32
);
const top = limitNumber(
position.y + tranboxSetting.btnOffsetY,
0,
window.innerHeight - 32
);
return ( return (
<div <div

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,22 +10,32 @@ 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";
import { useLangMap } from "../../hooks/I18n";
export default function Slection({ export default function Slection({
contextMenuType, contextMenuType,
tranboxSetting, tranboxSetting,
transApis, transApis,
uiLang,
langDetector,
}) { }) {
const { const {
hideTranBtn = false, hideTranBtn = false,
simpleStyle: initSimpleStyle = false, simpleStyle: initSimpleStyle = false,
hideClickAway: initHideClickAway = false, hideClickAway: initHideClickAway = false,
followSelection: initFollowMouse = false,
tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT, tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK, triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles, extStyles,
btnOffsetX,
btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
enDict = OPT_DICT_BAIDU,
} = tranboxSetting; } = tranboxSetting;
const boxWidth = const boxWidth =
@@ -37,6 +47,7 @@ export default function Slection({
? 200 ? 200
: limitNumber(window.innerHeight, 200, 400); : limitNumber(window.innerHeight, 200, 400);
const langMap = useLangMap(uiLang);
const [showBox, setShowBox] = useState(false); const [showBox, setShowBox] = useState(false);
const [showBtn, setShowBtn] = useState(false); const [showBtn, setShowBtn] = useState(false);
const [selectedText, setSelText] = useState(""); const [selectedText, setSelText] = useState("");
@@ -52,6 +63,7 @@ export default function Slection({
}); });
const [simpleStyle, setSimpleStyle] = useState(initSimpleStyle); const [simpleStyle, setSimpleStyle] = useState(initSimpleStyle);
const [hideClickAway, setHideClickAway] = useState(initHideClickAway); const [hideClickAway, setHideClickAway] = useState(initHideClickAway);
const [followSelection, setFollowSelection] = useState(initFollowMouse);
const handleTrigger = useCallback( const handleTrigger = useCallback(
(text) => { (text) => {
@@ -65,16 +77,27 @@ export default function Slection({
const handleTranbox = useCallback(() => { const handleTranbox = useCallback(() => {
setShowBtn(false); setShowBtn(false);
const selectedText = window.getSelection()?.toString()?.trim() || ""; const selection = window.getSelection();
const selectedText = selection?.toString()?.trim() || "";
if (!selectedText) { if (!selectedText) {
setShowBox((pre) => !pre); setShowBox((pre) => !pre);
return; return;
} }
const rect = selection?.getRangeAt(0)?.getBoundingClientRect();
if (rect && followSelection) {
const x = (rect.left + rect.right) / 2 + boxOffsetX;
const y = rect.bottom + boxOffsetY;
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth - 300),
y: limitNumber(y, 0, window.innerHeight - 200),
});
}
setSelText(selectedText); setSelText(selectedText);
setText(selectedText); setText(selectedText);
setShowBox(true); setShowBox(true);
}, []); }, [followSelection, boxOffsetX, boxOffsetY]);
const btnEvent = useMemo(() => { const btnEvent = useMemo(() => {
if (isMobile) { if (isMobile) {
@@ -90,13 +113,24 @@ export default function Slection({
e.stopPropagation(); e.stopPropagation();
await sleep(200); await sleep(200);
const selectedText = window.getSelection()?.toString()?.trim() || ""; const selection = window.getSelection();
const selectedText = selection?.toString()?.trim() || "";
setSelText(selectedText); setSelText(selectedText);
if (!selectedText) { if (!selectedText) {
setShowBtn(false); setShowBtn(false);
return; return;
} }
const rect = selection?.getRangeAt(0)?.getBoundingClientRect();
if (rect && followSelection) {
const x = (rect.left + rect.right) / 2 + boxOffsetX;
const y = rect.bottom + boxOffsetY;
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth - 300),
y: limitNumber(y, 0, window.innerHeight - 200),
});
}
if (triggerMode === OPT_TRANBOX_TRIGGER_SELECT) { if (triggerMode === OPT_TRANBOX_TRIGGER_SELECT) {
handleTrigger(selectedText); handleTrigger(selectedText);
return; return;
@@ -116,7 +150,14 @@ export default function Slection({
handleMouseup handleMouseup
); );
}; };
}, [hideTranBtn, triggerMode, handleTrigger]); }, [
hideTranBtn,
triggerMode,
followSelection,
boxOffsetX,
boxOffsetY,
handleTrigger,
]);
useEffect(() => { useEffect(() => {
if (isExt) { if (isExt) {
@@ -146,7 +187,7 @@ export default function Slection({
contextMenuType !== 0 && contextMenuType !== 0 &&
menuCommandIds.push( menuCommandIds.push(
GM.registerMenuCommand( GM.registerMenuCommand(
"Translate Selected Text", langMap("translate_selected_text"),
(event) => { (event) => {
handleTranbox(); handleTranbox();
}, },
@@ -162,7 +203,7 @@ export default function Slection({
} catch (err) { } catch (err) {
kissLog(err, "registerMenuCommand"); kissLog(err, "registerMenuCommand");
} }
}, [handleTranbox, contextMenuType]); }, [handleTranbox, contextMenuType, langMap]);
useEffect(() => { useEffect(() => {
if (hideClickAway) { if (hideClickAway) {
@@ -193,14 +234,19 @@ export default function Slection({
setSimpleStyle={setSimpleStyle} setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway} hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway} setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
extStyles={extStyles} extStyles={extStyles}
langDetector={langDetector}
enDict={enDict}
/> />
)} )}
{showBtn && ( {showBtn && (
<TranBtn <TranBtn
position={position} position={position}
tranboxSetting={tranboxSetting} btnOffsetX={btnOffsetX}
btnOffsetY={btnOffsetY}
btnEvent={btnEvent} btnEvent={btnEvent}
onTrigger={(e) => { onTrigger={(e) => {
e.stopPropagation(); e.stopPropagation();