Compare commits

..

137 Commits

Author SHA1 Message Date
Gabe
2a5e9db079 v1.9.1 2025-07-23 21:03:02 +08:00
Gabe
650d6e8b41 fix: workflows 2025-07-23 20:58:50 +08:00
Gabe
1daf134b31 fix: gemini api 2025-07-23 20:03:54 +08:00
Gabe
e1dfa35c6c v1.9.0 2025-07-23 00:38:33 +08:00
Gabe
73f80692d3 feat: add format script (prettier) 2025-07-03 19:08:33 +08:00
Gabe
42c7dae495 fix: log 2025-07-03 18:08:49 +08:00
Gabe
b2a1309caa feat: add gemini2 api 2025-07-02 21:54:18 +08:00
Gabe
94bf5f9580 fix: gemini api 2025-07-02 16:37:57 +08:00
Gabe
704ebdc9d7 fix: variable name 2025-07-02 13:38:30 +08:00
Gabe
165da4e559 fix: rule page: show more button 2025-07-01 23:34:16 +08:00
Gabe
d3e3b484bf fix: remove excess code 2025-07-01 22:47:04 +08:00
Gabe
192f8faa5b feat: support keypick for customize API 2025-07-01 22:42:57 +08:00
Gabe
579d5cb0a3 fix: update userscript 2025-07-01 17:55:57 +08:00
Gabe
07fca5b9af fix: #209 2025-07-01 17:03:52 +08:00
Gabe
866a63ab6c feat: move selected translation switch from setting to rule 2025-07-01 16:44:46 +08:00
Gabe
97b4935bc4 feat: api fetch timeout 2025-07-01 12:38:06 +08:00
Gabe
30129abef3 feat: custom API name 2025-07-01 10:54:30 +08:00
Gabe
24d904b32c feat: support volcengine api 2025-06-30 21:34:37 +08:00
Gabe
5f0ce57ead feat: qq transmart 2025-06-27 20:03:58 +08:00
Gabe
51f58d095a feat: add new google translate api (issue: 225, by: Bush2021) 2025-06-27 19:29:00 +08:00
Gabe
d22e3838c4 refactor: deepModels -> tinkIgnore 2025-06-27 16:33:30 +08:00
Gabe
adbb421b7b refactor: fetch timeout 2025-06-27 12:31:32 +08:00
Gabe
eaa47af269 fix: revert old google translate api 2025-06-26 11:13:51 +08:00
Gabe
a6cb5544f8 fix: browser.menus -> browser.contextMenus 2025-06-26 10:01:43 +08:00
Gabe
9e91faa660 Merge pull request #240 from unclemcz/dev
feat:ollama接口设置新增是否禁用深度思考参数
2025-06-25 20:42:31 +08:00
mcz
8636fadc72 接口设置ollama新增是否禁用深度思考参数 2025-06-03 23:07:10 +08:00
Gabe
0621957592 chore: thunderbird 2025-05-18 00:34:20 +08:00
Gabe
8ec06b0c84 chore: defined messenger in package.json 2025-05-18 00:29:06 +08:00
Gabe
d47f8d7ee9 Merge remote-tracking branch 'origin/dev' into dev 2025-05-17 23:22:09 +08:00
Gabe
24f8959525 fix: Ignore html comment elements 2025-05-17 23:19:38 +08:00
Gabe
983740578b Merge pull request #235 from unclemcz/dev
feat:基本设置增加请求超时参数&ollama接口配置增加<think>块忽略参数
2025-05-09 14:09:18 +08:00
Gabe
b5f79ed7cd Merge pull request #232 from Bush2021/fix-workflow
build: fix build errors caused by deprecated actions
2025-05-09 14:08:47 +08:00
Gabe
bbb0e79d4e Merge pull request #231 from Bush2021/fix-google-translate
fix: update API for Google Translate
2025-05-09 14:08:16 +08:00
mcz
471dc05897 ollama接口设置增加<think>块忽略参数 2025-05-01 23:41:08 +08:00
mcz
7a772d2459 在基本设置页面增加接口请求超时时间设置 2025-05-01 20:04:58 +08:00
mcz
1d92421960 Remove the <think></think> tags in qwen3 too. 2025-04-29 19:49:14 +08:00
Bush2021
aeaaf429d7 build: fix build errors caused by deprecated actions 2025-04-16 18:45:41 -04:00
Bush2021
84432e98ae fix: update API for Google Translate 2025-04-16 18:00:54 -04:00
Gabe
77c6102de7 Merge pull request #219 from unclemcz/dev
fix:(Ollama)Remove the <think></think> tags in deepseek-r1.
2025-03-12 17:18:26 +08:00
mcz
ab5dd82169 When using the deepseek-r1 model in Ollama, remove the content between the <think></think> tags. 2025-02-23 11:54:48 +08:00
Gabe
23e7b69dc5 Merge pull request #213 from htyxyt/master
采用更好的方式支持thunderbird
2025-02-19 18:09:01 +08:00
htyxyt
3dc8f393f2 Update package.json 2025-02-18 12:56:29 +08:00
htyxyt
8a2144f263 Update background.js 2025-02-18 12:44:06 +08:00
htyxyt
c1c59caa10 Update package.json 2025-02-18 12:29:43 +08:00
htyxyt
d27ebd90b6 Update package.json 2025-02-18 12:14:02 +08:00
htyxyt
467745c1e9 Update package.json 2025-02-18 11:58:39 +08:00
htyxyt
537378a038 Update background.js 2025-02-18 11:45:57 +08:00
htyxyt
4b5ed30e5b Delete src/background.thunderbird.js 2025-02-18 11:42:57 +08:00
htyxyt
52b7f6a225 Update background.js 2025-02-18 11:42:47 +08:00
htyxyt
f31675d8a2 Update index.js 2025-02-18 11:42:05 +08:00
htyxyt
dd46a8450c Update config-overrides.js 2025-02-18 11:41:28 +08:00
htyxyt
b0843f7d66 Update package.json 2025-02-18 11:39:58 +08:00
htyxyt
daadc0195c Update background.thunderbird.js 2025-02-18 10:25:12 +08:00
Gabe
298dec6957 Merge pull request #210 from htyxyt/master
添加对Thunderbird的支持
2025-02-17 23:48:40 +08:00
Gabe
bf39d85dfa Merge pull request #212 from qonmnop/patch-1
允许用户自定义 hooks 中重写signal 参数,自定义超时时间
2025-02-17 23:30:19 +08:00
htyxyt
30a9de25a8 Update config-overrides.js 2025-02-17 14:01:17 +08:00
htyxyt
af1ecf0bd4 Update package.json 2025-02-17 14:00:35 +08:00
qonmnop
fe55a2cd3c 允许用户自定义 hooks 中重写signal 参数,自定义超时时间 2025-02-17 12:14:10 +08:00
htyxyt
5a33d4e57e Add files via upload 2025-02-17 11:57:40 +08:00
htyxyt
7f46a9023c Delete public/background.thunderbird.js 2025-02-17 11:57:12 +08:00
htyxyt
dfd943b621 Rename manifest.thunderfird.json to manifest.thunderbird.json 2025-02-17 11:10:29 +08:00
htyxyt
7007d0d922 Update package.json 2025-02-17 10:39:08 +08:00
htyxyt
601678500d Update package.json 2025-02-14 15:23:03 +08:00
htyxyt
9bfb504381 Add files via upload 2025-02-14 15:13:40 +08:00
htyxyt
8a03b0cf15 Delete src/background.thunderfird.js 2025-02-14 15:12:57 +08:00
htyxyt
11ba89de0a Update manifest.thunderfird.json 2025-02-14 14:57:38 +08:00
htyxyt
bac7f62eea Update background.thunderfird.js 2025-02-14 14:56:58 +08:00
htyxyt
eef90ea02b Delete src/popup.thunderfird.js 2025-02-14 14:56:10 +08:00
htyxyt
0650df534a Delete src/content.thunderfird.js 2025-02-14 14:56:00 +08:00
htyxyt
9ef8c8b823 Delete src/options.thunderfird.js 2025-02-14 14:55:49 +08:00
htyxyt
d7e08da0b2 Add files via upload 2025-02-13 14:53:06 +08:00
htyxyt
ef361e0798 Add files via upload 2025-02-13 14:50:33 +08:00
Gabe
6855332092 fix: modify systemPrompt & userPrompt 2024-11-30 00:41:29 +08:00
Gabe
121d523e02 Merge remote-tracking branch 'origin/dev' into dev 2024-11-30 00:09:48 +08:00
Gabe
42a375c4c7 Merge pull request #188 from unclemcz/dev
给ollama增加system message
2024-11-30 00:07:14 +08:00
Gabe
a1dd705d97 fix: update pnpm-lock file 2024-11-29 21:14:30 +08:00
mcz
71f90b36ca 给ollama增加system message 2024-09-30 16:41:58 +08:00
Gabe
37facdc3c1 Merge pull request #186 from hoilc/dev
feat: enhance openai prompt
2024-09-26 23:38:34 +08:00
hoilc
66b4f547ff feat: enhance openai prompt 2024-09-25 14:03:12 +08:00
Gabe
d27b9c7f2d Merge pull request #185 from hoilc/dev
feat: support claude api
2024-09-24 23:30:00 +08:00
hoilc
278ff9c6bc feat: support claude api 2024-09-23 18:22:19 +08:00
Gabe Yuan
d6fe1ce9d7 fix: try detect language only when fromLang is auto 2024-05-30 21:05:05 +08:00
Gabe Yuan
0bfa5256b8 fix: simplify keepSelector logic 2024-05-30 17:18:39 +08:00
Gabe Yuan
72ccfc8aec v1.8.11 2024-05-23 20:06:48 +08:00
Gabe Yuan
d117c5dc10 feat: baidu dict can be disabled 2024-05-23 00:08:10 +08:00
Gabe Yuan
9312783f44 feat: lang detector can be selected 2024-05-22 23:33:30 +08:00
Gabe Yuan
e5b16ebfd3 Merge remote-tracking branch 'origin/master' into dev 2024-05-22 10:19:04 +08:00
Gabe Yuan
5d1d65c2d3 feat: the temperature and maxTokens of the openai can be configured 2024-05-21 23:15:46 +08:00
Gabe
9ca1309cec Merge pull request #126 from kebyn/master
fix: option translation
2024-05-21 20:02:53 +08:00
kebyn
a03afc05f5 fix: option translation 2024-05-21 08:56:09 +00:00
Gabe Yuan
0198963584 fix: deeplx: replace auto to blank string 2024-05-21 11:55:17 +08:00
Gabe Yuan
58e745d967 v1.8.10 2024-05-17 10:38:11 +08:00
Gabe Yuan
377e347d68 feat: support translate hooks 2024-05-15 11:07:13 +08:00
Gabe Yuan
bac0704d3d feat: download and upload settings 2024-05-12 20:24:40 +08:00
Gabe Yuan
d2ff46edf6 fix: show full gemini url 2024-05-12 16:25:20 +08:00
Gabe Yuan
f908372b4e feat: support hook for custom api 2024-05-12 16:10:11 +08:00
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
Gabe Yuan
63f989b31a v1.8.7 2024-04-18 10:07:53 +08:00
Gabe Yuan
2ae5d01d5c fix: custom option 2024-04-18 09:48:07 +08:00
Gabe Yuan
130f1deed1 fix: remove encodeURIComponent 2024-04-18 00:31:36 +08:00
Gabe Yuan
5880d85b48 fix: encodeURIComponent text 2024-04-17 23:44:53 +08:00
Gabe Yuan
9455670e80 feat: custom request 2024-04-17 22:35:12 +08:00
Gabe Yuan
e369321c66 feat: custom request 2024-04-17 17:38:54 +08:00
Gabe Yuan
efc51b0d46 feat: extend styles for transbox 2024-04-17 15:35:44 +08:00
Gabe Yuan
d6f3b23b88 fix: tranbox ui 2024-04-17 10:31:37 +08:00
Gabe Yuan
0a4fa7b9f8 fix: tranbox ui 2024-04-17 10:03:56 +08:00
Gabe Yuan
2b3e4a8d25 fix: reaplce loading button 2024-04-16 16:39:11 +08:00
Gabe Yuan
bf3a16f96d feat: export word & translation 2024-04-16 16:29:59 +08:00
Gabe Yuan
b416e72820 fix: transbox ui 2024-04-16 15:22:27 +08:00
Gabe Yuan
ca84bdb227 feat: tranbox hover trigger 2024-04-16 12:47:55 +08:00
Gabe Yuan
148a4e97a6 fix: tranbox ui 2024-04-16 11:25:04 +08:00
Gabe Yuan
a13493ebc2 feat: simple style tranbox 2024-04-16 00:54:37 +08:00
Gabe Yuan
ce4ac79e5f fix: optimization tranbox 2024-04-15 18:04:35 +08:00
Gabe Yuan
8f76ea49e7 fix: loading icon 2024-04-13 21:23:58 +08:00
Gabe Yuan
923d3293cd fix: limit selection btn in window & click to hide 2024-04-12 22:28:40 +08:00
Gabe Yuan
7379ff8d15 v1.8.6 2024-04-12 14:39:17 +08:00
Gabe Yuan
18ebec350d fix: clean env 2024-04-12 14:33:29 +08:00
Gabe Yuan
3b0cbc53aa fix: response err data: url 2024-04-12 11:47:22 +08:00
Gabe Yuan
f00e8ffa4d feat: add niutrans api 2024-04-12 11:31:01 +08:00
Gabe Yuan
d6f7aad1c3 fix: utils func 2024-04-11 10:44:25 +08:00
Gabe Yuan
092ea6e836 fix: custom api 2024-04-10 13:37:16 +08:00
Gabe Yuan
d565e2464a feat: tranbox: mobile support 2024-04-07 16:55:54 +08:00
56 changed files with 10597 additions and 6382 deletions

10
.env
View File

@@ -2,26 +2,18 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.8.5
REACT_APP_VERSION=1.9.1
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js

View File

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

4
.prettierignore Normal file
View File

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

24
.prettierrc Normal file
View File

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

View File

@@ -1,5 +1,7 @@
# KISS Translator
English | [简体中文](README.md)
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)
@@ -12,7 +14,7 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari
- [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] Covers common translation scenarios
- [x] Web bilingual translation

View File

@@ -1,5 +1,7 @@
# 简约翻译
[English](README.en.md) | 简体中文
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[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
- [ ] Safari
- [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] 自定义翻译接口
- [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译

View File

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

View File

@@ -1,21 +1,23 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.8.5",
"version": "1.9.1",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.9",
"@mui/material": "^5.14.10",
"@mui/icons-material": "^5.15.15",
"@mui/lab": "5.0.0-alpha.170",
"@mui/material": "^5.15.15",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"sval": "^0.5.2",
"webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0"
},
@@ -24,13 +26,16 @@
"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: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: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": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"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",
"pack": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .",
"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",
"zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
"build+zip": "pnpm build && pnpm zip",
"format": "prettier --write \"**/*.{js,json,html}\"",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
@@ -42,7 +47,8 @@
"globals": {
"GM": true,
"unsafeWindow": true,
"globalThis": true
"globalThis": true,
"messenger": true
}
},
"browserslist": {
@@ -62,6 +68,7 @@
"@babel/node": "^7.22.19",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.20",
"prettier": "3.6.2",
"react-app-rewired": "^2.2.1"
}
}

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
{
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.9.1",
"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,242 +1,5 @@
import queryString from "query-string";
import { getBdauth, setBdauth } from "../libs/storage";
import {
URL_BAIDU_WEB,
URL_BAIDU_TRANSAPI_V2,
URL_BAIDU_TRANSAPI,
} 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];
};
import { URL_BAIDU_TRANSAPI, DEFAULT_USER_AGENT } from "../config";
export const genBaidu = async ({ text, from, to }) => {
const data = {
@@ -248,7 +11,9 @@ export const genBaidu = async ({ text, from, to }) => {
const init = {
headers: {
// Origin: "https://fanyi.baidu.com",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"User-Agent": DEFAULT_USER_AGENT,
},
method: "POST",
body: queryString.stringify(data),

View File

@@ -1,19 +1,35 @@
import queryString from "query-string";
import { fetchPolyfill } from "../libs/fetch";
import { fetchData } from "../libs/fetch";
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_CACHE_TRAN,
KV_SALT_SYNC,
URL_GOOGLE_TRAN,
URL_MICROSOFT_LANGDETECT,
URL_BAIDU_LANGDETECT,
URL_BAIDU_SUGGEST,
URL_BAIDU_TTS,
@@ -21,8 +37,12 @@ import {
URL_TENCENT_TRANSMART,
OPT_LANGS_TENCENT,
OPT_LANGS_SPECIAL,
OPT_LANGS_MICROSOFT,
} from "../config";
import { sha256 } from "../libs/utils";
import interpreter from "../libs/interpreter";
import { msAuth } from "../libs/auth";
import { kissLog } from "../libs/log";
/**
* 同步数据
@@ -32,7 +52,7 @@ import { sha256 } from "../libs/utils";
* @returns
*/
export const apiSyncData = async (url, key, data) =>
fetchPolyfill(url, {
fetchData(url, {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
@@ -46,7 +66,53 @@ export const apiSyncData = async (url, key, data) =>
* @param {*} url
* @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;
};
/**
* 百度语言识别
@@ -54,7 +120,7 @@ export const apiFetch = (url) => fetchPolyfill(url);
* @returns
*/
export const apiBaiduLangdetect = async (text) => {
const res = await fetchPolyfill(URL_BAIDU_LANGDETECT, {
const res = await fetchData(URL_BAIDU_LANGDETECT, {
headers: {
"Content-type": "application/json",
},
@@ -78,7 +144,7 @@ export const apiBaiduLangdetect = async (text) => {
* @returns
*/
export const apiBaiduSuggest = async (text) => {
const res = await fetchPolyfill(URL_BAIDU_SUGGEST, {
const res = await fetchData(URL_BAIDU_SUGGEST, {
headers: {
"Content-type": "application/json",
},
@@ -105,7 +171,7 @@ export const apiBaiduSuggest = async (text) => {
*/
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`;
return fetchPolyfill(url, {
return fetchData(url, {
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
});
};
@@ -123,7 +189,7 @@ export const apiTencentLangdetect = async (text) => {
text,
});
const res = await fetchPolyfill(URL_TENCENT_TRANSMART, {
const res = await fetchData(URL_TENCENT_TRANSMART, {
headers: {
"Content-type": "application/json",
},
@@ -161,7 +227,7 @@ export const apiTranslate = async ({
OPT_LANGS_SPECIAL[translator].get("auto");
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
if (!to) {
console.log(`[trans] target lang: ${toLang} not support`);
kissLog(`target lang: ${toLang} not support`, "translate");
return [trText, isSame];
}
@@ -182,7 +248,7 @@ export const apiTranslate = async ({
to,
};
const res = await fetchPolyfill(
const res = await fetchData(
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
{
useCache,
@@ -197,6 +263,10 @@ export const apiTranslate = async ({
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
break;
case OPT_TRANS_GOOGLE_2:
trText = res?.[0]?.[0] || "";
isSame = to === res.src;
break;
case OPT_TRANS_MICROSOFT:
trText = res
.map((item) => item.translations.map((item) => item.text).join(" "))
@@ -215,6 +285,14 @@ export const apiTranslate = async ({
trText = res.data;
isSame = to === res.source_lang;
break;
case OPT_TRANS_NIUTRANS:
const json = JSON.parse(res);
if (json.error_msg) {
throw new Error(json.error_msg);
}
trText = json.tgt_text;
isSame = to === json.from;
break;
case OPT_TRANS_BAIDU:
// trText = res.trans_result?.data.map((item) => item.dst).join(" ");
// isSame = res.trans_result?.to === res.trans_result?.from;
@@ -227,10 +305,17 @@ export const apiTranslate = async ({
}
break;
case OPT_TRANS_TENCENT:
trText = res.auto_translation;
trText = res?.auto_translation?.[0];
isSame = text === trText;
break;
case OPT_TRANS_VOLCENGINE:
trText = res?.translation || "";
isSame = to === res?.detected_language;
break;
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(" ");
isSame = text === trText;
break;
@@ -240,13 +325,39 @@ export const apiTranslate = async ({
.join(" ");
isSame = text === trText;
break;
case OPT_TRANS_CLAUDE:
trText = res?.content?.map((item) => item.text).join(" ");
isSame = text === trText;
break;
case OPT_TRANS_CLOUDFLAREAI:
trText = res?.result?.translated_text;
isSame = text === trText;
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:
trText = res.text;
isSame = to === res.from;
case OPT_TRANS_CUSTOMIZE_2:
case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5:
const { resHook } = apiSetting;
if (resHook?.trim()) {
interpreter.run(`exports.resHook = ${resHook}`);
[trText, isSame] = interpreter.exports.resHook(res, text, from, to);
} else {
trText = res.text;
isSame = to === res.from;
}
break;
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,
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,
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: userPrompt,
},
],
temperature,
max_tokens: maxTokens,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genClaude = ({
text,
from,
to,
url,
key,
systemPrompt,
userPrompt,
model,
temperature,
maxTokens,
}) => {
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
model,
system: systemPrompt,
messages: [
{
role: "user",
content: userPrompt,
},
],
temperature,
max_tokens: maxTokens,
};
const init = {
headers: {
"Content-type": "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": key,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genOllama = ({
text,
from,
to,
think,
url,
key,
systemPrompt,
userPrompt,
model,
}) => {
systemPrompt = systemPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
userPrompt = userPrompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
model,
system: systemPrompt,
prompt: userPrompt,
think: think,
stream: false,
};
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 {
MSG_FETCH,
MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR,
MSG_GET_HTTPCACHE,
MSG_TRANS_TOGGLE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
@@ -18,10 +17,11 @@ import {
CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
CMD_OPEN_TRANBOX,
CLIENT_THUNDERBIRD,
} from "./config";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync";
import { fetchData, fetchPool } from "./libs/fetch";
import { fetchHandle, getHttpCache } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs";
@@ -45,7 +45,7 @@ const REMOVE_HEADERS = [
async function addContextMenus(contextMenuType = 1) {
// 添加前先删除,避免重复ID的错误
try {
await browser.contextMenus.removeAll();
await browser.menus.removeAll();
} 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(() => {
tryInitDefaultData();
//在thunderbird中注册脚本
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
registerMsgDisplayScript();
}
// 右键菜单
addContextMenus();
@@ -152,6 +166,11 @@ browser.runtime.onStartup.addListener(async () => {
tryClearCaches();
}
//在thunderbird中注册脚本
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
registerMsgDisplayScript();
}
// 右键菜单
// firefox重启后菜单会消失,故重复添加
addContextMenus(contextMenuType);
@@ -169,13 +188,10 @@ browser.runtime.onStartup.addListener(async () => {
browser.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_FETCH:
const { input, opts } = args;
return await fetchData(input, opts);
case MSG_FETCH_LIMIT:
const { interval, limit } = args;
return fetchPool.update(interval, limit);
case MSG_FETCH_CLEAR:
return fetchPool.clear();
return await fetchHandle(args);
case MSG_GET_HTTPCACHE:
const { input, init } = args;
return await getHttpCache(input, init);
case MSG_OPEN_OPTIONS:
return await browser.runtime.openOptionsPage();
case MSG_SAVE_RULE:

View File

@@ -72,7 +72,7 @@ function runtimeListener(translator) {
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
return { rule: translator.rule, setting: translator.setting };
});
}
@@ -135,12 +135,18 @@ async function showFab(translator) {
* @param {*} param0
* @returns
*/
function showTransbox({
contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
}) {
if (!tranboxSetting?.transOpen) {
function showTransbox(
{
contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
darkMode,
uiLang,
langDetector,
},
{ transSelected }
) {
if (transSelected === "false") {
return;
}
@@ -153,6 +159,8 @@ function showTransbox({
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowRootElement.classList.add(`KT-transbox`);
shadowRootElement.classList.add(`KT-transbox_${darkMode ? "dark" : "light"}`);
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
@@ -167,6 +175,8 @@ function showTransbox({
contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting}
transApis={transApis}
uiLang={uiLang}
langDetector={langDetector}
/>
</CacheProvider>
</React.StrictMode>
@@ -213,8 +223,7 @@ export async function run(isUserscript = false) {
if (
isUserscript &&
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
href.includes(process.env.REACT_APP_OPTIONSPAGE2))
href.includes(process.env.REACT_APP_OPTIONSPAGE))
) {
runSettingPage();
return;
@@ -245,7 +254,7 @@ export async function run(isUserscript = false) {
inputTranslate(setting);
// 划词翻译
showTransbox(setting);
showTransbox(setting, rule);
// 浮球按钮
await showFab(translator);

View File

@@ -42,58 +42,81 @@ const customApiLangs = `["en", "English - English"],
["vi", "Vietnamese - Tiếng Việt"],
`;
const customApiHelpZH = `/// 自定义翻译源接口说明
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)数据将按下面规范发送
{
url: {{YOUR_URL}},
method: "POST",
// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
"Authorization": "Bearer {{YOUR_KEY}}",
"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}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // 待翻译文字
"from": "{{from}}", // 文字的语言(可能为空)
"to": "{{to}}", // 目标语言
},
body: {
text: "", // 需要翻译的文字
from: "", // 源语言,可能为空,表示需要接口自动识别语言
to: "", // 目标语言
}
}
// 返回Response数据需符合下面的JSON规范
// 返回数据默认格式
{
text: "", // 翻译后的文字
from: "", // 识别的源语言
to: "", // 目标语言(可选)
}
// Hook 范例
${hookExample}
// 支持的语言代码如下
${customApiLangs}
`;
const customApiHelpEN = `/// Custom translation source interface description
// Request data will be sent according to the following specifications
const customApiHelpEN = `// Default request
{
url: {{YOUR_URL}},
method: "POST",
headers: {
"url": "{{url}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer {{YOUR_KEY}}",
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // Text to be translated
"from": "{{from}}", // The language of the text (may be empty)
"to": "{{to}}", // Target language
},
body: {
text: "", // text to be translated
from: "", // Source language, may be empty
to: "", // Target language
}
}
// The returned data must conform to the following JSON specification
// Default response
{
text: "", // translated text
from: "", // Recognized source language
to: "", // Target language (optional)
}
/// Hook Example
${hookExample}
// The supported language codes are as follows
${customApiLangs}
`;
@@ -163,6 +186,22 @@ export const I18N = {
zh: `最大并发请求数量 (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: {
zh: `每次请求间隔时间 (0-5000ms)`,
en: `Time Between Requests (0-5000ms)`,
@@ -171,6 +210,10 @@ export const I18N = {
zh: `重新翻译间隔时间 (100-5000ms)`,
en: `Retranslation Interval (100-5000ms)`,
},
http_timeout: {
zh: `请求超时时间 (5000-30000ms)`,
en: `Request Timeout Time (5000-30000ms)`,
},
min_translate_length: {
zh: `最小翻译字符数 (1-100)`,
en: `Minimum number Of Translated Characters (1-100)`,
@@ -404,8 +447,8 @@ export const I18N = {
en: `Keep unchanged selector`,
},
keep_selector_helper: {
zh: `1、遵循CSS选择器语法。2、子元素选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Sub-element selectors are separated by ">>>".`,
zh: `1、遵循CSS选择器语法。`,
en: `1. Follow CSS selector syntax.`,
},
terms: {
zh: `专业术语`,
@@ -463,6 +506,10 @@ export const I18N = {
zh: `导出`,
en: `Export`,
},
export_translation: {
zh: `导出释义`,
en: `Export Translation`,
},
error_cant_be_blank: {
zh: `不能为空`,
en: `Can not be blank`,
@@ -627,6 +674,10 @@ export const I18N = {
zh: `点击外部关闭弹窗`,
en: `Click outside to close the pop-up window`,
},
use_simple_style: {
zh: `使用简洁界面`,
en: `Use a simple interface`,
},
show: {
zh: `显示`,
en: `Show`,
@@ -712,12 +763,20 @@ export const I18N = {
en: `Open Translate Popup/Translate Selected Shortcut`,
},
tranbtn_offset_x: {
zh: `翻译按钮偏移X0-100`,
en: `Translate Button Offset X (0-100)`,
zh: `翻译按钮偏移X±200`,
en: `Translate Button Offset X (±200)`,
},
tranbtn_offset_y: {
zh: `翻译按钮偏移Y0-100`,
en: `Translate Button Offset Y (0-100)`,
zh: `翻译按钮偏移Y±200`,
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: {
zh: `译文`,
@@ -792,8 +851,8 @@ export const I18N = {
en: `Secondary Context Menus`,
},
mulkeys_help: {
zh: `支持用换行或英文逗号“,”分隔多个KEY轮询调用。`,
en: `Supports multiple KEY polling calls separated by newlines or English commas ",".`,
zh: `支持用换行或英文逗号“,”分隔轮询调用。`,
en: `Supports polling calls separated by newlines or English commas ",".`,
},
translation_element_tag: {
zh: `译文元素标签`,
@@ -815,8 +874,100 @@ export const I18N = {
zh: `更多`,
en: `More`,
},
less: {
zh: `更少`,
en: `Less`,
},
fixer_selector: {
zh: `网页修复选择器`,
en: `Fixer Selector`,
},
reg_niutrans: {
zh: `获取小牛翻译密钥【简约翻译专属新用户注册赠送300万字符】`,
en: `Get NiuTrans APIKey [KISS Translator Exclusive New User Registration Free 3 Million Characters]`,
},
trigger_mode: {
zh: `触发方式`,
en: `Trigger Mode`,
},
trigger_click: {
zh: `点击触发`,
en: `Click Trigger`,
},
trigger_hover: {
zh: `鼠标悬停触发`,
en: `Hover Trigger`,
},
trigger_select: {
zh: `选中触发`,
en: `Select Trigger`,
},
extend_styles: {
zh: `附加样式`,
en: `Extend Styles`,
},
custom_option: {
zh: `自定义选项`,
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_FIREFOX = "firefox";
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_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 MSG_FETCH = "fetch";
export const MSG_FETCH_LIMIT = "fetch_limit";
export const MSG_FETCH_CLEAR = "fetch_clear";
export const MSG_GET_HTTPCACHE = "get_httpcache";
export const MSG_OPEN_OPTIONS = "open_options";
export const MSG_SAVE_RULE = "save_rule";
export const MSG_TRANS_TOGGLE = "trans_toggle";
@@ -79,9 +84,20 @@ export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
// api.cognitive.microsofttranslator.com
export const URL_MICROSOFT_TRAN =
"https://api-edge.cognitive.microsofttranslator.com/translate";
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_SUGGEST = "https://fanyi.baidu.com/sug";
export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts";
@@ -90,17 +106,36 @@ export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
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 =
"https://niutrans.com/login?active=3&userSource=kiss-translator";
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";
export const OPT_DICT_BAIDU = "Baidu";
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_GOOGLE_2 = "Google2";
export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_DEEPLX = "DeepLX";
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_NIUTRANS = "NiuTrans";
export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_VOLCENGINE = "Volcengine";
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_2 = "Gemini2";
export const OPT_TRANS_CLAUDE = "Claude";
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_2 = "Custom2";
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
@@ -108,15 +143,25 @@ export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_GOOGLE_2,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
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,
@@ -124,6 +169,13 @@ export const OPT_TRANS_ALL = [
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 = [
["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"],
@@ -166,6 +218,7 @@ export const OPT_LANGS_TO = [
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
export const OPT_LANGS_SPECIAL = {
[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_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
@@ -186,10 +239,22 @@ export const OPT_LANGS_SPECIAL = {
]),
[OPT_TRANS_DEEPLX]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""],
["auto", "auto"],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_NIUTRANS]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", "auto"],
["zh-CN", "zh"],
["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_LANGS_FROM.map(([key]) => [key, key]),
["zh-CN", "zh"],
@@ -242,9 +307,30 @@ export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_OPENAI]: new Map(
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_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([
["auto", ""],
["zh-CN", "chinese"],
@@ -263,8 +349,30 @@ export const OPT_LANGS_SPECIAL = {
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_2]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_3]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_4]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_5]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
};
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(
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
v,
@@ -326,9 +434,12 @@ export const OPT_TIMING_ALL = [
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
export const PROMPT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
@@ -357,10 +468,14 @@ export const GLOBLA_RULE = {
transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
transTitle: "false", // 是否同时翻译页面标题
transSelected: "true", // 是否启用划词翻译
detectRemote: "false", // 是否使用远程语言检测
skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器
fixerFunc: "-", // 修复函数
transStartHook: "", // 钩子函数
transEndHook: "", // 钩子函数
transRemoveHook: "", // 钩子函数
};
// 输入框翻译
@@ -378,9 +493,21 @@ export const DEFAULT_INPUT_RULE = {
};
// 划词翻译
export const PHONIC_MAP = {
en_phonic: ["英", "uk"],
us_phonic: ["美", "en"],
};
export const OPT_TRANBOX_TRIGGER_CLICK = "click";
export const OPT_TRANBOX_TRIGGER_HOVER = "hover";
export const OPT_TRANBOX_TRIGGER_SELECT = "select";
export const OPT_TRANBOX_TRIGGER_ALL = [
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT,
];
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
export const DEFAULT_TRANBOX_SETTING = {
transOpen: true,
// transOpen: true, // 是否启用划词翻译作废移至rule
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",
@@ -388,8 +515,15 @@ export const DEFAULT_TRANBOX_SETTING = {
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10,
btnOffsetY: 10,
boxOffsetX: 0,
boxOffsetY: 10,
hideTranBtn: false, // 是否隐藏翻译按钮
hideClickAway: false, // 是否点击外部关闭弹窗
simpleStyle: false, // 是否简洁界面
followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
};
// 订阅列表
@@ -408,70 +542,189 @@ export const DEFAULT_SUBRULES_LIST = [
},
];
export const DEFAULT_HTTP_TIMEOUT = 5000; // 调用超时时间
// 翻译接口
const defaultCustomApi = {
url: "",
key: "",
customOption: "", // (作废)
reqHook: "", // request 钩子函数
resHook: "", // response 钩子函数
fetchLimit: DEFAULT_FETCH_LIMIT,
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 = {
[OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single",
url: URL_GOOGLE_TRAN,
key: "",
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
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]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_MICROSOFT,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_BAIDU]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_BAIDU,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_TENCENT]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
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]: {
url: "https://api-free.deepl.com/v2/translate",
key: "",
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_DEEPL,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_DEEPLFREE]: {
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_DEEPLFREE,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_DEEPLX]: {
url: "http://localhost:1188/translate",
key: "",
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_DEEPLX,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_OPENAI]: {
url: "https://api.openai.com/v1/chat/completions",
[OPT_TRANS_NIUTRANS]: {
url: "https://api.niutrans.com/NiuTransServer/translation",
key: "",
model: "gpt-4",
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
fetchLimit: 1,
fetchInterval: 500,
dictNo: "",
memoryNo: "",
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
apiName: OPT_TRANS_NIUTRANS,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
},
[OPT_TRANS_OPENAI]: defaultOpenaiApi,
[OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
[OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
[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: "",
model: "gemini-pro",
prompt: `Translate the following text from ${PROMPT_PLACE_FROM} to ${PROMPT_PLACE_TO}:\n\n${PROMPT_PLACE_TEXT}`,
model: "gemini-2.5-flash",
systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 2048,
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_GEMINI,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
},
[OPT_TRANS_GEMINI_2]: {
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
key: "",
model: "gemini-2.0-flash",
systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 2048,
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_GEMINI_2,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
},
[OPT_TRANS_CLAUDE]: {
url: "https://api.anthropic.com/v1/messages",
key: "",
model: "claude-3-haiku-20240307",
systemPrompt: `You are a professional, authentic machine translation engine.`,
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
temperature: 0,
maxTokens: 1024,
fetchLimit: 1,
fetchInterval: 500,
apiName: OPT_TRANS_CLAUDE,
isDisabled: false,
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
},
[OPT_TRANS_CLOUDFLAREAI]: {
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: "",
fetchLimit: 1,
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_2]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
@@ -511,6 +764,7 @@ export const DEFAULT_SETTING = {
minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH,
httpTimeout: DEFAULT_HTTP_TIMEOUT,
clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则
// injectWebfix: true, // 是否注入修复补丁(作废)
@@ -532,6 +786,7 @@ export const DEFAULT_SETTING = {
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
// disableLangs: [], // 不翻译的语言(移至rule作废)
transInterval: 500, // 翻译间隔时间
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
};
export const DEFAULT_RULES = [GLOBLA_RULE];

View File

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

View File

@@ -8,7 +8,10 @@ export function useApi(translator) {
const updateApi = useCallback(
async (obj) => {
const api = transApis[translator] || {};
const api = {
...DEFAULT_TRANS_APIS[translator],
...(transApis[translator] || {}),
};
Object.assign(transApis, { [translator]: { ...api, ...obj } });
await updateSetting({ transApis });
},
@@ -20,5 +23,12 @@ export function useApi(translator) {
await updateSetting({ transApis });
}, [translator, transApis, updateSetting]);
return { api: transApis[translator] || {}, updateApi, resetApi };
return {
api: {
...DEFAULT_TRANS_APIS[translator],
...(transApis[translator] || {}),
},
updateApi,
resetApi,
};
}

View File

@@ -2,6 +2,14 @@ import { useSetting } from "./Setting";
import { I18N, URL_RAW_PREFIX } from "../config";
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
* @returns
@@ -10,7 +18,7 @@ export const useI18n = () => {
const {
setting: { uiLang },
} = useSetting();
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
return useLangMap(uiLang);
};
export const useI18nMd = (key) => {

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { useDarkMode } from "./ColorMode";
import { THEME_DARK, THEME_LIGHT } from "../config";
@@ -9,7 +9,7 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
* @param {*} param0
* @returns
*/
export default function Theme({ children, options }) {
export default function Theme({ children, options, styles }) {
const { darkMode } = useDarkMode();
const theme = useMemo(() => {
let htmlFontSize = 16;
@@ -38,6 +38,7 @@ export default function Theme({ children, options }) {
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<GlobalStyles styles={styles} />
{children}
</ThemeProvider>
);

View File

@@ -30,7 +30,14 @@ export function useTranslate(q, rule, setting) {
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))) {
setSamelang(true);
} else {
@@ -39,8 +46,10 @@ export function useTranslate(q, rule, setting) {
text: q,
fromLang,
toLang,
apiSetting:
setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator],
apiSetting: {
...DEFAULT_TRANS_APIS[translator],
...(setting.transApis[translator] || {}),
},
});
setText(trText);
setSamelang(isSame);

View File

@@ -36,19 +36,10 @@ function App() {
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install/Update Userscript for Tampermonkey/Violentmonkey 2
</Link> */}
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install/Update Userscript for iOS Safari
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install/Update Userscript for iOS Safari 2
</Link> */}
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
{/* <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link> */}
</Stack>
{loading ? (

View File

@@ -1,6 +1,6 @@
import { getMsauth, setMsauth } from "./storage";
import { URL_MICROSOFT_AUTH } from "../config";
import { fetchData } from "./fetch";
import { fetchHandle } from "./fetch";
import { kissLog } from "./log";
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);
await setMsauth({ token, exp });
return [token, exp];

View File

@@ -1,20 +1,38 @@
import { isExt, isGm } from "./client";
import { sendBgMsg } from "./msg";
import { taskPool } from "./pool";
import { getSettingWithDefault } from "./storage";
import {
MSG_FETCH,
MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR,
MSG_GET_HTTPCACHE,
CACHE_NAME,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT,
DEFAULT_HTTP_TIMEOUT,
} from "../config";
import { isBg } from "./browser";
import { newCacheReq, newTransReq } from "./req";
import { genTransReq } from "../apis/trans";
import { kissLog } from "./log";
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
* @returns
*/
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
export const fetchGM = async (
input,
{ method = "GET", headers, body, timeout } = {}
) =>
new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method,
@@ -30,8 +51,8 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
headers,
data: body,
// withCredentials: true,
timeout: TIMEOUT,
onload: ({ response, responseHeaders, status, statusText, ...opts }) => {
timeout,
onload: ({ response, responseHeaders, status, statusText }) => {
const headers = {};
responseHeaders.split("\n").forEach((line) => {
const [name, value] = line.split(":").map((item) => item.trim());
@@ -55,110 +76,82 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
* @param {*} param0
* @returns
*/
export const fetchApi = async ({ input, init, transOpts, apiSetting }) => {
export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
if (transOpts?.translator) {
[input, init] = await newTransReq(transOpts, apiSetting);
[input, init] = await genTransReq(transOpts, apiSetting);
}
if (!input) {
throw new Error("url is empty");
}
if (isGm) {
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) {
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,
});
let timeout = apiSetting?.httpTimeout || DEFAULT_HTTP_TIMEOUT;
if (!apiSetting) {
try {
timeout = (await getSettingWithDefault()).httpTimeout;
} catch (err) {
//
}
}
if (AbortSignal?.timeout) {
Object.assign(init, { signal: AbortSignal.timeout(TIMEOUT) });
if (isGm) {
// 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);
};
/**
* 请求池实例
*/
export const fetchPool = taskPool(
fetchApi,
null,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT
);
/**
* 请求数据统一接口
* @param {*} input
* @param {*} opts
* 解析 response
* @param {*} res
* @returns
*/
export const fetchData = async (
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");
}
}
const parseResponse = async (res) => {
if (!res) {
// 发送请求
if (usePool) {
res = await fetchPool.push({ input, init, transOpts, apiSetting });
} else {
res = await fetchApi({ input, init, transOpts, apiSetting });
}
if (!res?.ok) {
const msg = {
url: input,
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");
}
}
return null;
}
const contentType = res.headers.get("Content-Type");
@@ -172,23 +165,141 @@ export const fetchData = async (
};
/**
* fetch 兼容性封装
* 查询 caches
* @param {*} input
* @param {*} opts
* @param {*} param1
* @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()) {
throw new Error("URL is empty");
}
// 插件
if (isExt && !isBg()) {
return await sendBgMsg(MSG_FETCH, { input, opts });
// 查询缓存
if (useCache) {
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 });
};
/**
@@ -196,21 +307,13 @@ export const fetchPolyfill = async (input, opts) => {
* @param {*} interval
* @param {*} limit
*/
export const updateFetchPool = async (interval, limit) => {
if (isExt) {
await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
} else {
fetchPool.update(interval, limit);
}
export const updateFetchPool = (interval, limit) => {
fetchPool.update(interval, limit);
};
/**
* 清空任务池
*/
export const clearFetchPool = async () => {
if (isExt) {
await sendBgMsg(MSG_FETCH_CLEAR);
} else {
fetchPool.clear();
}
export const clearFetchPool = () => {
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 { apiBaiduLangdetect } from "../apis";
import {
apiGoogleLangdetect,
apiMicrosoftLangdetect,
apiBaiduLangdetect,
apiTencentLangdetect,
} from "../apis";
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
* @returns
*/
export const tryDetectLang = async (q, useRemote = false) => {
export const tryDetectLang = async (
q,
useRemote = false,
langDetector = OPT_TRANS_MICROSOFT
) => {
let lang = "";
if (useRemote) {
try {
lang = await apiBaiduLangdetect(q);
lang = await langdetectMap[langDetector](q);
} catch (err) {
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,317 +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_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE,
URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART,
PROMPT_PLACE_FROM,
PROMPT_PLACE_TO,
PROMPT_PLACE_TEXT,
} 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 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(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_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(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to)
.replaceAll(PROMPT_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 }) => {
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}`;
}
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:
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_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:
return genCustom(args);
default:
throw new Error(`[trans] translator: ${translator} not support`);
}
};

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ import { sendBgMsg } from "./msg";
import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log";
import interpreter from "./interpreter";
/**
* 翻译类
@@ -52,7 +53,7 @@ export class Translator {
];
_eventName = genEventName();
_mouseoverNode = null;
_keepSelector = [null, null];
_keepSelector = "";
_terms = [];
_docTitle = "";
@@ -124,9 +125,7 @@ export class Translator {
this._setting = setting;
this._rule = rule;
this._keepSelector = (rule.keepSelector || "")
.split(SHADOW_KEY)
.map((item) => item.trim());
this._keepSelector = rule.keepSelector || "";
this._terms = (rule.terms || "")
.split(/\n|;/)
.map((item) => item.split(",").map((item) => item.trim()))
@@ -405,6 +404,7 @@ export class Translator {
// 移除键盘监听
window.removeEventListener("keydown", this._handleKeydown);
const { transRemoveHook } = this._rule;
this._tranNodes.forEach((innerHTML, node) => {
if (
!this._rule.transTiming ||
@@ -420,10 +420,17 @@ export class Translator {
}
// 移除/恢复元素
if (innerHTML && this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} else {
node.querySelector(APP_LCNAME)?.remove();
if (innerHTML) {
if (this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} 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 { transStartHook } = this._rule;
if (transStartHook?.trim()) {
interpreter.run(`exports.transStartHook = ${transStartHook}`);
interpreter.exports.transStartHook(el, q);
}
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
const keepSelector = this._keepSelector.trim();
if (keepSelector) {
let text = "";
el.childNodes.forEach((child) => {
if (
child.nodeType === 1 &&
((matchSelector && child.matches(matchSelector)) ||
(subSelector && child.querySelector(subSelector)))
) {
if (child.nodeType === 1 && child.matches(keepSelector)) {
if (child.nodeName === "IMG") {
child.style.cssText += `width: ${child.width}px;`;
child.style.cssText += `height: ${child.height}px;`;
}
text += `[${keeps.length}]`;
keeps.push(child.outerHTML);
} else {
} else if (child.nodeType === 1 || child.nodeType === 3) {
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;
el.appendChild(traEl);
el.style.cssText += selectStyle;
if (el.parentElement) {
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);
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;
};
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
@@ -202,26 +212,20 @@ export const removeEndchar = (s, c, count = 1) => {
* @returns
*/
export const matchInputStr = (str, sign) => {
let reg = /\/([\w-]+)\s+([^]+)/;
switch (sign) {
case "//":
reg = /\/\/([\w-]+)\s+([^]+)/;
break;
return str.match(/\/\/([\w-]+)\s+([^]+)/);
case "\\":
reg = /\\([\w-]+)\s+([^]+)/;
break;
return str.match(/\\([\w-]+)\s+([^]+)/);
case "\\\\":
reg = /\\\\([\w-]+)\s+([^]+)/;
break;
return str.match(/\\\\([\w-]+)\s+([^]+)/);
case ">":
reg = />([\w-]+)\s+([^]+)/;
break;
return str.match(/>([\w-]+)\s+([^]+)/);
case ">>":
reg = />>([\w-]+)\s+([^]+)/;
break;
return str.match(/>>([\w-]+)\s+([^]+)/);
default:
}
return str.match(reg);
return str.match(/\/([\w-]+)\s+([^]+)/);
};
/**

View File

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

View File

@@ -15,6 +15,7 @@ import {
import { useTranslate } from "../../hooks/Translate";
import { styled, css } from "@mui/material/styles";
import { APP_LCNAME } from "../../config";
import interpreter from "../../libs/interpreter";
const LINE_STYLES = {
[OPT_STYLE_LINE]: "solid",
@@ -85,8 +86,15 @@ const StyledSpan = styled("span")`
export default function Content({ q, keeps, translator, $el }) {
const [rule, setRule] = useState(translator.rule);
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { transOpen, textStyle, bgColor, textDiyStyle, transOnly, transTag } =
rule;
const {
transOpen,
textStyle,
bgColor,
textDiyStyle,
transOnly,
transTag,
transEndHook,
} = rule;
const { newlineLength } = translator.setting;
@@ -107,6 +115,14 @@ export default function Content({ q, keeps, translator, $el }) {
};
}, [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(() => {
if (transOnly === "true") {
return "";

View File

@@ -1,21 +1,40 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
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 {
OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLX,
OPT_TRANS_DEEPLFREE,
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,
OPT_TRANS_NIUTRANS,
URL_KISS_PROXY,
URL_NIUTRANS_REG,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT,
} from "../../config";
import { useState } from "react";
import { useI18n } from "../../hooks/I18n";
@@ -30,7 +49,7 @@ import { useApi } from "../../hooks/Api";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import { limitNumber } from "../../libs/utils";
import { limitNumber, limitFloat } from "../../libs/utils";
function TestButton({ translator, api }) {
const i18n = useI18n();
@@ -48,7 +67,7 @@ function TestButton({ translator, api }) {
useCache: false,
});
if (!text) {
throw new Error("empty reault");
throw new Error("empty result");
}
alert.success(i18n("test_success"));
} catch (err) {
@@ -62,14 +81,24 @@ function TestButton({ translator, api }) {
alert.error(
<>
<div>{i18n("test_failed")}</div>
<pre
style={{
maxWidth: 400,
overflow: "auto",
}}
>
{msg}
</pre>
{msg === err.message ? (
<div
style={{
maxWidth: 400,
}}
>
{msg}
</div>
) : (
<pre
style={{
maxWidth: 400,
overflow: "auto",
}}
>
{msg}
</pre>
)}
</>
);
} finally {
@@ -77,14 +106,15 @@ function TestButton({ translator, api }) {
}
};
if (loading) {
return <CircularProgress size={16} />;
}
return (
<Button size="small" variant="contained" onClick={handleApiTest}>
<LoadingButton
size="small"
variant="contained"
onClick={handleApiTest}
loading={loading}
>
{i18n("click_test")}
</Button>
</LoadingButton>
);
}
@@ -95,9 +125,21 @@ function ApiFields({ translator }) {
url = "",
key = "",
model = "",
prompt = "",
systemPrompt = "",
userPrompt = "",
think = false,
thinkIgnore = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
dictNo = "",
memoryNo = "",
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
apiName = "",
isDisabled = false,
} = api;
const handleChange = (e) => {
@@ -109,6 +151,15 @@ function ApiFields({ translator }) {
case "fetchInterval":
value = limitNumber(value, 0, 5000);
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:
}
updateApi({
@@ -116,23 +167,59 @@ function ApiFields({ translator }) {
});
};
const buildinTranslators = [
const builtinTranslators = [
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_VOLCENGINE,
];
const mulkeysTranslators = [
OPT_TRANS_DEEPL,
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_NIUTRANS,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
];
const keyHelper =
translator === OPT_TRANS_NIUTRANS ? (
<>
{i18n("mulkeys_help")}
<Link href={URL_NIUTRANS_REG} target="_blank">
{i18n("reg_niutrans")}
</Link>
</>
) : mulkeysTranslators.includes(translator) ? (
i18n("mulkeys_help")
) : (
""
);
return (
<Stack spacing={3}>
{!buildinTranslators.includes(translator) && (
<TextField
size="small"
label={i18n("api_name")}
name="apiName"
value={apiName}
onChange={handleChange}
/>
{!builtinTranslators.includes(translator) && (
<>
<TextField
size="small"
@@ -140,6 +227,11 @@ function ApiFields({ translator }) {
name="url"
value={url}
onChange={handleChange}
multiline={translator === OPT_TRANS_DEEPLX}
maxRows={10}
helperText={
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
}
/>
<TextField
size="small"
@@ -148,16 +240,16 @@ function ApiFields({ translator }) {
value={key}
onChange={handleChange}
multiline={mulkeysTranslators.includes(translator)}
helperText={
mulkeysTranslators.includes(translator)
? i18n("mulkeys_help")
: ""
}
maxRows={10}
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
size="small"
@@ -168,11 +260,110 @@ function ApiFields({ translator }) {
/>
<TextField
size="small"
label={"PROMPT"}
name="prompt"
value={prompt}
label={"SYSTEM PROMPT"}
name="systemPrompt"
value={systemPrompt}
onChange={handleChange}
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 ||
translator === OPT_TRANS_GEMINI_2) && (
<>
<TextField
size="small"
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
<TextField
size="small"
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</>
)}
{translator === OPT_TRANS_NIUTRANS && (
<>
<TextField
size="small"
label={"DictNo"}
name="dictNo"
value={dictNo}
onChange={handleChange}
/>
<TextField
size="small"
label={"MemoryNo"}
name="memoryNo"
value={memoryNo}
onChange={handleChange}
/>
</>
)}
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)}
@@ -195,6 +386,29 @@ function ApiFields({ translator }) {
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}>
<TestButton translator={translator} api={api} />
<Button

View File

@@ -1,10 +1,15 @@
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import Button from "@mui/material/Button";
import LoadingButton from "@mui/lab/LoadingButton";
import { useState } from "react";
import { kissLog } from "../../libs/log";
export default function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => {
export default function DownloadButton({ handleData, text, fileName }) {
const [loading, setLoading] = useState(false);
const handleClick = async (e) => {
e.preventDefault();
if (data) {
try {
setLoading(true);
const data = await handleData();
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
@@ -12,16 +17,21 @@ export default function DownloadButton({ data, text, fileName }) {
document.body.appendChild(link);
link.click();
link.remove();
} catch (err) {
kissLog(err, "download");
} finally {
setLoading(false);
}
};
return (
<Button
<LoadingButton
size="small"
variant="outlined"
onClick={handleClick}
loading={loading}
startIcon={<FileDownloadIcon />}
>
{text}
</Button>
</LoadingButton>
);
}

View File

@@ -1,6 +1,5 @@
import Stack from "@mui/material/Stack";
import { OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState } from "react";
import { useState } from "react";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
@@ -8,53 +7,18 @@ import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "../../hooks/I18n";
import Alert from "@mui/material/Alert";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import { useFavWords } from "../../hooks/FavWords";
import DictCont from "../Selection/DictCont";
import SugCont from "../Selection/SugCont";
import DownloadButton from "./DownloadButton";
import UploadButton from "./UploadButton";
import Button from "@mui/material/Button";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import { isValidWord } from "../../libs/utils";
import { kissLog } from "../../libs/log";
function DictField({ word }) {
const [dictResult, setDictResult] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
(async () => {
try {
setLoading(true);
setError("");
const dictRes = await apiTranslate({
text: word,
translator: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
dictRes[2].type === 1 && setDictResult(JSON.parse(dictRes[2].result));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [word]);
if (loading) {
return <CircularProgress size={24} />;
}
if (error) {
return <Alert severity="error">{error}</Alert>;
}
return <DictCont dictResult={dictResult} />;
}
import { apiTranslate } from "../../apis";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
function FavAccordion({ word, index }) {
const [expanded, setExpanded] = useState(false);
@@ -72,7 +36,12 @@ function FavAccordion({ word, index }) {
<Typography>{`${index + 1}. ${word}`}</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <DictField word={word} />}
{expanded && (
<Stack spacing={2}>
<DictCont text={word} />
<SugCont text={word} />
</Stack>
)}
</AccordionDetails>
</Accordion>
);
@@ -98,6 +67,43 @@ export default function FavWords() {
}
};
const handleTranslation = async () => {
const tranList = [];
for (const text of downloadList) {
try {
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
if (dictRes[2]?.type === 1) {
tranList.push(JSON.parse(dictRes[2].result));
}
} catch (err) {
// skip
}
}
return tranList
.map((dictResult) =>
[
`## ${dictResult.src}`,
dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
.join(" "),
dictResult.content[0].mean
.map(({ pre, cont }) => {
return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
})
.join("\n"),
].join("\n\n")
)
.join("\n\n");
};
return (
<Box>
<Stack spacing={3}>
@@ -115,10 +121,15 @@ export default function FavWords() {
fileExts={[".txt", ".csv"]}
/>
<DownloadButton
data={downloadList.join("\n")}
handleData={() => downloadList.join("\n")}
text={i18n("export")}
fileName={`kiss-words_${Date.now()}.txt`}
/>
<DownloadButton
handleData={handleTranslation}
text={i18n("export_translation")}
fileName={`kiss-words_${Date.now()}.md`}
/>
<Button
size="small"
variant="outlined"

View File

@@ -27,6 +27,7 @@ import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { useRules } from "../../hooks/Rules";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
@@ -93,10 +94,14 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
transTiming = OPT_TIMING_PAGESCROLL,
transTag = DEFAULT_TRANS_TAG,
transTitle = "false",
transSelected = "true",
detectRemote = "false",
skipLangs = [],
fixerSelector = "",
fixerFunc = "-",
transStartHook = "",
transEndHook = "",
transRemoveHook = "",
} = formValues;
const hasSamePattern = (str) => {
@@ -177,6 +182,30 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</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 (
<form onSubmit={handleSubmit}>
<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 && (
<>
<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("translate_timing")}
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="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
size="small"
label={i18n("fixer_selector")}
name="fixerSelector"
value={fixerSelector}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
select
size="small"
name="fixerFunc"
value={fixerFunc}
label={i18n("fixer_function")}
helperText={i18n("fixer_function_helper")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{FIXER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
select
@@ -458,34 +531,42 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("fixer_selector")}
name="fixerSelector"
value={fixerSelector}
label={i18n("translate_start_hook")}
helperText={i18n("translate_start_hook_helper")}
name="transStartHook"
value={transStartHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
select
size="small"
name="fixerFunc"
value={fixerFunc}
label={i18n("fixer_function")}
helperText={i18n("fixer_function_helper")}
label={i18n("translate_end_hook")}
helperText={i18n("translate_end_hook_helper")}
name="transEndHook"
value={transEndHook}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{FIXER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("translate_remove_hook")}
helperText={i18n("translate_remove_hook_helper")}
name="transRemoveHook"
value={transRemoveHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
@@ -564,18 +645,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
{i18n("delete")}
</Button>
)}
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
{ShowMoreButton}
</>
) : (
<>
@@ -595,18 +665,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
>
{i18n("cancel")}
</Button>
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
{ShowMoreButton}
</>
)}
</Stack>
@@ -629,17 +688,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
>
{i18n("cancel")}
</Button>
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
>
{i18n("more")}
</Button>
)}
{ShowMoreButton}
</Stack>
))}
</Stack>
@@ -776,7 +825,7 @@ function UserRules({ subRules }) {
<UploadButton text={i18n("import")} handleImport={handleImport} />
<DownloadButton
data={JSON.stringify([...rules.list].reverse(), null, 2)}
handleData={() => JSON.stringify([...rules.list].reverse(), null, 2)}
text={i18n("export")}
fileName={`kiss-rules_${Date.now()}.json`}
/>

View File

@@ -17,6 +17,8 @@ import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_LANGDETECTOR_ALL,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
@@ -25,12 +27,15 @@ import {
DEFAULT_CSPLIST,
MSG_CONTEXT_MENUS,
MSG_UPDATE_CSP,
DEFAULT_HTTP_TIMEOUT,
} from "../../config";
import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab";
import { sendBgMsg } from "../../libs/msg";
import { kissLog } from "../../libs/log";
import UploadButton from "./UploadButton";
import DownloadButton from "./DownloadButton";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
@@ -67,6 +72,9 @@ export default function Settings() {
case "newlineLength":
value = limitNumber(value, 1, 1000);
break;
case "httpTimeout":
value = limitNumber(value, 5000, 30000);
break;
case "touchTranslate":
value = limitNumber(value, 0, 4);
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 {
uiLang,
minLength,
maxLength,
clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
contextMenuType = 1,
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
transInterval = 500,
langDetector = OPT_TRANS_MICROSOFT,
} = setting;
const { isHide = false } = fab || {};
return (
<Box>
<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">
<InputLabel>{i18n("ui_lang")}</InputLabel>
<Select
@@ -160,7 +193,14 @@ export default function Settings() {
defaultValue={transInterval}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
defaultValue={httpTimeout}
onChange={handleChange}
/>
<FormControl size="small">
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
<Select
@@ -206,6 +246,22 @@ export default function Settings() {
</Select>
</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 ? (
<>
<FormControl size="small">
@@ -278,6 +334,7 @@ export default function Settings() {
name="blacklist"
defaultValue={blacklist}
onChange={handleChange}
maxRows={10}
multiline
/>
</Stack>

View File

@@ -6,6 +6,7 @@ import { useSync } from "../../hooks/Sync";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import MenuItem from "@mui/material/MenuItem";
import LoadingButton from "@mui/lab/LoadingButton";
import {
URL_KISS_WORKER,
OPT_SYNCTYPE_ALL,
@@ -14,10 +15,8 @@ import {
} from "../../config";
import { useState } from "react";
import { syncSettingAndRules } from "../../libs/sync";
import Button from "@mui/material/Button";
import { useAlert } from "../../hooks/Alert";
import SyncIcon from "@mui/icons-material/Sync";
import CircularProgress from "@mui/material/CircularProgress";
import { useSetting } from "../../hooks/Setting";
import { kissLog } from "../../libs/log";
@@ -123,16 +122,16 @@ export default function SyncSetting() {
useFlexGap
flexWrap="wrap"
>
<Button
<LoadingButton
size="small"
variant="contained"
disabled={!syncUrl || !syncKey || loading}
onClick={handleSyncTest}
startIcon={<SyncIcon />}
loading={loading}
>
{i18n("sync_now")}
</Button>
{loading && <CircularProgress size={16} />}
</LoadingButton>
</Stack>
</Stack>
</Box>

View File

@@ -3,10 +3,15 @@ import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
import {
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useCallback } from "react";
import { limitNumber } from "../../libs/utils";
import { useTranbox } from "../../hooks/Tranbox";
@@ -21,10 +26,10 @@ export default function Tranbox() {
let { name, value } = e.target;
switch (name) {
case "btnOffsetX":
value = limitNumber(value, 0, 100);
break;
case "btnOffsetY":
value = limitNumber(value, 0, 100);
case "boxOffsetX":
case "boxOffsetY":
value = limitNumber(value, -200, 200);
break;
default:
}
@@ -41,7 +46,6 @@ export default function Tranbox() {
);
const {
transOpen,
translator,
fromLang,
toLang,
@@ -49,27 +53,20 @@ export default function Tranbox() {
tranboxShortcut,
btnOffsetX,
btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
hideTranBtn = false,
hideClickAway = false,
simpleStyle = false,
followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles = "",
enDict = OPT_DICT_BAIDU,
} = tranboxSetting;
return (
<Box>
<Stack spacing={3}>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateTranbox({ transOpen: !transOpen });
}}
/>
}
label={i18n("toggle_selection_translate")}
/>
<TextField
select
size="small"
@@ -131,6 +128,18 @@ export default function Tranbox() {
))}
</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
size="small"
label={i18n("tranbtn_offset_x")}
@@ -149,6 +158,24 @@ export default function Tranbox() {
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
select
size="small"
@@ -173,6 +200,55 @@ export default function Tranbox() {
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
<TextField
select
size="small"
name="simpleStyle"
value={simpleStyle}
label={i18n("use_simple_style")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</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
select
size="small"
name="triggerMode"
value={triggerMode}
label={i18n("trigger_mode")}
onChange={handleChange}
>
{OPT_TRANBOX_TRIGGER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(`trigger_${item}`)}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("extend_styles")}
name="extStyles"
defaultValue={extStyles}
onChange={handleChange}
maxRows={10}
multiline
/>
{!isExt && (
<ShortcutInput
value={tranboxShortcut}

View File

@@ -81,15 +81,9 @@ export default function Options() {
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install/Update Userscript for Tampermonkey/Violentmonkey 2
</Link> */}
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install/Update Userscript for iOS Safari
</Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install/Update Userscript for iOS Safari 2
</Link> */}
</Stack>
</center>
);

View File

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

View File

@@ -7,23 +7,23 @@ export default function AudioBtn({ text, lan = "uk" }) {
if (error || !ready) {
return (
<IconButton disabled>
<VolumeUpIcon />
<IconButton disabled size="small">
<VolumeUpIcon fontSize="inherit" />
</IconButton>
);
}
if (playing) {
return (
<IconButton color="primary">
<VolumeUpIcon />
<IconButton color="primary" size="small">
<VolumeUpIcon fontSize="inherit" />
</IconButton>
);
}
return (
<IconButton onClick={onPlay}>
<VolumeUpIcon />
<IconButton onClick={onPlay} size="small">
<VolumeUpIcon fontSize="inherit" />
</IconButton>
);
}

View File

@@ -1,53 +1,113 @@
import Box from "@mui/material/Box";
import { useState, useEffect } from "react";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn";
import CircularProgress from "@mui/material/CircularProgress";
import Alert from "@mui/material/Alert";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn";
const phonicMap = {
en_phonic: ["英", "uk"],
us_phonic: ["美", "en"],
};
export default function DictCont({ text }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null);
export default function DictCont({ dictResult }) {
if (!dictResult) {
useEffect(() => {
(async () => {
try {
setLoading(true);
setError("");
setDictResult(null);
if (!isValidWord(text)) {
return;
}
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
if (dictRes[2]?.type === 1) {
setDictResult(JSON.parse(dictRes[2].result));
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [text]);
if (error) {
return <Alert severity="error">{error}</Alert>;
}
if (loading) {
return <CircularProgress size={16} />;
}
if (!text || !dictResult) {
return;
}
const copyText = [
dictResult.src,
dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
.join(" "),
dictResult.content[0].mean
.map(({ pre, cont }) => {
return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
})
.join("\n"),
].join("\n");
return (
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
>
<Stack className="KT-transbox-dict" spacing={1}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.src}
</Typography>
<FavBtn word={dictResult.src} />
<Stack direction="row" justifyContent="space-between">
<CopyBtn text={copyText} />
<FavBtn word={dictResult.src} />
</Stack>
</Stack>
<Typography component="div">
<Stack direction="row">
<Typography component="div">
{dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => (
<span>
<span>{`${phonicMap[key]?.[0] || key} ${val}`}</span>
<AudioBtn text={dictResult.src} lan={phonicMap[key]?.[1]} />
</span>
<Typography
component="div"
key={key}
style={{ display: "inline-block" }}
>
<Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
</Typography>
))}
</Stack>
<ul style={{ margin: "0.5em 0" }}>
</Typography>
<Typography component="ul">
{dictResult.content[0].mean.map(({ pre, cont }, idx) => (
<li key={idx}>
<Typography component="li" key={idx}>
{pre && `[${pre}] `}
{Object.keys(cont).join("; ")}
</li>
</Typography>
))}
</ul>
</Typography>
</Typography>
</Box>
</Stack>
);
}

View File

@@ -1,65 +0,0 @@
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
const exchangeMap = {
word_third: "第三人称单数",
word_ing: "现在分词",
word_done: "过去式",
word_past: "过去分词",
word_pl: "复数",
word_proto: "词源",
};
export default function DictCont({ dictResult }) {
if (!dictResult) {
return;
}
return (
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
>
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.simple_means?.word_name}
</Typography>
<FavBtn word={dictResult.simple_means?.word_name} />
</Stack>
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
<Typography key={idx} component="div">
{(ph_en || ph_am) && (
<Typography>{`英 /${ph_en || ""}/ 美 /${ph_am || ""}/`}</Typography>
)}
<ul style={{ margin: "0.5em 0" }}>
{parts.map(({ part, means }, idx) => (
<li key={idx}>
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")}
</li>
))}
</ul>
</Typography>
))}
<Typography>
{Object.entries(dictResult.simple_means?.exchange || {})
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
.join("; ")}
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{Object.values(dictResult.simple_means?.tags || {})
.flat()
.filter((item) => item)
.map((item) => (
<Chip label={item} size="small" />
))}
</Stack>
</Box>
);
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import { isMobile } from "../../libs/mobile";
@@ -130,11 +130,11 @@ function Pointer({
export default function DraggableResizable({
header,
children,
defaultPosition = {
position = {
x: 0,
y: 0,
},
defaultSize = {
size = {
w: 600,
h: 400,
},
@@ -146,14 +146,13 @@ export default function DraggableResizable({
w: 1200,
h: 1200,
},
setSize,
setPosition,
onChangeSize,
onChangePosition,
...props
}) {
const lineWidth = 4;
const [position, setPosition] = useState(defaultPosition);
const [size, setSize] = useState(defaultSize);
const opts = {
size,
setSize,
@@ -163,16 +162,9 @@ export default function DraggableResizable({
maxSize,
};
useEffect(() => {
onChangeSize && onChangeSize(size);
}, [size, onChangeSize]);
useEffect(() => {
onChangePosition && onChangePosition(position);
}, [position, onChangePosition]);
return (
<Box
className="KT-draggable"
style={{
touchAction: "none",
position: "fixed",
@@ -219,11 +211,17 @@ export default function DraggableResizable({
}}
{...opts}
/>
<Paper elevation={4}>
<Pointer direction="Header" style={{ cursor: "move" }} {...opts}>
<Paper className="KT-draggable-body" elevation={4}>
<Pointer
className="KT-draggable-header"
direction="Header"
style={{ cursor: "move" }}
{...opts}
>
{header}
</Pointer>
<div
<Box
className="KT-draggable-container"
style={{
width: size.w,
height: size.h,
@@ -231,7 +229,7 @@ export default function DraggableResizable({
}}
>
{children}
</div>
</Box>
</Paper>
<Pointer
direction="Right"

View File

@@ -1,17 +1,35 @@
import Box from "@mui/material/Box";
import { useState, useEffect } from "react";
import Typography from "@mui/material/Typography";
import { apiBaiduSuggest } from "../../apis";
import Stack from "@mui/material/Stack";
export default function SugCont({ text }) {
const [sugs, setSugs] = useState([]);
useEffect(() => {
(async () => {
try {
setSugs(await apiBaiduSuggest(text));
} catch (err) {
// skip
}
})();
}, [text]);
if (sugs.length === 0) {
return;
}
export default function SugCont({ sugs }) {
return (
<Box>
<Stack className="KT-transbox-sug" spacing={1}>
{sugs.map(({ k, v }) => (
<Typography component="div" key={k}>
<Typography>{k}</Typography>
<ul style={{ margin: "0" }}>
<li>{v}</li>
</ul>
<Typography component="ul" style={{ margin: "0" }}>
<Typography component="li">{v}</Typography>
</Typography>
</Typography>
))}
</Box>
</Stack>
);
}

View File

@@ -1,7 +1,6 @@
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import DraggableResizable from "./DraggableResizable";
import Header from "../Popup/Header";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
@@ -10,13 +9,112 @@ import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import DoneIcon from "@mui/icons-material/Done";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import PushPinIcon from "@mui/icons-material/PushPin";
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 { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
import { useState, useRef } from "react";
import {
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 DictCont from "./DictCont";
import SugCont from "./SugCont";
import CopyBtn from "./CopyBtn";
import { isValidWord } from "../../libs/utils";
import { isMobile } from "../../libs/mobile";
function TranForm({ text, setText, tranboxSetting, transApis }) {
function Header({
setShowPopup,
simpleStyle,
setSimpleStyle,
hideClickAway,
setHideClickAway,
followSelection,
setFollowSelection,
mouseHover,
}) {
if (!isMobile && simpleStyle && !mouseHover) {
return;
}
return (
<Box
className="KT-transbox-header"
onMouseUp={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<DragIndicatorIcon fontSize="small" />
<Stack direction="row" alignItems="center">
<IconButton
size="small"
onClick={() => {
setHideClickAway((pre) => !pre);
}}
>
{hideClickAway ? (
<LockOpenIcon fontSize="small" />
) : (
<LockIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setFollowSelection((pre) => !pre);
}}
>
{followSelection ? (
<PushPinOutlinedIcon fontSize="small" />
) : (
<PushPinIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setSimpleStyle((pre) => !pre);
}}
>
{simpleStyle ? (
<UnfoldMoreIcon fontSize="small" />
) : (
<UnfoldLessIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Stack>
</Stack>
<Divider />
</Box>
);
}
function TranForm({
text,
setText,
tranboxSetting,
transApis,
simpleStyle,
langDetector,
enDict,
}) {
const i18n = useI18n();
const [editMode, setEditMode] = useState(false);
@@ -24,133 +122,166 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
const [translator, setTranslator] = useState(tranboxSetting.translator);
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
const [toLang, setToLang] = useState(tranboxSetting.toLang);
const [toLang2, setToLang2] = useState(tranboxSetting.toLang2);
const inputRef = useRef(null);
return (
<Stack sx={{ p: 2 }} spacing={2}>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={(e) => {
setFromLang(e.target.value);
}}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={(e) => {
setToLang(e.target.value);
}}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
value={translator}
name="translator"
label={i18n("translate_service")}
onChange={(e) => {
setTranslator(e.target.value);
}}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
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]
);
<Box>
<TextField
size="small"
label={i18n("original_text")}
inputRef={inputRef}
fullWidth
multiline
value={editMode ? editText : text}
onChange={(e) => {
setEditText(e.target.value);
}}
onFocus={() => {
setEditMode(true);
setEditText(text);
}}
onBlur={() => {
setEditMode(false);
setText(editText.trim());
}}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
{editMode ? (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
return (
<Stack
className="KT-transbox-container"
sx={{ p: simpleStyle ? 1 : 2 }}
spacing={simpleStyle ? 1 : 2}
>
{!simpleStyle && (
<>
<Box className="KT-transbox-select">
<Grid container spacing={simpleStyle ? 1 : 2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={(e) => {
setFromLang(e.target.value);
}}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={(e) => {
setToLang(e.target.value);
}}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
value={translator}
name="translator"
label={i18n("translate_service")}
onChange={(e) => {
setTranslator(e.target.value);
}}
>
{optApis.map(({ key, name }) => (
<MenuItem key={key} value={key}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
<Box className="KT-transbox-origin">
<TextField
size="small"
label={i18n("original_text")}
inputRef={inputRef}
fullWidth
multiline
value={editMode ? editText : text}
onChange={(e) => {
setEditText(e.target.value);
}}
onFocus={() => {
setEditMode(true);
setEditText(text);
}}
onBlur={() => {
setEditMode(false);
setText(editText.trim());
}}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
<DoneIcon fontSize="inherit" />
</IconButton>
) : (
<CopyBtn text={text} />
)}
</Stack>
),
}}
/>
</Box>
{editMode ? (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
}}
>
<DoneIcon fontSize="inherit" />
</IconButton>
) : (
<CopyBtn text={text} />
)}
</Stack>
),
}}
/>
</Box>
</>
)}
<TranCont
text={text}
translator={translator}
fromLang={fromLang}
toLang={toLang}
toLang2={toLang2}
setToLang={setToLang}
setToLang2={setToLang2}
transApis={transApis}
/>
{(!simpleStyle ||
!isValidWord(text) ||
!toLang.startsWith("zh") ||
enDict === "-") && (
<TranCont
text={text}
translator={translator}
fromLang={fromLang}
toLang={toLang}
toLang2={tranboxSetting.toLang2}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
/>
)}
{enDict !== "-" && (
<>
<DictCont text={text} />
<SugCont text={text} />
</>
)}
</Stack>
);
}
@@ -165,24 +296,49 @@ export default function TranBox({
setBoxSize,
boxPosition,
setBoxPosition,
simpleStyle,
setSimpleStyle,
hideClickAway,
setHideClickAway,
followSelection,
setFollowSelection,
extStyles,
langDetector,
enDict,
}) {
const [mouseHover, setMouseHover] = useState(false);
return (
<SettingProvider>
<ThemeProvider>
<ThemeProvider styles={extStyles}>
<DraggableResizable
defaultPosition={boxPosition}
defaultSize={boxSize}
header={<Header setShowPopup={setShowBox} />}
onChangeSize={setBoxSize}
onChangePosition={setBoxPosition}
position={boxPosition}
size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
header={
<Header
setShowPopup={setShowBox}
simpleStyle={simpleStyle}
setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
mouseHover={mouseHover}
/>
}
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)}
>
<Divider />
<TranForm
text={text}
setText={setText}
tranboxSetting={tranboxSetting}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
/>
</DraggableResizable>
</ThemeProvider>

View File

@@ -1,32 +1,33 @@
import { isMobile } from "../../libs/mobile";
import { limitNumber } from "../../libs/utils";
export default function TranBtn({ onClick, position, tranboxSetting }) {
const left = position.x + tranboxSetting.btnOffsetX;
const top = position.y + tranboxSetting.btnOffsetY;
const touchProps = isMobile
? {
onTouchEnd: onClick,
}
: {
onMouseUp: onClick,
};
export default function TranBtn({
onTrigger,
btnEvent,
position,
btnOffsetX,
btnOffsetY,
}) {
const left = limitNumber(position.x + btnOffsetX, 0, window.innerWidth - 32);
const top = limitNumber(position.y + btnOffsetY, 0, window.innerHeight - 32);
return (
<div
className="KT-tranbtn"
style={{
cursor: "pointer",
position: "absolute",
// position: "absolute",
position: "fixed",
left,
top,
zIndex: 2147483647,
}}
{...touchProps}
{...{ [btnEvent]: onTrigger }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
width={isMobile ? "32" : "20"}
height={isMobile ? "32" : "20"}
viewBox="0 0 32 32"
version="1.1"
>

View File

@@ -1,16 +1,15 @@
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import Alert from "@mui/material/Alert";
import CircularProgress from "@mui/material/CircularProgress";
import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
import { DEFAULT_TRANS_APIS } from "../../config";
import { useEffect, useState } from "react";
import { apiTranslate, apiBaiduLangdetect, apiBaiduSuggest } from "../../apis";
import { isValidWord } from "../../libs/utils";
import { apiTranslate } from "../../apis";
import CopyBtn from "./CopyBtn";
import DictCont from "./DictCont";
import SugCont from "./SugCont";
import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert";
import { tryDetectLang } from "../../libs";
export default function TranCont({
text,
@@ -18,16 +17,14 @@ export default function TranCont({
fromLang,
toLang,
toLang2 = "en",
setToLang,
setToLang2,
transApis,
simpleStyle,
langDetector,
}) {
const i18n = useI18n();
const [trText, setTrText] = useState("");
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null);
const [sugs, setSugs] = useState([]);
useEffect(() => {
(async () => {
@@ -35,100 +32,73 @@ export default function TranCont({
setLoading(true);
setTrText("");
setError("");
setDictResult(null);
setSugs([]);
// 互译
if (toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await apiBaiduLangdetect(text);
let to = toLang;
if (fromLang === "auto" && toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await tryDetectLang(text, true, langDetector);
if (detectLang === toLang) {
setToLang(toLang2);
setToLang2(toLang);
return;
to = toLang2;
}
}
// 翻译
const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator];
const tranRes = await apiTranslate({
text,
translator,
fromLang,
toLang,
toLang: to,
apiSetting,
});
setTrText(tranRes[0]);
// 词典
if (isValidWord(text) && toLang.startsWith("zh")) {
if (fromLang === "en" && translator === OPT_TRANS_BAIDU) {
tranRes[2].type === 1 &&
setDictResult(JSON.parse(tranRes[2].result));
} else {
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
dictRes[2].type === 1 &&
setDictResult(JSON.parse(dictRes[2].result));
}
}
// 建议
if (text.length < 20) {
setSugs(await apiBaiduSuggest(text));
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [
text,
translator,
fromLang,
toLang,
toLang2,
setToLang,
setToLang2,
transApis,
]);
}, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
if (simpleStyle) {
return (
<Box className="KT-transbox-target KT-transbox-target_simple">
{error ? (
<Alert severity="error">{error}</Alert>
) : loading ? (
<CircularProgress size={16} />
) : (
<Typography style={{ whiteSpace: "pre-line" }}>{trText}</Typography>
)}
</Box>
);
}
return (
<>
<Box>
<TextField
size="small"
label={i18n("translated_text")}
// disabled
fullWidth
multiline
value={trText}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
<CopyBtn text={trText} />
</Stack>
),
}}
/>
</Box>
{loading && <CircularProgress size={24} />}
{error && <Alert severity="error">{error}</Alert>}
{dictResult && <DictCont dictResult={dictResult} />}
{sugs.length > 0 && <SugCont sugs={sugs} />}
</>
<Box className="KT-transbox-target KT-transbox-target_default">
<TextField
size="small"
label={i18n("translated_text")}
// disabled
fullWidth
multiline
value={trText}
helperText={error}
InputProps={{
startAdornment: loading ? <CircularProgress size={16} /> : null,
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
<CopyBtn text={trText} />
</Stack>
),
}}
/>
</Box>
);
}

View File

@@ -1,21 +1,53 @@
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import TranBtn from "./TranBtn";
import TranBox from "./TranBox";
import { shortcutRegister } from "../../libs/shortcut";
import { sleep, limitNumber } from "../../libs/utils";
import { isGm, isExt } from "../../libs/client";
import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config";
import {
MSG_OPEN_TRANBOX,
DEFAULT_TRANBOX_SHORTCUT,
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT,
OPT_DICT_BAIDU,
} from "../../config";
import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
import { useLangMap } from "../../hooks/I18n";
export default function Slection({
contextMenuType,
tranboxSetting,
transApis,
uiLang,
langDetector,
}) {
const boxWidth = limitNumber(window.innerWidth, 300, 600);
const boxHeight = limitNumber(window.innerHeight, 200, 400);
const {
hideTranBtn = false,
simpleStyle: initSimpleStyle = false,
hideClickAway: initHideClickAway = false,
followSelection: initFollowMouse = false,
tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles,
btnOffsetX,
btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
enDict = OPT_DICT_BAIDU,
} = tranboxSetting;
const boxWidth =
isMobile || initSimpleStyle
? 300
: limitNumber(window.innerWidth, 300, 600);
const boxHeight =
isMobile || initSimpleStyle
? 200
: limitNumber(window.innerHeight, 200, 400);
const langMap = useLangMap(uiLang);
const [showBox, setShowBox] = useState(false);
const [showBtn, setShowBtn] = useState(false);
const [selectedText, setSelText] = useState("");
@@ -29,72 +61,113 @@ export default function Slection({
x: (window.innerWidth - boxWidth) / 2,
y: (window.innerHeight - boxHeight) / 2,
});
const [simpleStyle, setSimpleStyle] = useState(initSimpleStyle);
const [hideClickAway, setHideClickAway] = useState(initHideClickAway);
const [followSelection, setFollowSelection] = useState(initFollowMouse);
const handleClick = (e) => {
e.stopPropagation();
setShowBtn(false);
setText(selectedText);
setShowBox(true);
};
const handleTrigger = useCallback(
(text) => {
setShowBtn(false);
setText(text || selectedText);
setShowBox(true);
},
[selectedText]
);
const handleTranbox = useCallback(() => {
setShowBtn(false);
const selectedText = window.getSelection()?.toString()?.trim() || "";
const selection = window.getSelection();
const selectedText = selection?.toString()?.trim() || "";
if (!selectedText) {
setShowBox((pre) => !pre);
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);
setText(selectedText);
setShowBox(true);
}, []);
}, [followSelection, boxOffsetX, boxOffsetY]);
const btnEvent = useMemo(() => {
if (isMobile) {
return "onTouchEnd";
} else if (triggerMode === OPT_TRANBOX_TRIGGER_HOVER) {
return "onMouseOver";
}
return "onMouseUp";
}, [triggerMode]);
useEffect(() => {
async function handleMouseup(e) {
e.stopPropagation();
await sleep(10);
await sleep(200);
const selectedText = window.getSelection()?.toString()?.trim() || "";
const selection = window.getSelection();
const selectedText = selection?.toString()?.trim() || "";
setSelText(selectedText);
if (!selectedText) {
setShowBtn(false);
return;
}
const { pageX, pageY } = isMobile ? e.changedTouches[0] : e;
!tranboxSetting.hideTranBtn && setShowBtn(true);
// setPosition({ x: e.clientX, y: e.clientY });
setPosition({ x: pageX, y: pageY });
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) {
handleTrigger(selectedText);
return;
}
const { clientX, clientY } = isMobile ? e.changedTouches[0] : e;
setShowBtn(!hideTranBtn);
setPosition({ x: clientX, y: clientY });
}
// todo: mobile support
window.addEventListener("mouseup", handleMouseup);
// window.addEventListener(isMobile ? "touchend" : "mouseup", handleMouseup);
// window.addEventListener("mouseup", handleMouseup);
window.addEventListener(isMobile ? "touchend" : "mouseup", handleMouseup);
return () => {
window.removeEventListener(
isMobile ? "touchend" : "mouseup",
handleMouseup
);
};
}, [tranboxSetting.hideTranBtn]);
}, [
hideTranBtn,
triggerMode,
followSelection,
boxOffsetX,
boxOffsetY,
handleTrigger,
]);
useEffect(() => {
if (isExt) {
return;
}
const clearShortcut = shortcutRegister(
tranboxSetting.tranboxShortcut || DEFAULT_TRANBOX_SHORTCUT,
handleTranbox
);
const clearShortcut = shortcutRegister(tranboxShortcut, handleTranbox);
return () => {
clearShortcut();
};
}, [tranboxSetting.tranboxShortcut, handleTranbox]);
}, [tranboxShortcut, handleTranbox]);
useEffect(() => {
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
@@ -114,7 +187,7 @@ export default function Slection({
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
"Translate Selected Text",
langMap("translate_selected_text"),
(event) => {
handleTranbox();
},
@@ -130,10 +203,10 @@ export default function Slection({
} catch (err) {
kissLog(err, "registerMenuCommand");
}
}, [handleTranbox, contextMenuType]);
}, [handleTranbox, contextMenuType, langMap]);
useEffect(() => {
if (tranboxSetting.hideClickAway) {
if (hideClickAway) {
const handleHideBox = () => {
setShowBox(false);
};
@@ -142,7 +215,7 @@ export default function Slection({
window.removeEventListener("click", handleHideBox);
};
}
}, [tranboxSetting.hideClickAway]);
}, [hideClickAway]);
return (
<>
@@ -157,14 +230,28 @@ export default function Slection({
tranboxSetting={tranboxSetting}
transApis={transApis}
setShowBox={setShowBox}
simpleStyle={simpleStyle}
setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
extStyles={extStyles}
langDetector={langDetector}
enDict={enDict}
/>
)}
{showBtn && (
<TranBtn
position={position}
tranboxSetting={tranboxSetting}
onClick={handleClick}
btnOffsetX={btnOffsetX}
btnOffsetY={btnOffsetY}
btnEvent={btnEvent}
onTrigger={(e) => {
e.stopPropagation();
handleTrigger();
}}
/>
)}
</>