Compare commits

...

79 Commits

Author SHA1 Message Date
Gabe Yuan
71bbd2e54a v1.8.1 2024-02-06 10:09:33 +08:00
Gabe Yuan
3083d8e147 feat: context menu type 2024-02-05 11:28:34 +08:00
Gabe Yuan
e74883e9c2 fix: contextMenus: duplicate id err 2024-02-05 10:51:42 +08:00
Gabe Yuan
0816a9d167 fix: add BLOCKQUOTE to webfix 2024-02-05 10:02:30 +08:00
Gabe Yuan
4b3e91fa84 v1.8.1 2024-02-02 15:45:33 +08:00
Gabe Yuan
0973a0b60e fix: some js syntax 2024-02-02 15:44:44 +08:00
Gabe Yuan
de5f61126d fix: terms hepler text 2024-02-02 12:35:56 +08:00
Gabe Yuan
0c20ca761f fix: update toggle_translate CN text 2024-02-02 12:22:08 +08:00
Gabe Yuan
4bce56207e fix: optimize terms function 2024-02-02 12:10:27 +08:00
Gabe Yuan
dca54e0033 feat: setting: translate page title 2024-02-02 11:20:39 +08:00
Gabe Yuan
309646bf1d feat: setting: translate page title 2024-02-02 11:13:41 +08:00
Gabe Yuan
18b9961b39 fix: try add context menux on startup 2024-02-02 10:49:15 +08:00
Gabe Yuan
1e51ff17f2 v1.8.0 2024-01-22 13:19:37 +08:00
Gabe Yuan
63b5f707e2 fix: update ui when shortcut changed 2024-01-22 13:11:02 +08:00
Gabe Yuan
30efb6ee7a fix: title translate 2024-01-19 21:03:51 +08:00
Gabe Yuan
61b017618a feat: supported translation all when page opened 2024-01-19 17:55:18 +08:00
Gabe Yuan
1e0397adc9 feat: translate page title 2024-01-19 17:18:05 +08:00
Gabe Yuan
48b34bf95f fix: save new rule with hostname 2024-01-19 16:13:46 +08:00
Gabe Yuan
d5fc69e210 feat: support custom terms 2024-01-19 16:02:53 +08:00
Gabe Yuan
59f9dd697f fix: update baidu translate api 2024-01-18 15:26:37 +08:00
Gabe Yuan
c9d72323f1 keep selector support for sub-element 2024-01-12 16:04:34 +08:00
Gabe Yuan
e87f7f3abe fix: help text 2024-01-12 09:42:49 +08:00
Gabe Yuan
82ebbcb6d6 v1.7.16 2024-01-04 15:55:28 +08:00
Gabe Yuan
2db11070c5 fix: move clear_cache button to bottom of popup 2024-01-04 15:41:20 +08:00
Gabe Yuan
5efd2517e7 fix: tranbox shortcut in usserscript 2024-01-04 12:18:36 +08:00
Gabe Yuan
c0ba654678 fix: remove bgcolor input from popup 2024-01-04 10:49:44 +08:00
Gabe Yuan
546a5a549b fix: comment text 2024-01-04 10:39:40 +08:00
Gabe Yuan
cbf02c34e3 fix: remove position limit for tranbtn 2024-01-04 10:34:12 +08:00
Gabe Yuan
74a7258f10 fix: optimize key pick 2024-01-04 09:40:03 +08:00
Gabe Yuan
1006c044bc fix: update readme 2024-01-03 15:48:34 +08:00
Gabe
ef4ea719f3 fix: Update README.md 2024-01-03 15:47:24 +08:00
Gabe
8b34afe69f fix: Update README.md 2024-01-03 15:25:40 +08:00
Gabe Yuan
01292af298 feat: move open_tranbox shortcurt to browser commands 2024-01-03 13:10:02 +08:00
Gabe Yuan
cff8b2fe39 feat: move open_tranbox shortcurt to browser commands 2024-01-03 11:59:41 +08:00
Gabe Yuan
2cb20b5cc0 fix: update rules 2024-01-03 10:43:02 +08:00
Gabe Yuan
8f2aed18fe fix: contextMenus created on page and selection 2024-01-03 10:32:11 +08:00
Gabe Yuan
d85831cc9a fix: keep the translated image size unchanged 2024-01-03 10:10:54 +08:00
Gabe Yuan
55dc3a5556 feat: keep unchanged elements 2024-01-02 17:57:04 +08:00
Gabe Yuan
591afe08bd feat: keep unchanged elements 2024-01-02 17:55:59 +08:00
Gabe Yuan
748f2002ab fix: run webfix before translate 2023-12-27 15:44:02 +08:00
Gabe Yuan
d2d18a2384 fix: instagram input translate: addEventListener keyup 2023-12-27 11:25:53 +08:00
Gabe Yuan
35f4fa6aa7 fix: register menu command when hide fab button 2023-12-26 10:08:36 +08:00
Gabe Yuan
66fc2d22ed feat: toto: selection translation on mobile support 2023-12-25 17:25:00 +08:00
Gabe Yuan
16cf9ee1ed feat: toto: selection translation on mobile support 2023-12-25 17:21:59 +08:00
Gabe Yuan
d9d97bf14c fix: selection button position 2023-12-25 14:42:13 +08:00
Gabe Yuan
dc811bd3c7 feat: selection translation on mobile support 2023-12-25 11:50:30 +08:00
Gabe Yuan
b939d1849a feat: multi key calling support 2023-12-22 11:35:46 +08:00
Gabe Yuan
beca31f55d v1.7.15 2023-12-21 14:24:08 +08:00
Gabe Yuan
c7df103950 feat: add gemini translator 2023-12-21 14:15:14 +08:00
Gabe Yuan
4bf7972ad5 fix: fab button fontsize 2023-12-19 15:34:40 +08:00
Gabe Yuan
534eaed1ed refactor: input translate 2023-12-18 11:46:37 +08:00
Gabe Yuan
7e014e7385 fix: contextMenus setting 2023-12-15 11:43:01 +08:00
Gabe Yuan
34adb2660b fix: grant GM.unregisterMenuCommand 2023-12-15 10:58:49 +08:00
Gabe Yuan
b6bc165cf0 fix: context munus 2023-12-11 17:26:49 +08:00
Gabe Yuan
bdd5ed7fc7 feat: mutual translation effect with the target language 2023-12-11 15:54:54 +08:00
Gabe Yuan
95d19417c3 fix: touch tap limit 2023-12-11 11:40:20 +08:00
Gabe Yuan
30ebebdd71 fix: tranbtn position: absolute 2023-12-11 11:38:47 +08:00
Gabe Yuan
e9c557776d feat: context menus setting 2023-12-11 11:25:02 +08:00
Gabe Yuan
535a43b698 release: v1.7.14 2023-11-30 16:44:56 +08:00
Gabe Yuan
59752ed4aa fix: set FormControl small size 2023-11-30 15:11:06 +08:00
Gabe Yuan
b3e7b8f3f1 fix: readme 2023-11-28 15:15:02 +08:00
Gabe Yuan
c4e9365512 fix: clipboard.writeText run with async 2023-11-28 14:59:31 +08:00
Gabe Yuan
7d3972d3a8 perf: merge Translate Popup/Selected shortcut 2023-11-28 13:36:40 +08:00
Gabe Yuan
52ca4306fd feat: blockquote style 2023-11-28 11:41:45 +08:00
Gabe Yuan
da368ee612 feat: disable languages 2023-11-28 11:11:59 +08:00
Gabe Yuan
22c50e7765 feat: translate blacklist 2023-11-24 17:07:29 +08:00
Gabe Yuan
7bc39dd1bc fix: default shortcut: open setting page 2023-11-23 17:47:50 +08:00
Gabe Yuan
c80ead6116 v1.7.13 2023-11-22 15:24:54 +08:00
Gabe Yuan
67e76e4009 update readme 2023-11-22 15:08:26 +08:00
Gabe Yuan
b213218a30 update readme 2023-11-22 15:04:52 +08:00
Gabe Yuan
c629a1252c GM registerMenuCommand: open transbox, translate selected 2023-11-22 14:45:01 +08:00
Gabe Yuan
64d2481e93 update rules 2023-11-22 13:38:49 +08:00
Gabe Yuan
e7d6a6add8 update readme 2023-11-22 12:27:38 +08:00
Gabe Yuan
edc25f7da4 add touch option: four finger tap 2023-11-22 11:27:41 +08:00
Gabe Yuan
5bff84ace1 add context menus: open tranbox 2023-11-22 11:02:48 +08:00
Gabe Yuan
f8bfcba317 fix html fontsize 2023-11-22 10:23:14 +08:00
Gabe Yuan
013a05201b add context menus 2023-11-21 11:36:46 +08:00
Gabe Yuan
433e811821 add context menus 2023-11-21 11:20:05 +08:00
Gabe Yuan
df4cfc0fbc update readme 2023-11-17 11:38:42 +08:00
47 changed files with 1738 additions and 821 deletions

2
.env
View File

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

View File

@@ -1,6 +1,6 @@
# KISS Translator # KISS Translator
A simple [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator). A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
@@ -12,7 +12,7 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
- [x] Chrome/Edge/Firefox/Kiwi - [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari - [ ] Safari
- [x] Supports multiple translation services - [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] Custom translation interface - [x] Custom translation interface
- [x] Covers common translation scenarios - [x] Covers common translation scenarios
- [x] Web bilingual translation - [x] Web bilingual translation
@@ -26,12 +26,13 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
- [x] WebDAV - [x] WebDAV
- [x] Custom translation rules - [x] Custom translation rules
- [x] Rule subscription/rule sharing - [x] Rule subscription/rule sharing
- [x] Customized terminology
- [x] Custom translation style - [x] Custom translation style
- [x] Custom shortcut keys - [x] Custom shortcut keys
- `Alt+Q` Toggle Translation - `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles - `Alt+C` Toggle Styles
- `Alt+K` Open Setting Popup - `Alt+K` Open Setting Popup
- `Alt+B` Open Translate Popup - `Alt+S` Open Translate Popup / Translate Selected Text
- `Alt+O` Open Options Page - `Alt+O` Open Options Page
- `Alt+I` Input Box Translation - `Alt+I` Input Box Translation
@@ -39,8 +40,8 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
> Note: For the following reasons, it is recommended to use browser extensions first > Note: For the following reasons, it is recommended to use browser extensions first
> >
> - Browser extension can use local language recognition > - Browser extensions have more complete functions (local language recognition, context menu, etc.)
> - Grease Monkey script will encounter more usage problems > - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
- [x] Browser extension - [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN) - [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
@@ -84,3 +85,7 @@ pnpm build
## Discussion ## Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl) - Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
## Appreciate
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

View File

@@ -1,6 +1,6 @@
# 简约翻译 # 简约翻译
一个简约的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。 一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
@@ -12,7 +12,7 @@
- [x] Chrome/Edge/Firefox/Kiwi - [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari - [ ] Safari
- [x] 支持多种翻译服务 - [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] 自定义翻译接口 - [x] 自定义翻译接口
- [x] 覆盖常见翻译场景 - [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译 - [x] 网页双语对照翻译
@@ -26,12 +26,13 @@
- [x] WebDAV - [x] WebDAV
- [x] 自定义翻译规则 - [x] 自定义翻译规则
- [x] 规则订阅/规则分享 - [x] 规则订阅/规则分享
- [x] 自定义专业术语
- [x] 自定义译文样式 - [x] 自定义译文样式
- [x] 自定义快捷键 - [x] 自定义快捷键
- `Alt+Q` 启翻译 - `Alt+Q`翻译
- `Alt+C` 切换样式 - `Alt+C` 切换样式
- `Alt+K` 打开设置弹窗 - `Alt+K` 打开设置弹窗
- `Alt+B` 打开翻译弹窗 - `Alt+S` 打开翻译弹窗/翻译选中文字
- `Alt+O` 打开设置页面 - `Alt+O` 打开设置页面
- `Alt+I` 输入框翻译 - `Alt+I` 输入框翻译
@@ -39,8 +40,8 @@
> 注:基于以下原因,建议优先使用浏览器扩展 > 注:基于以下原因,建议优先使用浏览器扩展
> >
> - 浏览器扩展可以使用本地语言识别 > - 浏览器扩展的功能更完整(本地语言识别、右键菜单等)
> - 油猴脚本会遇到更多使用上的问题 > - 油猴脚本会遇到更多使用上的问题(跨域问题、脚本冲突等)
- [x] 浏览器扩展 - [x] 浏览器扩展
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN) - [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
@@ -65,7 +66,7 @@
- 针对一些特殊网站的修正脚本。 - 针对一些特殊网站的修正脚本。
- 以便翻译软件得到更好的展示效果。 - 以便翻译软件得到更好的展示效果。
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy) - 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。 - 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
- 自己部署,自己管理。 - 自己部署,自己管理。
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary) - 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- 搭配本项目一起使用的划词翻译插件。 - 搭配本项目一起使用的划词翻译插件。
@@ -84,3 +85,7 @@ pnpm build
## 交流 ## 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl) - 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
## 赞赏
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

View File

@@ -85,6 +85,7 @@ const userscriptWebpack = (config, env) => {
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL} // @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @grant GM.xmlHttpRequest // @grant GM.xmlHttpRequest
// @grant GM.registerMenuCommand // @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.setValue // @grant GM.setValue
// @grant GM.getValue // @grant GM.getValue
// @grant GM.deleteValue // @grant GM.deleteValue
@@ -97,6 +98,7 @@ const userscriptWebpack = (config, env) => {
// @connect api.deepl.com // @connect api.deepl.com
// @connect www2.deepl.com // @connect www2.deepl.com
// @connect api.openai.com // @connect api.openai.com
// @connect generativelanguage.googleapis.com
// @connect openai.azure.com // @connect openai.azure.com
// @connect workers.dev // @connect workers.dev
// @connect github.io // @connect github.io

View File

@@ -1,7 +1,7 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.7.12", "version": "1.8.2",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

@@ -13,5 +13,8 @@
}, },
"open_options": { "open_options": {
"message": "Open Options" "message": "Open Options"
},
"open_tranbox": {
"message": "Translate Popup/Selected"
} }
} }

View File

@@ -6,12 +6,15 @@
"message": "一个简约的双语对照翻译扩展 & 油猴脚本" "message": "一个简约的双语对照翻译扩展 & 油猴脚本"
}, },
"toggle_translate": { "toggle_translate": {
"message": "启翻译" "message": "启翻译"
}, },
"toggle_style": { "toggle_style": {
"message": "切换样式" "message": "切换样式"
}, },
"open_options": { "open_options": {
"message": "打开设置" "message": "打开设置"
},
"open_tranbox": {
"message": "翻译弹窗/选中文字"
} }
} }

View File

@@ -64,6 +64,45 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"> <div id="root">
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
<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 />
<br />
<br />
<br />
<br />
<br />
<br />
<div id="content"> <div id="content">
<p>You need to enable JavaScript to run <span>this app.</span></p> <p>You need to enable JavaScript to run <span>this app.</span></p>
The <span>embargo</span> has just lifted to confirm that AmpereOne is The <span>embargo</span> has just lifted to confirm that AmpereOne is

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.7.12", "version": "1.8.2",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -28,6 +28,12 @@
}, },
"description": "__MSG_toggle_translate__" "description": "__MSG_toggle_translate__"
}, },
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": { "toggleStyle": {
"suggested_key": { "suggested_key": {
"default": "Alt+C" "default": "Alt+C"
@@ -35,13 +41,10 @@
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
}, },
"openOptions": { "openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__" "description": "__MSG_open_options__"
} }
}, },
"permissions": ["<all_urls>", "storage"], "permissions": ["<all_urls>", "storage", "contextMenus"],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",
"32": "images/logo32.png", "32": "images/logo32.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.7.12", "version": "1.8.2",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -29,6 +29,12 @@
}, },
"description": "__MSG_toggle_translate__" "description": "__MSG_toggle_translate__"
}, },
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": { "toggleStyle": {
"suggested_key": { "suggested_key": {
"default": "Alt+C" "default": "Alt+C"
@@ -36,13 +42,10 @@
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
}, },
"openOptions": { "openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__" "description": "__MSG_open_options__"
} }
}, },
"permissions": ["storage"], "permissions": ["storage", "contextMenus"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",

View File

@@ -1,6 +1,10 @@
import queryString from "query-string"; import queryString from "query-string";
import { getBdauth, setBdauth } from "../libs/storage"; import { getBdauth, setBdauth } from "../libs/storage";
import { URL_BAIDU_WEB, URL_BAIDU_TRAN } from "../config"; import {
URL_BAIDU_WEB,
URL_BAIDU_TRANSAPI_V2,
URL_BAIDU_TRANSAPI,
} from "../config";
import { fetchApi } from "../libs/fetch"; import { fetchApi } from "../libs/fetch";
/* eslint-disable */ /* eslint-disable */
@@ -203,7 +207,12 @@ const _bdAuth = () => {
const bdAuth = _bdAuth(); const bdAuth = _bdAuth();
export const genBaidu = async ({ text, from, to }) => { /**
* 失效作废
* @param {*} param0
* @returns
*/
export const genBaiduV2 = async ({ text, from, to }) => {
const { token, gtk } = await bdAuth(); const { token, gtk } = await bdAuth();
const sign = getSign(text, gtk); const sign = getSign(text, gtk);
const data = { const data = {
@@ -217,7 +226,7 @@ export const genBaidu = async ({ text, from, to }) => {
ts: Date.now(), ts: Date.now(),
}; };
const input = `${URL_BAIDU_TRAN}?from=${from}&to=${to}`; const input = `${URL_BAIDU_TRANSAPI_V2}?from=${from}&to=${to}`;
const init = { const init = {
headers: { headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8", "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
@@ -228,3 +237,22 @@ export const genBaidu = async ({ text, from, to }) => {
return [input, init]; return [input, init];
}; };
export const genBaidu = async ({ text, from, to }) => {
const data = {
from,
to,
query: text,
source: "txt",
};
const init = {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: queryString.stringify(data),
};
return [URL_BAIDU_TRANSAPI, init];
};

View File

@@ -9,6 +9,7 @@ import {
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
URL_CACHE_TRAN, URL_CACHE_TRAN,
@@ -124,11 +125,14 @@ export const apiTranslate = async ({
return [trText, isSame]; return [trText, isSame];
} }
// 版本号一/二位升级,旧缓存失效
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
const cacheOpts = { const cacheOpts = {
translator, translator,
text, text,
fromLang, fromLang,
toLang, toLang,
version: [v1, v2].join("."),
}; };
const transOpts = { const transOpts = {
@@ -154,7 +158,9 @@ export const apiTranslate = async ({
isSame = to === res.src; isSame = to === res.src;
break; break;
case OPT_TRANS_MICROSOFT: case OPT_TRANS_MICROSOFT:
trText = res[0].translations.map((item) => item.text).join(" "); trText = res
.map((item) => item.translations.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_DEEPL: case OPT_TRANS_DEEPL:
@@ -170,15 +176,28 @@ export const apiTranslate = async ({
isSame = to === res.source_lang; isSame = to === res.source_lang;
break; break;
case OPT_TRANS_BAIDU: case OPT_TRANS_BAIDU:
trText = res.trans_result?.data.map((item) => item.dst).join(" "); // trText = res.trans_result?.data.map((item) => item.dst).join(" ");
isSame = res.trans_result?.to === res.trans_result?.from; // isSame = res.trans_result?.to === res.trans_result?.from;
if (res.type === 1) {
trText = Object.keys(JSON.parse(res.result).content[0].mean[0].cont)[0];
isSame = to === res.from;
} else if (res.type === 2) {
trText = res.data.map((item) => item.dst).join(" ");
isSame = to === res.from;
}
break; break;
case OPT_TRANS_TENCENT: case OPT_TRANS_TENCENT:
trText = res.auto_translation; trText = res.auto_translation;
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_OPENAI: case OPT_TRANS_OPENAI:
trText = res?.choices?.[0].message.content; trText = res?.choices?.map((item) => item.message.content).join(" ");
isSame = text === trText;
break;
case OPT_TRANS_GEMINI:
trText = res?.candidates
?.map((item) => item.content.parts.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:

View File

@@ -7,9 +7,13 @@ import {
MSG_OPEN_OPTIONS, MSG_OPEN_OPTIONS,
MSG_SAVE_RULE, MSG_SAVE_RULE,
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
MSG_OPEN_TRANBOX,
MSG_CONTEXT_MENUS,
MSG_COMMAND_SHORTCUTS,
CMD_TOGGLE_TRANSLATE, CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE, CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS, CMD_OPEN_OPTIONS,
CMD_OPEN_TRANBOX,
} from "./config"; } from "./config";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage"; import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync"; import { trySyncSettingAndRules } from "./libs/sync";
@@ -21,30 +25,87 @@ import { saveRule } from "./libs/rules";
globalThis.ContextType = "BACKGROUND"; globalThis.ContextType = "BACKGROUND";
/**
* 添加右键菜单
*/
async function addContextMenus(contextMenuType = 1) {
// 添加前先删除,避免重复ID的错误
try {
await browser.contextMenus.removeAll();
} catch (err) {
//
}
switch (contextMenuType) {
case 1:
browser.contextMenus.create({
id: CMD_TOGGLE_TRANSLATE,
title: browser.i18n.getMessage("toggle_translate"),
contexts: ["page", "selection"],
});
break;
case 2:
browser.contextMenus.create({
id: CMD_TOGGLE_TRANSLATE,
title: browser.i18n.getMessage("toggle_translate"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_TOGGLE_STYLE,
title: browser.i18n.getMessage("toggle_style"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_TRANBOX,
title: browser.i18n.getMessage("open_tranbox"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: "options_separator",
type: "separator",
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_OPTIONS,
title: browser.i18n.getMessage("open_options"),
contexts: ["page", "selection"],
});
break;
default:
}
}
/** /**
* 插件安装 * 插件安装
*/ */
browser.runtime.onInstalled.addListener(() => { browser.runtime.onInstalled.addListener(() => {
tryInitDefaultData(); tryInitDefaultData();
// 右键菜单
addContextMenus();
}); });
/** /**
* 浏览器启动 * 浏览器启动
*/ */
browser.runtime.onStartup.addListener(async () => { browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup");
// 同步数据 // 同步数据
await trySyncSettingAndRules(); await trySyncSettingAndRules();
const { clearCache, contextMenuType, subrulesList } =
await getSettingWithDefault();
// 清除缓存 // 清除缓存
const setting = await getSettingWithDefault(); if (clearCache) {
if (setting.clearCache) {
tryClearCaches(); tryClearCaches();
} }
// 右键菜单
// firefox重启后菜单会消失,故重复添加
addContextMenus(contextMenuType);
// 同步订阅规则 // 同步订阅规则
trySyncAllSubRules(setting); trySyncAllSubRules({ subrulesList });
}); });
/** /**
@@ -78,6 +139,20 @@ browser.runtime.onMessage.addListener(
case MSG_SAVE_RULE: case MSG_SAVE_RULE:
saveRule(args); saveRule(args);
break; break;
case MSG_CONTEXT_MENUS:
const { contextMenuType } = args;
addContextMenus(contextMenuType);
break;
case MSG_COMMAND_SHORTCUTS:
browser.commands
.getAll()
.then((commands) => {
sendResponse({ data: commands });
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
default: default:
sendResponse({ error: `message action is unavailable: ${action}` }); sendResponse({ error: `message action is unavailable: ${action}` });
} }
@@ -94,6 +169,9 @@ browser.commands.onCommand.addListener((command) => {
case CMD_TOGGLE_TRANSLATE: case CMD_TOGGLE_TRANSLATE:
sendTabMsg(MSG_TRANS_TOGGLE); sendTabMsg(MSG_TRANS_TOGGLE);
break; break;
case CMD_OPEN_TRANBOX:
sendTabMsg(MSG_OPEN_TRANBOX);
break;
case CMD_TOGGLE_STYLE: case CMD_TOGGLE_STYLE:
sendTabMsg(MSG_TRANS_TOGGLE_STYLE); sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
break; break;
@@ -103,3 +181,24 @@ browser.commands.onCommand.addListener((command) => {
default: default:
} }
}); });
/**
* 监听右键菜单
*/
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
switch (menuItemId) {
case CMD_TOGGLE_TRANSLATE:
sendTabMsg(MSG_TRANS_TOGGLE);
break;
case CMD_TOGGLE_STYLE:
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case CMD_OPEN_TRANBOX:
sendTabMsg(MSG_OPEN_TRANBOX);
break;
case CMD_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
default:
}
});

View File

@@ -8,27 +8,80 @@ import {
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE, MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
MSG_OPEN_TRANBOX,
APP_LCNAME, APP_LCNAME,
DEFAULT_TRANBOX_SETTING, DEFAULT_TRANBOX_SETTING,
} from "./config"; } from "./config";
import { getRulesWithDefault, getFabWithDefault } from "./libs/storage"; import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator"; import { Translator } from "./libs/translator";
import { sendIframeMsg, sendParentMsg } from "./libs/iframe"; import { isIframe, sendIframeMsg, sendParentMsg } from "./libs/iframe";
import { matchRule } from "./libs/rules";
import Slection from "./views/Selection"; import Slection from "./views/Selection";
import { touchTapListener } from "./libs/touch"; import { touchTapListener } from "./libs/touch";
import { debounce } from "./libs/utils"; import { debounce, genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm";
import { browser } from "./libs/browser";
import { matchFixer } from "./libs/webfix";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist";
import inputTranslate from "./libs/inputTranslate";
export async function runTranslator(setting) { /**
const href = document.location.href; * 油猴脚本设置页面
const rules = await getRulesWithDefault(); */
const rule = await matchRule(rules, href, setting); function runSettingPage() {
const translator = new Translator(rule, setting); if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM;
return { translator, rule }; unsafeWindow.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
};
} else {
const ping = genEventName();
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
} }
export function runIframe(setting) { /**
* 插件监听后端事件
* @param {*} translator
*/
function runtimeListener(translator) {
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break;
case MSG_OPEN_TRANBOX:
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
});
}
/**
* iframe 页面执行
* @param {*} setting
*/
function runIframe(setting) {
let translator; let translator;
window.addEventListener("message", (e) => { window.addEventListener("message", (e) => {
const { action, args } = e.data || {}; const { action, args } = e.data || {};
@@ -52,12 +105,13 @@ export function runIframe(setting) {
sendParentMsg(MSG_TRANS_GETRULE); sendParentMsg(MSG_TRANS_GETRULE);
} }
export async function showFab(translator) { /**
* 悬浮按钮
* @param {*} translator
* @returns
*/
async function showFab(translator) {
const fab = await getFabWithDefault(); const fab = await getFabWithDefault();
if (fab.isHide) {
return;
}
const $action = document.createElement("div"); const $action = document.createElement("div");
$action.setAttribute("id", APP_LCNAME); $action.setAttribute("id", APP_LCNAME);
document.body.parentElement.appendChild($action); document.body.parentElement.appendChild($action);
@@ -80,7 +134,13 @@ export async function showFab(translator) {
); );
} }
export function showTransbox({ /**
* 划词翻译
* @param {*} param0
* @returns
*/
function showTransbox({
contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING, tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis, transApis,
}) { }) {
@@ -104,13 +164,21 @@ export function showTransbox({
ReactDOM.createRoot(shadowRootElement).render( ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode> <React.StrictMode>
<CacheProvider value={cache}> <CacheProvider value={cache}>
<Slection tranboxSetting={tranboxSetting} transApis={transApis} /> <Slection
contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting}
transApis={transApis}
/>
</CacheProvider> </CacheProvider>
</React.StrictMode> </React.StrictMode>
); );
} }
export function windowListener(rule) { /**
* 监听来自iframe页面消息
* @param {*} rule
*/
function windowListener(rule) {
window.addEventListener("message", (e) => { window.addEventListener("message", (e) => {
const { action } = e.data || {}; const { action } = e.data || {};
switch (action) { switch (action) {
@@ -122,14 +190,23 @@ export function windowListener(rule) {
}); });
} }
export function showErr(message) { /**
* 显示错误信息到页面顶部
* @param {*} message
*/
function showErr(message) {
const $err = document.createElement("div"); const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${message}`; $err.innerText = `KISS-Translator: ${message}`;
$err.style.cssText = "background:red; color:#fff;"; $err.style.cssText = "background:red; color:#fff;";
document.body.prepend($err); document.body.prepend($err);
} }
export function touchOperation(translator) { /**
* 监听触屏操作
* @param {*} translator
* @returns
*/
function touchOperation(translator) {
const { touchTranslate = 2 } = translator.setting; const { touchTranslate = 2 } = translator.setting;
if (touchTranslate === 0) { if (touchTranslate === 0) {
return; return;
@@ -141,3 +218,66 @@ export function touchOperation(translator) {
}); });
touchTapListener(handleTap, touchTranslate); touchTapListener(handleTap, touchTranslate);
} }
/**
* 入口函数
*/
export async function run(isUserscript = false) {
try {
const href = document.location.href;
// 设置页面
if (
isUserscript &&
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
href.includes(process.env.REACT_APP_OPTIONSPAGE2))
) {
runSettingPage();
return;
}
// 读取设置信息
const setting = await getSettingWithDefault();
// 黑名单
if (isInBlacklist(href, setting)) {
return;
}
// 适配iframe
if (isIframe) {
runIframe(setting);
return;
}
// 不规范网页修复
const fixerSetting = await matchFixer(href, setting);
// 翻译网页
const rule = await matchRule(href, setting);
const translator = new Translator(rule, setting, fixerSetting);
// 监听消息
windowListener(rule);
!isUserscript && runtimeListener(translator);
// 输入框翻译
inputTranslate(setting);
// 划词翻译
showTransbox(setting);
// 浮球按钮
await showFab(translator);
// 触屏操作
touchOperation(translator);
// 同步订阅规则
isUserscript && (await trySyncAllSubRules(setting));
} catch (err) {
console.error("[KISS-Translator]", err);
showErr(err.message);
}
}

View File

@@ -112,8 +112,8 @@ export const I18N = {
en: customApiHelpEN, en: customApiHelpEN,
}, },
translate_alt: { translate_alt: {
zh: `翻译 (Alt+Q)`, zh: `翻译`,
en: `Translate (Alt+Q)`, en: `Translate`,
}, },
basic_setting: { basic_setting: {
zh: `基本设置`, zh: `基本设置`,
@@ -183,13 +183,17 @@ export const I18N = {
zh: `翻译服务`, zh: `翻译服务`,
en: `Translate Service`, en: `Translate Service`,
}, },
mouseover_translation: { translate_timing: {
zh: `鼠标悬停翻译`, zh: `翻译时机`,
en: `Mouseover translation`, en: `Translate Timing`,
}, },
mk_disable: { mk_disable: {
zh: `禁用`, zh: `滚动加载(建议)`,
en: `Disable`, en: `Rolling Loading (Suggested)`,
},
mk_pageopen: {
zh: `页面打开`,
en: `Page Open`,
}, },
mk_mouseover: { mk_mouseover: {
zh: `鼠标悬停`, zh: `鼠标悬停`,
@@ -215,13 +219,21 @@ export const I18N = {
zh: `目标语言`, zh: `目标语言`,
en: `Target Language`, en: `Target Language`,
}, },
to_lang2: {
zh: `第二目标语言`,
en: `Target Language 2`,
},
to_lang2_helper: {
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
},
text_style: { text_style: {
zh: `文字样式`, zh: `文字样式`,
en: `Text Style`, en: `Text Style`,
}, },
text_style_alt: { text_style_alt: {
zh: `文字样式 (Alt+C)`, zh: `文字样式`,
en: `Text Style (Alt+C)`, en: `Text Style`,
}, },
bg_color: { bg_color: {
zh: `样式颜色`, zh: `样式颜色`,
@@ -335,6 +347,10 @@ export const I18N = {
zh: `高亮`, zh: `高亮`,
en: `Highlight`, en: `Highlight`,
}, },
blockquote: {
zh: `引用`,
en: `Blockquote`,
},
diy_style: { diy_style: {
zh: `自定义样式`, zh: `自定义样式`,
en: `Custom Style`, en: `Custom Style`,
@@ -344,23 +360,23 @@ export const I18N = {
en: `Follow the syntax of "CSS"`, en: `Follow the syntax of "CSS"`,
}, },
setting: { setting: {
zh: `设置 (Alt+O)`, zh: `设置`,
en: `Setting (Alt+O)`, en: `Setting`,
}, },
pattern: { pattern: {
zh: `匹配网址`, zh: `匹配网址`,
en: `URL pattern`, en: `URL pattern`,
}, },
pattern_helper: { pattern_helper: {
zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔。`, zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔。`,
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`, en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
}, },
selector_helper: { selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`, zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`, en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
}, },
translate_switch: { translate_switch: {
zh: `启翻译`, zh: `翻译`,
en: `Translate Switch`, en: `Translate Switch`,
}, },
default_enabled: { default_enabled: {
@@ -375,6 +391,22 @@ export const I18N = {
zh: `选择器`, zh: `选择器`,
en: `Selector`, en: `Selector`,
}, },
keep_selector: {
zh: `保留元素选择器`,
en: `Keep unchanged selector`,
},
keep_selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、子元素选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3.Sub-element selectors are separated by ">>>".`,
},
terms: {
zh: `专业术语`,
en: `Terms`,
},
terms_helper: {
zh: `0、支持正则表达式匹配。1、多条术语用换行或分号“;”隔开。2、术语和译文用英文逗号“,”隔开。3、没有译文视为不翻译术语。4、留空表示采用全局设置。`,
en: `0. Supports regular expression matching. 1. Separate multiple terms with newlines or semicolons ";". 2. Terms and translations are separated by English commas ",". 3. If there is no translation, the term will be deemed not to be translated. 4. Leave blank to adopt the global setting.`,
},
root_selector: { root_selector: {
zh: `根选择器`, zh: `根选择器`,
en: `Root Selector`, en: `Root Selector`,
@@ -528,7 +560,7 @@ export const I18N = {
en: `Shortcuts Setting`, en: `Shortcuts Setting`,
}, },
toggle_translate_shortcut: { toggle_translate_shortcut: {
zh: `"启翻译"快捷键`, zh: `"启翻译"快捷键`,
en: `"Toggle Translate" Shortcut`, en: `"Toggle Translate" Shortcut`,
}, },
toggle_style_shortcut: { toggle_style_shortcut: {
@@ -547,6 +579,10 @@ export const I18N = {
zh: `隐藏悬浮按钮`, zh: `隐藏悬浮按钮`,
en: `Hide Fab Button`, en: `Hide Fab Button`,
}, },
hide_tran_button: {
zh: `隐藏翻译按钮`,
en: `Hide Translate Button`,
},
show: { show: {
zh: `显示`, zh: `显示`,
en: `Show`, en: `Show`,
@@ -628,8 +664,8 @@ export const I18N = {
en: `Use Selection Translate`, en: `Use Selection Translate`,
}, },
trigger_tranbox_shortcut: { trigger_tranbox_shortcut: {
zh: `显示翻译框快捷键`, zh: `显示翻译框/翻译选中文字快捷键`,
en: `Toggle Translate Box Shortcut`, en: `Open Translate Popup/Translate Selected Shortcut`,
}, },
tranbtn_offset_x: { tranbtn_offset_x: {
zh: `翻译按钮偏移X0-100`, zh: `翻译按钮偏移X0-100`,
@@ -671,4 +707,44 @@ export const I18N = {
zh: `三指轻触`, zh: `三指轻触`,
en: `Three finger tap`, en: `Three finger tap`,
}, },
touch_tap_4: {
zh: `四指轻触`,
en: `Four finger tap`,
},
translate_blacklist: {
zh: `禁用翻译名单`,
en: `Translate Blacklist`,
},
disable_langs: {
zh: `不翻译的语言`,
en: `Disable Languages`,
},
disable_langs_helper: {
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
},
context_menus: {
zh: `右键菜单`,
en: `Context Menus`,
},
hide_context_menus: {
zh: `隐藏右键菜单`,
en: `Hide Context Menus`,
},
simple_context_menus: {
zh: `简单右键菜单`,
en: `Simple_context_menus Context Menus`,
},
secondary_context_menus: {
zh: `二级右键菜单`,
en: `Secondary Context Menus`,
},
mulkeys_help: {
zh: `支持用换行或英文逗号“,”分隔多个KEY轮询调用。`,
en: `Supports multiple KEY polling calls separated by newlines or English commas ",".`,
},
translate_page_title: {
zh: `是否同时翻译页面标题`,
en: `Translate Page Title`,
},
}; };

View File

@@ -1,5 +1,6 @@
import { import {
DEFAULT_SELECTOR, DEFAULT_SELECTOR,
DEFAULT_KEEP_SELECTOR,
GLOBAL_KEY, GLOBAL_KEY,
REMAIN_KEY, REMAIN_KEY,
SHADOW_KEY, SHADOW_KEY,
@@ -33,6 +34,7 @@ export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate"; export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle"; export const CMD_TOGGLE_STYLE = "toggleStyle";
export const CMD_OPEN_OPTIONS = "openOptions"; export const CMD_OPEN_OPTIONS = "openOptions";
export const CMD_OPEN_TRANBOX = "openTranbox";
export const CLIENT_WEB = "web"; export const CLIENT_WEB = "web";
export const CLIENT_CHROME = "chrome"; export const CLIENT_CHROME = "chrome";
@@ -58,9 +60,12 @@ export const MSG_OPEN_OPTIONS = "open_options";
export const MSG_SAVE_RULE = "save_rule"; export const MSG_SAVE_RULE = "save_rule";
export const MSG_TRANS_TOGGLE = "trans_toggle"; export const MSG_TRANS_TOGGLE = "trans_toggle";
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style"; export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
export const MSG_OPEN_TRANBOX = "open_tranbox";
export const MSG_TRANS_GETRULE = "trans_getrule"; export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule"; export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule"; export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_CONTEXT_MENUS = "context_menus";
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const THEME_LIGHT = "light"; export const THEME_LIGHT = "light";
export const THEME_DARK = "dark"; export const THEME_DARK = "dark";
@@ -79,7 +84,8 @@ export const URL_MICROSOFT_TRAN =
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth"; export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect"; export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/"; export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
export const URL_BAIDU_TRAN = "https://fanyi.baidu.com/v2transapi"; 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_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt"; export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
@@ -91,17 +97,19 @@ export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_BAIDU = "Baidu"; export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent"; export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_OPENAI = "OpenAI"; export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_GEMINI = "Gemini";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI"; export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_CUSTOMIZE = "Custom"; export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_ALL = [ export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX, OPT_TRANS_DEEPLX,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
]; ];
@@ -224,6 +232,9 @@ export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_OPENAI]: new Map( [OPT_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
), ),
[OPT_TRANS_GEMINI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CLOUDFLAREAI]: new Map([ [OPT_TRANS_CLOUDFLAREAI]: new Map([
["auto", ""], ["auto", ""],
["zh-CN", "chinese"], ["zh-CN", "chinese"],
@@ -265,6 +276,7 @@ export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线 export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊 export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮 export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式 export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
export const OPT_STYLE_ALL = [ export const OPT_STYLE_ALL = [
OPT_STYLE_NONE, OPT_STYLE_NONE,
@@ -274,6 +286,7 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
OPT_STYLE_DIY, OPT_STYLE_DIY,
]; ];
export const OPT_STYLE_USE_COLOR = [ export const OPT_STYLE_USE_COLOR = [
@@ -282,15 +295,18 @@ export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_HIGHLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
]; ];
export const OPT_MOUSEKEY_DISABLE = "mk_disable"; export const OPT_MOUSEKEY_DISABLE = "mk_disable"; // 滚动加载翻译
export const OPT_MOUSEKEY_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover"; export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey"; export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey"; export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
export const OPT_MOUSEKEY_ALT = "mk_altKey"; export const OPT_MOUSEKEY_ALT = "mk_altKey";
export const OPT_MOUSEKEY_ALL = [ export const OPT_MOUSEKEY_ALL = [
OPT_MOUSEKEY_DISABLE, OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_PAGEOPEN,
OPT_MOUSEKEY_MOUSEOVER, OPT_MOUSEKEY_MOUSEOVER,
OPT_MOUSEKEY_CONTROL, OPT_MOUSEKEY_CONTROL,
OPT_MOUSEKEY_SHIFT, OPT_MOUSEKEY_SHIFT,
@@ -302,6 +318,7 @@ export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符 export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符 export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
export const PROMPT_PLACE_TEXT = "{{text}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色 export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
@@ -309,6 +326,8 @@ export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const GLOBLA_RULE = { export const GLOBLA_RULE = {
pattern: "*", pattern: "*",
selector: DEFAULT_SELECTOR, selector: DEFAULT_SELECTOR,
keepSelector: DEFAULT_KEEP_SELECTOR,
terms: "",
translator: OPT_TRANS_MICROSOFT, translator: OPT_TRANS_MICROSOFT,
fromLang: "auto", fromLang: "auto",
toLang: "zh-CN", toLang: "zh-CN",
@@ -333,15 +352,17 @@ export const DEFAULT_INPUT_RULE = {
}; };
// 划词翻译 // 划词翻译
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyB"]; export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
export const DEFAULT_TRANBOX_SETTING = { export const DEFAULT_TRANBOX_SETTING = {
transOpen: true, transOpen: true,
translator: OPT_TRANS_MICROSOFT, translator: OPT_TRANS_MICROSOFT,
fromLang: "auto", fromLang: "auto",
toLang: "zh-CN", toLang: "zh-CN",
toLang2: "en",
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT, tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10, btnOffsetX: 10,
btnOffsetY: 10, btnOffsetY: 10,
hideTranBtn: false,
}; };
// 订阅列表 // 订阅列表
@@ -380,6 +401,12 @@ export const DEFAULT_TRANS_APIS = {
model: "gpt-4", 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}.`, prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
}, },
[OPT_TRANS_GEMINI]: {
url: "https://generativelanguage.googleapis.com/v1/models",
key: "",
model: "gemini-pro",
prompt: `Translate the following text from ${PROMPT_PLACE_FROM} to ${PROMPT_PLACE_TO}:\n\n${PROMPT_PLACE_TEXT}`,
},
[OPT_TRANS_CLOUDFLAREAI]: { [OPT_TRANS_CLOUDFLAREAI]: {
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b", url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
key: "", key: "",
@@ -399,12 +426,19 @@ export const DEFAULT_SHORTCUTS = {
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"], [OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"], [OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"], [OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyN"], [OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
}; };
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度 export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度 export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数 export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
export const DEFAULT_BLACKLIST = [
"https://fishjar.github.io/kiss-translator/options.html",
"https://translate.google.com",
"https://www.deepl.com/translator",
"oapi.dingtalk.com",
"login.dingtalk.com",
]; // 禁用翻译名单
export const DEFAULT_SETTING = { export const DEFAULT_SETTING = {
darkMode: false, // 深色模式 darkMode: false, // 深色模式
@@ -418,14 +452,19 @@ export const DEFAULT_SETTING = {
injectRules: true, // 是否注入订阅规则 injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁 injectWebfix: true, // 是否注入修复补丁
detectRemote: false, // 是否使用远程语言检测 detectRemote: false, // 是否使用远程语言检测
contextMenus: true, // 是否添加右键菜单(作废)
contextMenuType: 1, // 右键菜单类型(0不显示1简单菜单2多级菜单)
transTitle: false, // 是否同时翻译页面标题
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表 subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则 owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
transApis: DEFAULT_TRANS_APIS, // 翻译接口 transApis: DEFAULT_TRANS_APIS, // 翻译接口
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译 mouseKey: OPT_MOUSEKEY_DISABLE, // 翻译时机/鼠标悬停翻译
shortcuts: DEFAULT_SHORTCUTS, // 快捷键 shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置 inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置 tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
touchTranslate: 2, // 触屏翻译 touchTranslate: 2, // 触屏翻译
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
disableLangs: [], // 不翻译的语言
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -1,6 +1,5 @@
const els = `li, p, h1, h2, h3, h4, h5, h6, dd, blockquote`; export const DEFAULT_SELECTOR = `:is(li, p, h1, h2, h3, h4, h5, h6, dd, blockquote)`;
export const DEFAULT_KEEP_SELECTOR = `code, img, svg`;
export const DEFAULT_SELECTOR = `:is(${els})`;
export const GLOBAL_KEY = "*"; export const GLOBAL_KEY = "*";
export const REMAIN_KEY = "-"; export const REMAIN_KEY = "-";
@@ -10,6 +9,8 @@ export const SHADOW_KEY = ">>>";
export const DEFAULT_RULE = { export const DEFAULT_RULE = {
pattern: "", pattern: "",
selector: "", selector: "",
keepSelector: "",
terms: "",
translator: GLOBAL_KEY, translator: GLOBAL_KEY,
fromLang: GLOBAL_KEY, fromLang: GLOBAL_KEY,
toLang: GLOBAL_KEY, toLang: GLOBAL_KEY,
@@ -42,145 +43,156 @@ export const DEFAULT_OW_RULE = {
textDiyStyle: DEFAULT_DIY_STYLE, textDiyStyle: DEFAULT_DIY_STYLE,
}; };
const RULES = [ const RULES_MAP = {
{ "www.google.com/search": [`h3, .IsZvec, .VwiC3b`],
pattern: `www.google.com/search`, "news.google.com": [`[role="link"], .DY5T1d, .ifw3f, ${DEFAULT_SELECTOR}`],
selector: `h3, .IsZvec, .VwiC3b`, "www.foxnews.com": [
}, `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
{ ],
pattern: `news.google.com`, "bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": [
selector: `h4`, `${DEFAULT_SELECTOR}`,
}, ],
{ "themessenger.com": [
pattern: `www.foxnews.com`, `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`, ],
}, "www.telegraph.co.uk, go.dev/doc/": [`article ${DEFAULT_SELECTOR}`],
{ "www.theguardian.com": [
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`, `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
selector: DEFAULT_SELECTOR, ],
}, "www.semafor.com": [
{ `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
pattern: `themessenger.com`, ],
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`, "www.noemamag.com": [
}, `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
{ ],
pattern: `www.telegraph.co.uk`, "restofworld.org": [
selector: `article ${DEFAULT_SELECTOR}`, `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
}, ],
{ "www.axios.com": [`.h7, ${DEFAULT_SELECTOR}`],
pattern: `www.theguardian.com`, "www.newyorker.com": [
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`, `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`,
}, ],
{ "time.com": [
pattern: `www.semafor.com`, `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`, ],
}, "www.dw.com": [
{ `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
pattern: `www.noemamag.com`, ],
selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`, "www.bbc.com": [
}, `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`,
{ ],
pattern: `restofworld.org`, "www.chinadaily.com.cn": [
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`, `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
}, ],
{ "www.facebook.com": [`[role="main"] [dir="auto"]`],
pattern: `www.axios.com`, "www.reddit.com": [
selector: `.h7, ${DEFAULT_SELECTOR}`, `div:is(.tbIApBd2DM_drfZQJjIum, ._1zPvgKHteTOub9dKkvrOl4,.ULWj94BYSOqoJDetxgcnU),a:is([class^="_334yl59"],[class^="_2GrMpxD"]),h1,h2,h3,h4,h5,h6,p,button`,
}, ],
{ "www.quora.com": [`.qu-wordBreak--break-word`],
pattern: `www.newyorker.com`, "edition.cnn.com": [
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO`, `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
}, ],
{ "www.reuters.com": [
pattern: `https://time.com/`, `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`, ],
}, "www.bloomberg.com": [
{ `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
pattern: `www.dw.com`, ],
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`, "deno.land, docs.github.com": [`main ${DEFAULT_SELECTOR}`, `code, img, svg`],
}, "doc.rust-lang.org": [`.content ${DEFAULT_SELECTOR}`, `code, img, svg`],
{ "www.indiehackers.com": [
pattern: `www.bbc.com`, `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-1mrs5ns-PromoLink, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro`, ],
}, "platform.openai.com/docs": [
{ `.docs-body ${DEFAULT_SELECTOR}`,
pattern: `www.chinadaily.com.cn`, `code, img, svg`,
selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`, ],
}, "en.wikipedia.org": [
{ `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
pattern: `www.facebook.com`, `.mwe-math-element`,
selector: `[role="main"] [dir="auto"]`, ],
}, "stackoverflow.com": [
{ `h1, .s-prose p, .comment-body .comment-copy`,
pattern: `www.reddit.com`, `code, img, svg`,
selector: `[slot="title"], [slot="text-body"] ${DEFAULT_SELECTOR}, #-post-rtjson-content p`, ],
}, "www.npmjs.com/package, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org":
{ [`article ${DEFAULT_SELECTOR}`],
pattern: `www.quora.com`, "news.ycombinator.com": [`.title, .commtext`],
selector: `.qu-wordBreak--break-word`, "github.com": [
}, `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
{ `code, img, svg`,
pattern: `edition.cnn.com`, ],
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`, "twitter.com": [
}, `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
{ `img, a, .r-18u37iz, .css-175oi2r`,
pattern: `www.reuters.com`, ],
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`, "m.youtube.com": [
}, `.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`,
{ ],
pattern: `www.bloomberg.com`, "www.youtube.com": [
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`, `h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
}, ],
{ "bard.google.com": [
pattern: `deno.land, docs.github.com`, `.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
selector: `main ${DEFAULT_SELECTOR}`, ],
}, "www.bing.com": [
{ `.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
pattern: `doc.rust-lang.org`, ],
selector: `#content ${DEFAULT_SELECTOR}`, "www.phoronix.com": [`article ${DEFAULT_SELECTOR}`],
}, "wx2.qq.com": [`.js_message_plain`],
{ "app.slack.com/client/": [
pattern: `www.indiehackers.com`, `.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`, ],
}, "discord.com/channels/": [
{ `li[id^=chat-messages] div[id^=message-content], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content]`,
pattern: `platform.openai.com/docs`, ],
selector: `.docs-body ${DEFAULT_SELECTOR}`, "t.me/s/": [`.js-message_text ${DEFAULT_SELECTOR}`],
}, "web.telegram.org/k/": [
{ `.message, .bot-commands-list-element-description, .reply-markup-button-text`,
pattern: `en.wikipedia.org`, ],
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`, "web.telegram.org/a/": [
}, `.message, .text-content, .bot-commands-list-element-description, .reply-markup-button-text`,
{ ],
pattern: `stackoverflow.com`, "chromereleases.googleblog.com": [
selector: `h1, .s-prose p, .comment-body .comment-copy`, `.title, .publishdate, p, i, .header-desc, .header-title, .text`,
}, ],
{ "www.instagram.com/": [`h1, article span[dir=auto] > span[dir=auto], ._ab1y`],
pattern: `www.npmjs.com/package/, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org/`, "www.instagram.com/p/,www.instagram.com/reels/": [
selector: `article ${DEFAULT_SELECTOR}`, `h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`,
}, ],
{ "mail.google.com": [
pattern: `news.ycombinator.com`, `${DEFAULT_SELECTOR}, h2[data-thread-perm-id], span[data-thread-id], div[data-message-id] div[class=''], .messageBody, #views`,
selector: `.title, .commtext`, ],
}, "web.whatsapp.com": [`.copyable-text > span`],
{ "chat.openai.com": [
pattern: `https://github.com/`, `div[data-message-author-role] > div ${DEFAULT_SELECTOR}`,
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`, ],
}, "forum.ru-board.com": [`.tit, .dats, span.post, .lgf ${DEFAULT_SELECTOR}`],
{ "education.github.com": [
pattern: `twitter.com`, `${DEFAULT_SELECTOR}, a, summary, span.Button-content`,
selector: `[data-testid='tweetText']`, ],
}, "blogs.windows.com": [`${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`],
{ "developer.apple.com/documentation/": [
pattern: `youtube.com`, `#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`,
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`, `code, img, svg`,
}, ],
]; "greasyfork.org": [
`h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`,
],
"www.fmkorea.com": [`#container ${DEFAULT_SELECTOR}`],
"forum.arduino.cc": [
`.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`,
],
"docs.arduino.cc": [`[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`],
"www.historydefined.net": [`.wp-element-caption, ${DEFAULT_SELECTOR}`],
};
export const BUILTIN_RULES = RULES.sort((a, b) => export const BUILTIN_RULES = Object.entries(RULES_MAP)
a.pattern.localeCompare(b.pattern) .sort((a, b) => a[0].localeCompare(b[0]))
).map((item) => ({ .map(([pattern, [selector, keepSelector = "", terms = ""]]) => ({
...DEFAULT_RULE, ...DEFAULT_RULE,
...item, pattern,
transOpen: "true", selector,
})); keepSelector,
terms,
}));

View File

@@ -1,81 +1,3 @@
import { browser } from "./libs/browser"; import { run } from "./common";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getSettingWithDefault } from "./libs/storage";
import { isIframe, sendIframeMsg } from "./libs/iframe";
import { runWebfix } from "./libs/webfix";
import {
runIframe,
runTranslator,
showFab,
showTransbox,
windowListener,
showErr,
touchOperation,
} from "./common";
function runtimeListener(translator) { run();
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
});
}
/**
* 入口函数
*/
(async () => {
try {
// 读取设置信息
const setting = await getSettingWithDefault();
// 适配iframe
if (isIframe) {
runIframe(setting);
return;
}
// 不规范网页修复
await runWebfix(setting);
// 翻译网页
const { translator, rule } = await runTranslator(setting);
// 监听消息
windowListener(rule);
runtimeListener(translator);
// 划词翻译
showTransbox(setting);
// 浮球按钮
await showFab(translator);
// 触屏操作
touchOperation(translator);
} catch (err) {
console.error("[KISS-Translator]", err);
showErr(err.message);
}
})();

View File

@@ -12,10 +12,24 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
export default function Theme({ children, options }) { export default function Theme({ children, options }) {
const { darkMode } = useDarkMode(); const { darkMode } = useDarkMode();
const theme = useMemo(() => { const theme = useMemo(() => {
let htmlFontSize = 16;
try {
const s = window.getComputedStyle(document.body.parentNode).fontSize;
const fontSize = parseInt(s.replace("px", ""));
if (fontSize > 0 && fontSize < 1000) {
htmlFontSize = fontSize;
}
} catch (err) {
//
}
return createTheme({ return createTheme({
palette: { palette: {
mode: darkMode ? THEME_DARK : THEME_LIGHT, mode: darkMode ? THEME_DARK : THEME_LIGHT,
}, },
typography: {
htmlFontSize,
},
...options, ...options,
}); });
}, [darkMode, options]); }, [darkMode, options]);

View File

@@ -23,8 +23,18 @@ export function useTranslate(q, rule, setting) {
try { try {
setLoading(true); setLoading(true);
if (!q.replace(/\[(\d+)\]/g, "").trim()) {
setText(q);
setSamelang(false);
return;
}
const deLang = await tryDetectLang(q, setting.detectRemote); const deLang = await tryDetectLang(q, setting.detectRemote);
if (deLang && toLang.includes(deLang)) { const disableLangs = setting.disableLangs || [];
if (
deLang &&
(toLang.includes(deLang) || disableLangs.includes(deLang))
) {
setSamelang(true); setSamelang(true);
} else { } else {
const [trText, isSame] = await apiTranslate({ const [trText, isSame] = await apiTranslate({

13
src/libs/blacklist.js Normal file
View File

@@ -0,0 +1,13 @@
import { isMatch } from "./utils";
import { DEFAULT_BLACKLIST } from "../config";
/**
* 检查是否在黑名单中
* @param {*} href
* @param {*} param1
* @returns
*/
export const isInBlacklist = (
href,
{ blacklist = DEFAULT_BLACKLIST.join(",\n") }
) => blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));

205
src/libs/inputTranslate.js Normal file
View File

@@ -0,0 +1,205 @@
import {
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config";
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
}
/**
* 输入框翻译
*/
export default function inputTranslate ({
inputRule: {
transOpen,
triggerShortcut,
translator,
fromLang,
toLang,
triggerCount,
triggerTime,
transSign,
} = DEFAULT_INPUT_RULE,
transApis,
detectRemote,
}) {
if (!transOpen) {
return;
}
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
}
stepShortcutRegister(
triggerShortcut,
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
return;
}
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
}
let text = initText;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
}
}
// console.log("input -->", text);
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const deLang = await tryDetectLang(text, detectRemote);
if (deLang && toLang.includes(deLang)) {
return;
}
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(node, loadingId);
}
},
triggerCount,
triggerTime
);
}

View File

@@ -8,17 +8,39 @@ import {
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
URL_MICROSOFT_TRAN, URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART, URL_TENCENT_TRANSMART,
PROMPT_PLACE_FROM, PROMPT_PLACE_FROM,
PROMPT_PLACE_TO, PROMPT_PLACE_TO,
PROMPT_PLACE_TEXT,
} from "../config"; } from "../config";
import { msAuth } from "./auth"; import { msAuth } from "./auth";
import { genDeeplFree } from "../apis/deepl"; import { genDeeplFree } from "../apis/deepl";
import { genBaidu } from "../apis/baidu"; 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 * 构造缓存 request
* @param {*} request * @param {*} request
@@ -178,6 +200,37 @@ const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
return [url, init]; 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 genCloudflareAI = ({ text, from, to, url, key }) => {
const data = { const data = {
text, text,
@@ -224,6 +277,17 @@ const genCustom = ({ text, from, to, url, key }) => {
*/ */
export const newTransReq = ({ translator, text, from, to }, apiSetting) => { export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
const args = { 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) { switch (translator) {
case OPT_TRANS_GOOGLE: case OPT_TRANS_GOOGLE:
return genGoogle(args); return genGoogle(args);
@@ -241,6 +305,8 @@ export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
return genTencent(args); return genTencent(args);
case OPT_TRANS_OPENAI: case OPT_TRANS_OPENAI:
return genOpenAI(args); return genOpenAI(args);
case OPT_TRANS_GEMINI:
return genGemini(args);
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args); return genCloudflareAI(args);
case OPT_TRANS_CUSTOMIZE: case OPT_TRANS_CUSTOMIZE:

View File

@@ -21,7 +21,6 @@ import { trySyncRules } from "./sync";
* @returns * @returns
*/ */
export const matchRule = async ( export const matchRule = async (
rules,
href, href,
{ {
injectRules = true, injectRules = true,
@@ -29,7 +28,7 @@ export const matchRule = async (
owSubrule = DEFAULT_OW_RULE, owSubrule = DEFAULT_OW_RULE,
} }
) => { ) => {
rules = [...rules]; const rules = await getRulesWithDefault();
if (injectRules) { if (injectRules) {
try { try {
const selectedSub = subrulesList.find((item) => item.selected); const selectedSub = subrulesList.find((item) => item.selected);
@@ -67,6 +66,8 @@ export const matchRule = async (
} }
rule.selector = rule.selector?.trim() || globalRule.selector; rule.selector = rule.selector?.trim() || globalRule.selector;
rule.keepSelector = rule.keepSelector?.trim() || globalRule.keepSelector;
rule.terms = rule.terms?.trim() || globalRule.terms;
if (rule.textStyle === GLOBAL_KEY) { if (rule.textStyle === GLOBAL_KEY) {
rule.textStyle = globalRule.textStyle; rule.textStyle = globalRule.textStyle;
rule.bgColor = globalRule.bgColor; rule.bgColor = globalRule.bgColor;
@@ -113,6 +114,8 @@ export const checkRules = (rules) => {
({ ({
pattern, pattern,
selector, selector,
keepSelector,
terms,
translator, translator,
fromLang, fromLang,
toLang, toLang,
@@ -123,6 +126,8 @@ export const checkRules = (rules) => {
}) => ({ }) => ({
pattern: pattern.trim(), pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "", selector: type(selector) === "string" ? selector : "",
keepSelector: type(keepSelector) === "string" ? keepSelector : "",
terms: type(terms) === "string" ? terms : "",
bgColor: type(bgColor) === "string" ? bgColor : "", bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "", textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator), translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),

View File

@@ -36,8 +36,8 @@ export const shortcutListener = (fn, target = document, timeout = 3000) => {
} }
}; };
target.addEventListener("keydown", handleKeydown); target.addEventListener("keydown", handleKeydown, true);
target.addEventListener("keyup", handleKeyup); target.addEventListener("keyup", handleKeyup, true);
return () => { return () => {
if (timer) { if (timer) {
clearTimeout(timer); clearTimeout(timer);

View File

@@ -42,7 +42,7 @@ export const syncSubRules = async (url) => {
* @returns * @returns
*/ */
export const syncAllSubRules = async (subrulesList) => { export const syncAllSubRules = async (subrulesList) => {
for (let subrules of subrulesList) { for (const subrules of subrulesList) {
try { try {
await syncSubRules(subrules.url); await syncSubRules(subrules.url);
await updateSyncDataCache(subrules.url); await updateSyncDataCache(subrules.url);

View File

@@ -8,103 +8,23 @@ import {
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
SHADOW_KEY, SHADOW_KEY,
OPT_MOUSEKEY_DISABLE, OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_PAGEOPEN,
OPT_MOUSEKEY_MOUSEOVER, OPT_MOUSEKEY_MOUSEOVER,
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS, DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch"; import { updateFetchPool, clearFetchPool } from "./fetch";
import { import { debounce, genEventName } from "./utils";
debounce, import { runFixer } from "./webfix";
genEventName,
removeEndchar,
matchInputStr,
sleep,
} from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis"; import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
}
/** /**
* 翻译类 * 翻译类
*/ */
export class Translator { export class Translator {
_rule = {}; _rule = {};
_inputRule = {};
_setting = {}; _setting = {};
_fixerSetting = null;
_rootNodes = new Set(); _rootNodes = new Set();
_tranNodes = new Map(); _tranNodes = new Map();
_skipNodeNames = [ _skipNodeNames = [
@@ -125,6 +45,9 @@ export class Translator {
]; ];
_eventName = genEventName(); _eventName = genEventName();
_mouseoverNode = null; _mouseoverNode = null;
_keepSelector = [null, null];
_terms = [];
_docTitle = "";
// 显示 // 显示
_interseObserver = new IntersectionObserver( _interseObserver = new IntersectionObserver(
@@ -176,22 +99,26 @@ export class Translator {
}; };
}; };
constructor(rule, setting) { constructor(rule, setting, fixerSetting) {
const { fetchInterval, fetchLimit } = setting; const { fetchInterval, fetchLimit } = setting;
updateFetchPool(fetchInterval, fetchLimit); updateFetchPool(fetchInterval, fetchLimit);
this._overrideAttachShadow(); this._overrideAttachShadow();
this._setting = setting; this._setting = setting;
this._rule = rule; this._rule = rule;
this._fixerSetting = fixerSetting;
this._keepSelector = (rule.keepSelector || "")
.split(SHADOW_KEY)
.map((item) => item.trim());
this._terms = (rule.terms || "")
.split(/\n|;/)
.map((item) => item.split(",").map((item) => item.trim()))
.filter(([term]) => Boolean(term));
if (rule.transOpen === "true") { if (rule.transOpen === "true") {
this._register(); this._register();
} }
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
if (this._inputRule.transOpen) {
this._registerInput();
}
} }
get setting() { get setting() {
@@ -245,6 +172,20 @@ export class Translator {
this.rule = { ...this.rule, textStyle }; this.rule = { ...this.rule, textStyle };
}; };
translateText = async (text) => {
const { translator, fromLang, toLang } = this._rule;
const apiSetting =
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
const [trText] = await apiTranslate({
text,
translator,
fromLang,
toLang,
apiSetting,
});
return trText;
};
_querySelectorAll = (selector, node) => { _querySelectorAll = (selector, node) => {
try { try {
return Array.from(node.querySelectorAll(selector)); return Array.from(node.querySelectorAll(selector));
@@ -325,6 +266,11 @@ export class Translator {
return; return;
} }
// webfix
if (this._fixerSetting) {
runFixer(this._fixerSetting);
}
// 搜索节点 // 搜索节点
this._queryNodes(); this._queryNodes();
@@ -345,6 +291,11 @@ export class Translator {
this._tranNodes.forEach((_, node) => { this._tranNodes.forEach((_, node) => {
this._interseObserver.observe(node); this._interseObserver.observe(node);
}); });
} else if (this._setting.mouseKey === OPT_MOUSEKEY_PAGEOPEN) {
// 全文直接翻译
this._tranNodes.forEach((_, node) => {
this._render(node);
});
} else { } else {
// 监听鼠标悬停 // 监听鼠标悬停
window.addEventListener("keydown", this._handleKeydown); window.addEventListener("keydown", this._handleKeydown);
@@ -353,125 +304,15 @@ export class Translator {
node.addEventListener("mouseleave", this._handleMouseout); node.addEventListener("mouseleave", this._handleMouseout);
}); });
} }
};
_registerInput = () => { // 翻译页面标题
const { if (this._setting.transTitle && !this._docTitle) {
triggerShortcut: initTriggerShortcut, const title = document.title;
translator, this._docTitle = title;
fromLang, this.translateText(title).then((trText) => {
toLang: initToLang, document.title = `${trText} | ${title}`;
triggerCount: initTriggerCount, });
triggerTime,
transSign,
} = this._inputRule;
const apiSetting =
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
const { detectRemote } = this._setting;
let triggerShortcut = initTriggerShortcut;
let triggerCount = initTriggerCount;
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
} }
stepShortcutRegister(
triggerShortcut,
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
return;
}
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
}
let text = initText;
let toLang = initToLang;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
}
}
// console.log("input -->", text);
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const deLang = await tryDetectLang(text, detectRemote);
if (deLang && toLang.includes(deLang)) {
return;
}
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(node, loadingId);
}
},
triggerCount,
triggerTime
);
}; };
_handleMouseover = (e) => { _handleMouseover = (e) => {
@@ -519,6 +360,12 @@ export class Translator {
}; };
_unRegister = () => { _unRegister = () => {
// 恢复页面标题
if (this._docTitle) {
document.title = this._docTitle;
this._docTitle = "";
}
// 解除节点变化监听 // 解除节点变化监听
this._mutaObserver.disconnect(); this._mutaObserver.disconnect();
@@ -535,6 +382,10 @@ export class Translator {
// 移除已插入元素 // 移除已插入元素
node.querySelector(APP_LCNAME)?.remove(); node.querySelector(APP_LCNAME)?.remove();
}); });
} else if (this._setting.mouseKey === OPT_MOUSEKEY_PAGEOPEN) {
this._tranNodes.forEach((_, node) => {
node.querySelector(APP_LCNAME)?.remove();
});
} else { } else {
// 移除鼠标悬停监听 // 移除鼠标悬停监听
window.removeEventListener("keydown", this._handleKeydown); window.removeEventListener("keydown", this._handleKeydown);
@@ -561,6 +412,11 @@ export class Translator {
} }
}, 500); }, 500);
_invalidLength = (q) =>
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH);
_render = (el) => { _render = (el) => {
let traEl = el.querySelector(APP_LCNAME); let traEl = el.querySelector(APP_LCNAME);
@@ -580,19 +436,52 @@ export class Translator {
traEl.remove(); traEl.remove();
} }
const q = el.innerText.trim(); let q = el.innerText.trim();
this._tranNodes.set(el, q); this._tranNodes.set(el, q);
const keeps = [];
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
let text = "";
el.childNodes.forEach((child) => {
if (
child.nodeType === 1 &&
((matchSelector && child.matches(matchSelector)) ||
(subSelector && child.querySelector(subSelector)))
) {
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 {
text += child.textContent;
}
});
if (keeps.length > 0) {
q = text;
}
}
// 太长或太短 // 太长或太短
if ( if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
return; return;
} }
// console.log("---> ", q); // 专业术语
if (this._terms.length > 0) {
for (const term of this._terms) {
const re = new RegExp(term[0], "g");
q = q.replace(re, (t) => {
const text = `[${keeps.length}]`;
keeps.push(term[1] || t);
return text;
});
}
}
traEl = document.createElement(APP_LCNAME); traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible"; traEl.style.visibility = "visible";
@@ -604,7 +493,9 @@ export class Translator {
"-webkit-line-clamp: unset; max-height: none; height: auto;"; "-webkit-line-clamp: unset; max-height: none; height: auto;";
} }
// console.log({ q, keeps });
const root = createRoot(traEl); const root = createRoot(traEl);
root.render(<Content q={q} translator={this} />); root.render(<Content q={q} keeps={keeps} translator={this} />);
}; };
} }

View File

@@ -15,7 +15,7 @@ export const FIXER_ALL = [
FIXER_BN, FIXER_BN,
FIXER_BR_DIV, FIXER_BR_DIV,
FIXER_BN_DIV, FIXER_BN_DIV,
FIXER_FONTSIZE, // FIXER_FONTSIZE,
]; ];
/** /**
@@ -69,8 +69,8 @@ function brFixer(node, tag = "p") {
} }
node.setAttribute(fixedSign, "true"); node.setAttribute(fixedSign, "true");
var gapTags = ["BR", "WBR"]; const gapTags = ["BR", "WBR"];
var newlineTags = [ const newlineTags = [
"DIV", "DIV",
"UL", "UL",
"OL", "OL",
@@ -85,9 +85,10 @@ function brFixer(node, tag = "p") {
"HR", "HR",
"PRE", "PRE",
"TABLE", "TABLE",
"BLOCKQUOTE",
]; ];
var html = ""; let html = "";
node.childNodes.forEach(function (child, index) { node.childNodes.forEach(function (child, index) {
if (index === 0) { if (index === 0) {
html += `<${tag} class="kiss-p">`; html += `<${tag} class="kiss-p">`;
@@ -99,8 +100,8 @@ function brFixer(node, tag = "p") {
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`; html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
} else if (child.outerHTML) { } else if (child.outerHTML) {
html += child.outerHTML; html += child.outerHTML;
} else if (child.nodeValue) { } else if (child.textContent) {
html += child.nodeValue; html += child.textContent;
} }
if (index === node.childNodes.length - 1) { if (index === node.childNodes.length - 1) {
@@ -160,7 +161,7 @@ const fixerMap = {
* @param {*} rootSelector * @param {*} rootSelector
*/ */
function run(selector, fixer, rootSelector) { function run(selector, fixer, rootSelector) {
var mutaObserver = new MutationObserver(function (mutations) { const mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) { mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) { mutation.addedNodes.forEach(function (addNode) {
if (addNode && addNode.querySelectorAll) { if (addNode && addNode.querySelectorAll) {
@@ -172,7 +173,7 @@ function run(selector, fixer, rootSelector) {
}); });
}); });
var rootNodes = [document]; let rootNodes = [document];
if (rootSelector) { if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector); rootNodes = document.querySelectorAll(rootSelector);
} }
@@ -218,28 +219,38 @@ export const loadOrFetchWebfix = async (url) => {
}; };
/** /**
* 匹配站点 * 执行fixer
* @param {*} param0
*/ */
export async function runWebfix({ injectWebfix }) { export async function runFixer({ selector, fixer, rootSelector }) {
try { try {
if (!injectWebfix) { run(selector, fixerMap[fixer], rootSelector);
return; } catch (err) {
} console.error(`[kiss-webfix run]: ${err.message}`);
}
}
const href = document.location.href; /**
* 匹配fixer配置
*/
export async function matchFixer(href, { injectWebfix }) {
if (!injectWebfix) {
return null;
}
try {
const userSites = await getWebfixRulesWithDefault(); const userSites = await getWebfixRulesWithDefault();
const subSites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL); const subSites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
const sites = [...userSites, ...subSites]; const sites = [...userSites, ...subSites];
for (var i = 0; i < sites.length; i++) { for (let i = 0; i < sites.length; i++) {
var site = sites[i]; const site = sites[i];
if (isMatch(href, site.pattern)) { if (isMatch(href, site.pattern) && fixerMap[site.fixer]) {
if (fixerMap[site.fixer]) { return site;
run(site.selector, fixerMap[site.fixer], site.rootSelector);
}
break;
} }
} }
} catch (err) { } catch (err) {
console.error(`[kiss-webfix]: ${err.message}`); console.error(`[kiss-webfix match]: ${err.message}`);
} }
return null;
} }

View File

@@ -1,82 +1,3 @@
import { getSettingWithDefault } from "./libs/storage"; import { run } from "./common";
import { trySyncAllSubRules } from "./libs/subRules";
import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { genEventName } from "./libs/utils";
import { runWebfix } from "./libs/webfix";
import {
runIframe,
runTranslator,
showFab,
showTransbox,
windowListener,
showErr,
touchOperation,
} from "./common";
function runSettingPage() { run(true);
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM;
unsafeWindow.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
};
} else {
const ping = genEventName();
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
}
/**
* 入口函数
*/
(async () => {
try {
// 设置页面
if (
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
) {
runSettingPage();
return;
}
// 读取设置信息
const setting = await getSettingWithDefault();
// 适配iframe
if (isIframe) {
runIframe(setting);
return;
}
// 不规范网页修复
await runWebfix(setting);
// 翻译网页
const { translator, rule } = await runTranslator(setting);
// 监听消息
windowListener(rule);
// 划词翻译
showTransbox(setting);
// 浮球按钮
await showFab(translator);
// 触屏操作
touchOperation(translator);
// 同步订阅规则
await trySyncAllSubRules(setting);
} catch (err) {
console.error("[KISS-Translator]", err);
showErr(err.message);
}
})();

View File

@@ -95,40 +95,42 @@ export default function Action({ translator, fab }) {
// 注册菜单 // 注册菜单
try { try {
const menuCommandIds = []; const menuCommandIds = [];
menuCommandIds.push( const { contextMenuType } = translator.setting;
GM.registerMenuCommand( contextMenuType !== 0 &&
"Toggle Translate (Alt+q)", menuCommandIds.push(
(event) => { GM.registerMenuCommand(
translator.toggle(); "Toggle Translate",
sendIframeMsg(MSG_TRANS_TOGGLE); (event) => {
setShowPopup(false); translator.toggle();
}, sendIframeMsg(MSG_TRANS_TOGGLE);
"Q" setShowPopup(false);
), },
GM.registerMenuCommand( "Q"
"Toggle Style (Alt+c)", ),
(event) => { GM.registerMenuCommand(
translator.toggleStyle(); "Toggle Style",
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE); (event) => {
setShowPopup(false); translator.toggleStyle();
}, sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
"C" setShowPopup(false);
), },
GM.registerMenuCommand( "C"
"Open Menu (Alt+k)", ),
(event) => { GM.registerMenuCommand(
setShowPopup((pre) => !pre); "Open Menu",
}, (event) => {
"K" setShowPopup((pre) => !pre);
), },
GM.registerMenuCommand( "K"
"Open Setting (Alt+o)", ),
(event) => { GM.registerMenuCommand(
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"); "Open Setting",
}, (event) => {
"O" window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
) },
); "O"
)
);
return () => { return () => {
menuCommandIds.forEach((id) => { menuCommandIds.forEach((id) => {
@@ -215,7 +217,12 @@ export default function Action({ translator, fab }) {
} }
}} }}
> >
<TranslateIcon /> <TranslateIcon
sx={{
width: 24,
height: 24,
}}
/>
</Fab> </Fab>
} }
/> />

View File

@@ -7,6 +7,7 @@ import {
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
OPT_STYLE_DIY, OPT_STYLE_DIY,
DEFAULT_COLOR, DEFAULT_COLOR,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
@@ -34,6 +35,18 @@ const LineSpan = styled("span")`
} }
`; `;
const BlockquoteSpan = styled("span")`
opacity: 0.6;
-webkit-opacity: 0.6;
display: block;
padding: 0 0.75em;
border-left: 0.25em solid ${(props) => props.$lineColor};
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
const FuzzySpan = styled("span")` const FuzzySpan = styled("span")`
filter: blur(0.2em); filter: blur(0.2em);
-webkit-filter: blur(0.2em); -webkit-filter: blur(0.2em);
@@ -86,6 +99,12 @@ function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
{children} {children}
</HighlightSpan> </HighlightSpan>
); );
case OPT_STYLE_BLOCKQUOTE: // 引用
return (
<BlockquoteSpan $lineColor={bgColor || DEFAULT_COLOR}>
{children}
</BlockquoteSpan>
);
case OPT_STYLE_DIY: // 自定义 case OPT_STYLE_DIY: // 自定义
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>; return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
default: default:
@@ -93,7 +112,7 @@ function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
} }
} }
export default function Content({ q, translator }) { export default function Content({ q, keeps, translator }) {
const [rule, setRule] = useState(translator.rule); const [rule, setRule] = useState(translator.rule);
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting); const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { textStyle, bgColor = "", textDiyStyle = "" } = rule; const { textStyle, bgColor = "", textDiyStyle = "" } = rule;
@@ -126,18 +145,28 @@ export default function Content({ q, translator }) {
); );
} }
if (text && !sameLang) { if (!text || sameLang) {
return ( return;
<>
{q.length >= newlineLength ? <br /> : " "}
<StyledSpan
textStyle={textStyle}
textDiyStyle={textDiyStyle}
bgColor={bgColor}
>
{text}
</StyledSpan>
</>
);
} }
return (
<>
{q.length >= newlineLength ? <br /> : " "}
<StyledSpan
textStyle={textStyle}
textDiyStyle={textDiyStyle}
bgColor={bgColor}
>
{keeps.length > 0 ? (
<span
dangerouslySetInnerHTML={{
__html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
}}
/>
) : (
text
)}
</StyledSpan>
</>
);
} }

View File

@@ -5,10 +5,13 @@ import CircularProgress from "@mui/material/CircularProgress";
import { import {
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU, OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
URL_KISS_PROXY, URL_KISS_PROXY,
} from "../../config"; } from "../../config";
@@ -95,6 +98,13 @@ function ApiFields({ translator }) {
OPT_TRANS_TENCENT, OPT_TRANS_TENCENT,
]; ];
const mulkeysTranslators = [
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
];
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
{!buildinTranslators.includes(translator) && ( {!buildinTranslators.includes(translator) && (
@@ -112,10 +122,14 @@ function ApiFields({ translator }) {
name="key" name="key"
value={key} value={key}
onChange={handleChange} onChange={handleChange}
multiline={mulkeysTranslators.includes(translator)}
helperText={
mulkeysTranslators.includes(translator) ? i18n("mulkeys_help") : ""
}
/> />
</> </>
)} )}
{translator === OPT_TRANS_OPENAI && ( {(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && (
<> <>
<TextField <TextField
size="small" size="small"

View File

@@ -35,7 +35,7 @@ function DictField({ word }) {
fromLang: "en", fromLang: "en",
toLang: "zh-CN", toLang: "zh-CN",
}); });
setDictResult(dictRes[2].dict_result); dictRes[2].type === 1 && setDictResult(JSON.parse(dictRes[2].result));
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {

View File

@@ -6,6 +6,7 @@ import Box from "@mui/material/Box";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import DarkModeButton from "./DarkModeButton"; import DarkModeButton from "./DarkModeButton";
import Typography from "@mui/material/Typography";
function Header(props) { function Header(props) {
const i18n = useI18n(); const i18n = useI18n();
@@ -30,14 +31,14 @@ function Header(props) {
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ flexGrow: 1 }}> <Typography component="div" sx={{ flexGrow: 1, fontWeight: "bold" }}>
<Link <Link
underline="none" underline="none"
color="inherit" color="inherit"
href={process.env.REACT_APP_HOMEPAGE} href={process.env.REACT_APP_HOMEPAGE}
target="_blank" target="_blank"
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link> >{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box> </Typography>
<DarkModeButton /> <DarkModeButton />
</Toolbar> </Toolbar>
</AppBar> </AppBar>

View File

@@ -65,6 +65,8 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
const { const {
pattern, pattern,
selector, selector,
keepSelector = "",
terms = "",
translator, translator,
fromLang, fromLang,
toLang, toLang,
@@ -179,6 +181,26 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
onFocus={handleFocus} onFocus={handleFocus}
multiline multiline
/> />
<TextField
size="small"
label={i18n("keep_selector")}
helperText={i18n("keep_selector_helper")}
name="keepSelector"
value={keepSelector}
disabled={disabled}
onChange={handleChange}
multiline
/>
<TextField
size="small"
label={i18n("terms")}
helperText={i18n("terms_helper")}
name="terms"
value={terms}
disabled={disabled}
onChange={handleChange}
multiline
/>
<Box> <Box>
<Grid container spacing={2} columns={12}> <Grid container spacing={2} columns={12}>

View File

@@ -23,10 +23,14 @@ import {
OPT_SHORTCUT_STYLE, OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP, OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING, OPT_SHORTCUT_SETTING,
OPT_LANGS_TO,
DEFAULT_BLACKLIST,
MSG_CONTEXT_MENUS,
} from "../../config"; } from "../../config";
import { useShortcut } from "../../hooks/Shortcut"; import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab"; import { useFab } from "../../hooks/Fab";
import { sendBgMsg } from "../../libs/msg";
function ShortcutItem({ action, label }) { function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action); const { shortcut, setShortcut } = useShortcut(action);
@@ -61,7 +65,10 @@ export default function Settings() {
value = limitNumber(value, 1, 1000); value = limitNumber(value, 1, 1000);
break; break;
case "touchTranslate": case "touchTranslate":
value = limitNumber(value, 0, 3); value = limitNumber(value, 0, 4);
break;
case "contextMenuType":
isExt && sendBgMsg(MSG_CONTEXT_MENUS, { contextMenuType: value });
break; break;
default: default:
} }
@@ -89,7 +96,11 @@ export default function Settings() {
newlineLength = TRANS_NEWLINE_LENGTH, newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE, mouseKey = OPT_MOUSEKEY_DISABLE,
detectRemote = false, detectRemote = false,
contextMenuType = 1,
transTitle = false,
touchTranslate = 2, touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
disableLangs = [],
} = setting; } = setting;
const { isHide = false } = fab || {}; const { isHide = false } = fab || {};
@@ -158,11 +169,11 @@ export default function Settings() {
/> />
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("mouseover_translation")}</InputLabel> <InputLabel>{i18n("translate_timing")}</InputLabel>
<Select <Select
name="mouseKey" name="mouseKey"
value={mouseKey} value={mouseKey}
label={i18n("mouseover_translation")} label={i18n("translate_timing")}
onChange={handleChange} onChange={handleChange}
> >
{OPT_MOUSEKEY_ALL.map((item) => ( {OPT_MOUSEKEY_ALL.map((item) => (
@@ -173,6 +184,19 @@ export default function Settings() {
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small">
<InputLabel>{i18n("translate_page_title")}</InputLabel>
<Select
name="transTitle"
value={transTitle}
label={i18n("translate_page_title")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</Select>
</FormControl>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel> <InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
<Select <Select
@@ -181,7 +205,7 @@ export default function Settings() {
label={i18n("touch_translate_shortcut")} label={i18n("touch_translate_shortcut")}
onChange={handleChange} onChange={handleChange}
> >
{[0, 2, 3].map((item) => ( {[0, 2, 3, 4].map((item) => (
<MenuItem key={item} value={item}> <MenuItem key={item} value={item}>
{i18n(`touch_tap_${item}`)} {i18n(`touch_tap_${item}`)}
</MenuItem> </MenuItem>
@@ -204,6 +228,20 @@ export default function Settings() {
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small">
<InputLabel>{i18n("context_menus")}</InputLabel>
<Select
name="contextMenuType"
value={contextMenuType}
label={i18n("context_menus")}
onChange={handleChange}
>
<MenuItem value={0}>{i18n("hide_context_menus")}</MenuItem>
<MenuItem value={1}>{i18n("simple_context_menus")}</MenuItem>
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</Select>
</FormControl>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("detect_lang_remote")}</InputLabel> <InputLabel>{i18n("detect_lang_remote")}</InputLabel>
<Select <Select
@@ -218,24 +256,44 @@ export default function Settings() {
<FormHelperText>{i18n("detect_lang_remote_help")}</FormHelperText> <FormHelperText>{i18n("detect_lang_remote_help")}</FormHelperText>
</FormControl> </FormControl>
<FormControl size="small">
<InputLabel>{i18n("disable_langs")}</InputLabel>
<Select
multiple
name="disableLangs"
value={disableLangs}
label={i18n("disable_langs")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([langKey, langName]) => (
<MenuItem key={langKey} value={langKey}>
{langName}
</MenuItem>
))}
</Select>
<FormHelperText>{i18n("disable_langs_helper")}</FormHelperText>
</FormControl>
{isExt ? ( {isExt ? (
<FormControl size="small"> <>
<InputLabel>{i18n("if_clear_cache")}</InputLabel> <FormControl size="small">
<Select <InputLabel>{i18n("if_clear_cache")}</InputLabel>
name="clearCache" <Select
value={clearCache} name="clearCache"
label={i18n("if_clear_cache")} value={clearCache}
onChange={handleChange} label={i18n("if_clear_cache")}
> onChange={handleChange}
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem> >
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem> <MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
</Select> <MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
<FormHelperText> </Select>
<Link component="button" onClick={handleClearCache}> <FormHelperText>
{i18n("clear_all_cache_now")} <Link component="button" onClick={handleClearCache}>
</Link> {i18n("clear_all_cache_now")}
</FormHelperText> </Link>
</FormControl> </FormHelperText>
</FormControl>
</>
) : ( ) : (
<> <>
<Box> <Box>
@@ -268,6 +326,16 @@ export default function Settings() {
</Box> </Box>
</> </>
)} )}
<TextField
size="small"
label={i18n("translate_blacklist")}
helperText={i18n("pattern_helper")}
name="blacklist"
defaultValue={blacklist}
onChange={handleChange}
multiline
/>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -10,6 +10,7 @@ import Switch from "@mui/material/Switch";
import { useCallback } from "react"; import { useCallback } from "react";
import { limitNumber } from "../../libs/utils"; import { limitNumber } from "../../libs/utils";
import { useTranbox } from "../../hooks/Tranbox"; import { useTranbox } from "../../hooks/Tranbox";
import { isExt } from "../../libs/client";
export default function Tranbox() { export default function Tranbox() {
const i18n = useI18n(); const i18n = useI18n();
@@ -44,9 +45,11 @@ export default function Tranbox() {
translator, translator,
fromLang, fromLang,
toLang, toLang,
toLang2 = "en",
tranboxShortcut, tranboxShortcut,
btnOffsetX, btnOffsetX,
btnOffsetY, btnOffsetY,
hideTranBtn = false,
} = tranboxSetting; } = tranboxSetting;
return ( return (
@@ -111,6 +114,22 @@ export default function Tranbox() {
))} ))}
</TextField> </TextField>
<TextField
select
size="small"
name="toLang2"
value={toLang2}
label={i18n("to_lang2")}
helperText={i18n("to_lang2_helper")}
onChange={handleChange}
>
{[["none", "None"], ...OPT_LANGS_TO].map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField <TextField
size="small" size="small"
label={i18n("tranbtn_offset_x")} label={i18n("tranbtn_offset_x")}
@@ -129,11 +148,25 @@ export default function Tranbox() {
onChange={handleChange} onChange={handleChange}
/> />
<ShortcutInput <TextField
value={tranboxShortcut} select
onChange={handleShortcutInput} size="small"
label={i18n("trigger_tranbox_shortcut")} name="hideTranBtn"
/> value={hideTranBtn}
label={i18n("hide_tran_button")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("show")}</MenuItem>
<MenuItem value={true}>{i18n("hide")}</MenuItem>
</TextField>
{!isExt && (
<ShortcutInput
value={tranboxShortcut}
onChange={handleShortcutInput}
label={i18n("trigger_tranbox_shortcut")}
/>
)}
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,9 +1,9 @@
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import HomeIcon from "@mui/icons-material/Home"; import HomeIcon from "@mui/icons-material/Home";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import DarkModeButton from "../Options/DarkModeButton"; import DarkModeButton from "../Options/DarkModeButton";
import Typography from "@mui/material/Typography";
export default function Header({ setShowPopup }) { export default function Header({ setShowPopup }) {
const handleHomepage = () => { const handleHomepage = () => {
@@ -21,14 +21,16 @@ export default function Header({ setShowPopup }) {
<IconButton onClick={handleHomepage}> <IconButton onClick={handleHomepage}>
<HomeIcon /> <HomeIcon />
</IconButton> </IconButton>
<Box <Typography
component="div"
sx={{ sx={{
userSelect: "none", userSelect: "none",
WebkitUserSelect: "none", WebkitUserSelect: "none",
fontWeight: "bold",
}} }}
> >
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`} {`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box> </Typography>
</Stack> </Stack>
{setShowPopup ? ( {setShowPopup ? (

View File

@@ -18,11 +18,11 @@ import {
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS, MSG_OPEN_OPTIONS,
MSG_SAVE_RULE, MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_STYLE_USE_COLOR,
} from "../../config"; } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules"; import { saveRule } from "../../libs/rules";
@@ -31,6 +31,7 @@ import { tryClearCaches } from "../../libs";
export default function Popup({ setShowPopup, translator: tran }) { export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n(); const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule); const [rule, setRule] = useState(tran?.rule);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => { const handleOpenSetting = () => {
if (!tran) { if (!tran) {
@@ -85,7 +86,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
const tab = await getTabInfo(); const tab = await getTabInfo();
href = tab.url; href = tab.url;
} }
const newRule = { ...rule, pattern: href }; const newRule = { ...rule, pattern: href.split("/")[2] };
if (isExt && tran) { if (isExt && tran) {
sendBgMsg(MSG_SAVE_RULE, newRule); sendBgMsg(MSG_SAVE_RULE, newRule);
} else { } else {
@@ -112,6 +113,32 @@ export default function Popup({ setShowPopup, translator: tran }) {
})(); })();
}, [tran]); }, [tran]);
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
if (!res.error) {
res.data.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
}
} else {
const shortcuts = tran.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
console.log("[query cmds]", err);
}
})();
}, [tran]);
if (!rule) { if (!rule) {
return ( return (
<Box minWidth={300}> <Box minWidth={300}>
@@ -130,7 +157,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
); );
} }
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule; const { transOpen, translator, fromLang, toLang, textStyle } = rule;
return ( return (
<Box minWidth={300}> <Box minWidth={300}>
@@ -154,13 +181,12 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle} onChange={handleTransToggle}
/> />
} }
label={i18n("translate_alt")} label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/> />
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
</Stack> </Stack>
<TextField <TextField
@@ -217,7 +243,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small" size="small"
value={textStyle} value={textStyle}
name="textStyle" name="textStyle"
label={i18n("text_style_alt")} label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange} onChange={handleChange}
> >
{OPT_STYLE_ALL.map((item) => ( {OPT_STYLE_ALL.map((item) => (
@@ -227,7 +257,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
))} ))}
</TextField> </TextField>
{OPT_STYLE_USE_COLOR.includes(textStyle) && ( {/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
<TextField <TextField
size="small" size="small"
name="bgColor" name="bgColor"
@@ -235,7 +265,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
label={i18n("bg_color")} label={i18n("bg_color")}
onChange={handleChange} onChange={handleChange}
/> />
)} )} */}
<Stack <Stack
direction="row" direction="row"
@@ -246,6 +276,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
<Button variant="text" onClick={handleSaveRule}> <Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")} {i18n("save_rule")}
</Button> </Button>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
<Button variant="text" onClick={handleOpenSetting}> <Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")} {i18n("setting")}
</Button> </Button>

View File

@@ -5,9 +5,9 @@ import { useState } from "react";
export default function CopyBtn({ text }) { export default function CopyBtn({ text }) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleClick = (e) => { const handleClick = async (e) => {
e.stopPropagation(); e.stopPropagation();
navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
setCopied(true); setCopied(true);
const timer = setTimeout(() => { const timer = setTimeout(() => {
clearTimeout(timer); clearTimeout(timer);

View File

@@ -1,15 +1,11 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn"; import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
const exchangeMap = { const phonicMap = {
word_third: "第三人称单数", en_phonic: "",
word_ing: "现在分词", us_phonic: "",
word_done: "过去式",
word_past: "过去分词",
word_pl: "复数",
word_proto: "词源",
}; };
export default function DictCont({ dictResult }) { export default function DictCont({ dictResult }) {
@@ -24,41 +20,29 @@ export default function DictCont({ dictResult }) {
justifyContent="space-between" justifyContent="space-between"
alignItems="flex-start" alignItems="flex-start"
> >
<div style={{ fontWeight: "bold" }}> <Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.simple_means?.word_name} {dictResult.src}
</div> </Typography>
<FavBtn word={dictResult.simple_means?.word_name} /> <FavBtn word={dictResult.src} />
</Stack> </Stack>
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => ( <Typography component="div">
<div key={idx}> <Typography>
{(ph_en || ph_am) && ( {dictResult.voice
<div>{`英 /${ph_en || ""}/ 美 /${ph_am || ""}/`}</div> .map(Object.entries)
)} .map((item) => item[0])
<ul style={{ margin: "0.5em 0" }}> .map(([key, val]) => `${phonicMap[key] || key} ${val}`)
{parts.map(({ part, means }, idx) => ( .join(" ")}
<li key={idx}> </Typography>
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")} <ul style={{ margin: "0.5em 0" }}>
</li> {dictResult.content[0].mean.map(({ pre, cont }, idx) => (
))} <li key={idx}>
</ul> {pre && `[${pre}] `}
</div> {Object.keys(cont).join("; ")}
))} </li>
<div>
{Object.entries(dictResult.simple_means?.exchange || {})
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
.join("; ")}
</div>
<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> </ul>
</Typography>
</Box> </Box>
); );
} }

View File

@@ -0,0 +1,65 @@
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,6 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { isMobile } from "../../libs/mobile";
function Pointer({ function Pointer({
direction, direction,
@@ -16,21 +17,23 @@ function Pointer({
const [origin, setOrigin] = useState(null); const [origin, setOrigin] = useState(null);
function handlePointerDown(e) { function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId); !isMobile && e.target.setPointerCapture(e.pointerId);
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
setOrigin({ setOrigin({
x: position.x, x: position.x,
y: position.y, y: position.y,
w: size.w, w: size.w,
h: size.h, h: size.h,
clientX: e.clientX, clientX,
clientY: e.clientY, clientY,
}); });
} }
function handlePointerMove(e) { function handlePointerMove(e) {
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
if (origin) { if (origin) {
const dx = e.clientX - origin.clientX; const dx = clientX - origin.clientX;
const dy = e.clientY - origin.clientY; const dy = clientY - origin.clientY;
let x = position.x; let x = position.x;
let y = position.y; let y = position.y;
let w = size.w; let w = size.w;
@@ -101,16 +104,24 @@ function Pointer({
} }
function handlePointerUp(e) { function handlePointerUp(e) {
e.stopPropagation();
setOrigin(null); setOrigin(null);
} }
const touchProps = isMobile
? {
onTouchStart: handlePointerDown,
onTouchMove: handlePointerMove,
onTouchEnd: handlePointerUp,
}
: {
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
};
return ( return (
<div <div {...props} {...touchProps}>
{...props}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
{children} {children}
</div> </div>
); );
@@ -162,6 +173,7 @@ export default function DraggableResizable({
return ( return (
<Box <Box
style={{ style={{
touchAction: "none",
position: "fixed", position: "fixed",
left: position.x, left: position.x,
top: position.y, top: position.y,

View File

@@ -24,6 +24,7 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
const [translator, setTranslator] = useState(tranboxSetting.translator); const [translator, setTranslator] = useState(tranboxSetting.translator);
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang); const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
const [toLang, setToLang] = useState(tranboxSetting.toLang); const [toLang, setToLang] = useState(tranboxSetting.toLang);
const [toLang2, setToLang2] = useState(tranboxSetting.toLang2);
const inputRef = useRef(null); const inputRef = useRef(null);
return ( return (
@@ -95,6 +96,7 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
<Box> <Box>
<TextField <TextField
size="small"
label={i18n("original_text")} label={i18n("original_text")}
inputRef={inputRef} inputRef={inputRef}
fullWidth fullWidth
@@ -149,6 +151,9 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
translator={translator} translator={translator}
fromLang={fromLang} fromLang={fromLang}
toLang={toLang} toLang={toLang}
toLang2={toLang2}
setToLang={setToLang}
setToLang2={setToLang2}
transApis={transApis} transApis={transApis}
/> />
</Stack> </Stack>

View File

@@ -1,17 +1,27 @@
import { isMobile } from "../../libs/mobile";
export default function TranBtn({ onClick, position, tranboxSetting }) { 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,
};
return ( return (
<div <div
style={{ style={{
cursor: "pointer", cursor: "pointer",
position: "fixed", position: "absolute",
left: position.x + tranboxSetting.btnOffsetX, left,
top: position.y + tranboxSetting.btnOffsetY, top,
zIndex: 2147483647, zIndex: 2147483647,
}} }}
onClick={onClick} {...touchProps}
onMouseUp={(e) => {
e.stopPropagation();
}}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config"; import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis"; import { apiTranslate, apiBaiduLangdetect } from "../../apis";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import DictCont from "./DictCont"; import DictCont from "./DictCont";
@@ -16,6 +16,9 @@ export default function TranCont({
translator, translator,
fromLang, fromLang,
toLang, toLang,
toLang2 = "en",
setToLang,
setToLang2,
transApis, transApis,
}) { }) {
const i18n = useI18n(); const i18n = useI18n();
@@ -32,6 +35,16 @@ export default function TranCont({
setError(""); setError("");
setDictResult(null); setDictResult(null);
// 互译
if (toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await apiBaiduLangdetect(text);
if (detectLang === toLang) {
setToLang(toLang2);
setToLang2(toLang);
return;
}
}
const apiSetting = const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator]; transApis[translator] || DEFAULT_TRANS_APIS[translator];
const tranRes = await apiTranslate({ const tranRes = await apiTranslate({
@@ -46,7 +59,8 @@ export default function TranCont({
// 词典 // 词典
if (isValidWord(text) && toLang.startsWith("zh")) { if (isValidWord(text) && toLang.startsWith("zh")) {
if (fromLang === "en" && translator === OPT_TRANS_BAIDU) { if (fromLang === "en" && translator === OPT_TRANS_BAIDU) {
setDictResult(tranRes[2].dict_result); tranRes[2].type === 1 &&
setDictResult(JSON.parse(tranRes[2].result));
} else { } else {
const dictRes = await apiTranslate({ const dictRes = await apiTranslate({
text, text,
@@ -54,7 +68,8 @@ export default function TranCont({
fromLang: "en", fromLang: "en",
toLang: "zh-CN", toLang: "zh-CN",
}); });
setDictResult(dictRes[2].dict_result); dictRes[2].type === 1 &&
setDictResult(JSON.parse(dictRes[2].result));
} }
} }
} catch (err) { } catch (err) {
@@ -63,12 +78,22 @@ export default function TranCont({
setLoading(false); setLoading(false);
} }
})(); })();
}, [text, translator, fromLang, toLang, transApis]); }, [
text,
translator,
fromLang,
toLang,
toLang2,
setToLang,
setToLang2,
transApis,
]);
return ( return (
<> <>
<Box> <Box>
<TextField <TextField
size="small"
label={i18n("translated_text")} label={i18n("translated_text")}
// disabled // disabled
fullWidth fullWidth

View File

@@ -1,64 +1,135 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import TranBtn from "./TranBtn"; import TranBtn from "./TranBtn";
import TranBox from "./TranBox"; import TranBox from "./TranBox";
import { shortcutRegister } from "../../libs/shortcut"; import { shortcutRegister } from "../../libs/shortcut";
import { sleep } from "../../libs/utils"; import { sleep, limitNumber } from "../../libs/utils";
import { isGm, isExt } from "../../libs/client";
import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config";
import { isMobile } from "../../libs/mobile";
export default function Slection({
contextMenuType,
tranboxSetting,
transApis,
}) {
const boxWidth = limitNumber(window.innerWidth, 300, 600);
const boxHeight = limitNumber(window.innerHeight, 200, 400);
export default function Slection({ tranboxSetting, transApis }) {
const [showBox, setShowBox] = useState(false); const [showBox, setShowBox] = useState(false);
const [showBtn, setShowBtn] = useState(false); const [showBtn, setShowBtn] = useState(false);
const [selectedText, setSelText] = useState(""); const [selectedText, setSelText] = useState("");
const [text, setText] = useState(""); const [text, setText] = useState("");
const [position, setPosition] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
const [boxSize, setBoxSize] = useState({ w: 600, h: 400 }); const [boxSize, setBoxSize] = useState({
w: boxWidth,
h: boxHeight,
});
const [boxPosition, setBoxPosition] = useState({ const [boxPosition, setBoxPosition] = useState({
x: (window.innerWidth - 600) / 2, x: (window.innerWidth - boxWidth) / 2,
y: (window.innerHeight - 400) / 2, y: (window.innerHeight - boxHeight) / 2,
}); });
async function handleMouseup(e) { const handleClick = (e) => {
await sleep(10); e.stopPropagation();
setShowBtn(false);
setText(selectedText);
setShowBox(true);
};
const handleTranbox = useCallback(() => {
setShowBtn(false);
const selectedText = window.getSelection()?.toString()?.trim() || ""; const selectedText = window.getSelection()?.toString()?.trim() || "";
if (!selectedText) { if (!selectedText) {
setShowBtn(false); setShowBox((pre) => !pre);
return; return;
} }
setSelText(selectedText); setSelText(selectedText);
setShowBtn(true);
setPosition({ x: e.clientX, y: e.clientY });
}
const handleClick = (e) => {
e.stopPropagation();
setShowBtn(false);
setText(selectedText); setText(selectedText);
if (!showBox) { setShowBox(true);
setShowBox(true);
}
};
useEffect(() => {
window.addEventListener("mouseup", handleMouseup);
return () => {
window.removeEventListener("mouseup", handleMouseup);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
const clearShortcut = shortcutRegister( async function handleMouseup(e) {
tranboxSetting.tranboxShortcut, e.stopPropagation();
() => { await sleep(10);
setShowBox((pre) => !pre);
const selectedText = window.getSelection()?.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 });
}
// todo: mobile support
window.addEventListener("mouseup", handleMouseup);
// window.addEventListener(isMobile ? "touchend" : "mouseup", handleMouseup);
return () => {
window.removeEventListener(
isMobile ? "touchend" : "mouseup",
handleMouseup
);
};
}, [tranboxSetting.hideTranBtn]);
useEffect(() => {
if (isExt) {
return;
}
const clearShortcut = shortcutRegister(
tranboxSetting.tranboxShortcut || DEFAULT_TRANBOX_SHORTCUT,
handleTranbox
); );
return () => { return () => {
clearShortcut(); clearShortcut();
}; };
}, [tranboxSetting.tranboxShortcut, setShowBox]); }, [tranboxSetting.tranboxShortcut, handleTranbox]);
useEffect(() => {
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
return () => {
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
};
}, [handleTranbox]);
useEffect(() => {
if (!isGm) {
return;
}
// 注册菜单
try {
const menuCommandIds = [];
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
"Translate Selected Text",
(event) => {
handleTranbox();
},
"S"
)
);
return () => {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
};
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}, [handleTranbox, contextMenuType]);
return ( return (
<> <>