Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b3b00117 | ||
|
|
763019f0c5 | ||
|
|
d743271be8 | ||
|
|
992dad26aa | ||
|
|
9bd0e67474 | ||
|
|
5767a4afb2 | ||
|
|
9e4c510684 | ||
|
|
16607fb069 | ||
|
|
e047a06432 | ||
|
|
9e09fd898a | ||
|
|
799c32a871 | ||
|
|
483f33b5c9 | ||
|
|
d444fd4fba | ||
|
|
0aae93ba2e | ||
|
|
9608bea3bf | ||
|
|
a038a1ecdc | ||
|
|
c82cdd7f8f | ||
|
|
0c6d5c3c61 | ||
|
|
900426f359 | ||
|
|
1d760fc93a | ||
|
|
61571e0f61 | ||
|
|
c9eb423c89 | ||
|
|
45b294a121 | ||
|
|
3a3f1fabe1 | ||
|
|
03177a09b3 | ||
|
|
cae391f62b | ||
|
|
2a5e9db079 | ||
|
|
650d6e8b41 | ||
|
|
1daf134b31 | ||
|
|
e1dfa35c6c | ||
|
|
73f80692d3 | ||
|
|
42c7dae495 | ||
|
|
b2a1309caa | ||
|
|
94bf5f9580 | ||
|
|
704ebdc9d7 | ||
|
|
165da4e559 | ||
|
|
d3e3b484bf | ||
|
|
192f8faa5b | ||
|
|
579d5cb0a3 | ||
|
|
07fca5b9af | ||
|
|
866a63ab6c | ||
|
|
97b4935bc4 | ||
|
|
30129abef3 | ||
|
|
24d904b32c | ||
|
|
5f0ce57ead | ||
|
|
51f58d095a | ||
|
|
d22e3838c4 | ||
|
|
adbb421b7b | ||
|
|
eaa47af269 | ||
|
|
a6cb5544f8 | ||
|
|
9e91faa660 | ||
|
|
8636fadc72 | ||
|
|
0621957592 | ||
|
|
8ec06b0c84 | ||
|
|
d47f8d7ee9 | ||
|
|
24f8959525 | ||
|
|
983740578b | ||
|
|
b5f79ed7cd | ||
|
|
bbb0e79d4e | ||
|
|
471dc05897 | ||
|
|
7a772d2459 | ||
|
|
1d92421960 | ||
|
|
aeaaf429d7 | ||
|
|
84432e98ae | ||
|
|
77c6102de7 | ||
|
|
ab5dd82169 | ||
|
|
23e7b69dc5 | ||
|
|
3dc8f393f2 | ||
|
|
8a2144f263 | ||
|
|
c1c59caa10 | ||
|
|
d27ebd90b6 | ||
|
|
467745c1e9 | ||
|
|
537378a038 | ||
|
|
4b5ed30e5b | ||
|
|
52b7f6a225 | ||
|
|
f31675d8a2 | ||
|
|
dd46a8450c | ||
|
|
b0843f7d66 | ||
|
|
daadc0195c | ||
|
|
298dec6957 | ||
|
|
bf39d85dfa | ||
|
|
30a9de25a8 | ||
|
|
af1ecf0bd4 | ||
|
|
fe55a2cd3c | ||
|
|
5a33d4e57e | ||
|
|
7f46a9023c | ||
|
|
dfd943b621 | ||
|
|
7007d0d922 | ||
|
|
601678500d | ||
|
|
9bfb504381 | ||
|
|
8a03b0cf15 | ||
|
|
11ba89de0a | ||
|
|
bac7f62eea | ||
|
|
eef90ea02b | ||
|
|
0650df534a | ||
|
|
9ef8c8b823 | ||
|
|
d7e08da0b2 | ||
|
|
ef361e0798 | ||
|
|
6855332092 | ||
|
|
121d523e02 | ||
|
|
42a375c4c7 | ||
|
|
a1dd705d97 | ||
|
|
71f90b36ca | ||
|
|
37facdc3c1 | ||
|
|
66b4f547ff | ||
|
|
d27b9c7f2d | ||
|
|
278ff9c6bc | ||
|
|
d6fe1ce9d7 | ||
|
|
0bfa5256b8 | ||
|
|
72ccfc8aec | ||
|
|
d117c5dc10 | ||
|
|
9312783f44 | ||
|
|
e5b16ebfd3 | ||
|
|
5d1d65c2d3 | ||
|
|
9ca1309cec | ||
|
|
a03afc05f5 | ||
|
|
0198963584 | ||
|
|
58e745d967 | ||
|
|
377e347d68 | ||
|
|
bac0704d3d | ||
|
|
d2ff46edf6 | ||
|
|
f908372b4e | ||
|
|
5d44ff4913 | ||
|
|
4c9aa66048 | ||
|
|
b6a09b99ab | ||
|
|
3a0dcb1a52 | ||
|
|
5015503b4c | ||
|
|
16423feea4 |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.8.8
|
||||
REACT_APP_VERSION=1.9.2
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
|
||||
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@@ -7,28 +7,28 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.7.6
|
||||
- uses: actions/setup-node@v3
|
||||
version: latest
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.17.0"
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm build
|
||||
- uses: actions/upload-artifact@v3
|
||||
- run: pnpm build+zip
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
deploy-web:
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
@@ -37,7 +37,8 @@ jobs:
|
||||
with:
|
||||
folder: build/web
|
||||
create-release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||
steps:
|
||||
@@ -54,18 +55,14 @@ jobs:
|
||||
needs: [build, create-release]
|
||||
strategy:
|
||||
matrix:
|
||||
client: ["chrome", "edge", "firefox", "userscript"]
|
||||
runs-on: ubuntu-22.04
|
||||
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
- name: Zip Release
|
||||
run: |
|
||||
cd build
|
||||
zip -r ${{ matrix.client }}.zip ${{ matrix.client }}
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
public
|
||||
package.json
|
||||
24
.prettierrc
Normal file
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"experimentalTernaries": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
120
README.en.md
120
README.en.md
@@ -1,5 +1,7 @@
|
||||
# KISS Translator
|
||||
|
||||
English | [简体中文](README.md)
|
||||
|
||||
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
@@ -9,10 +11,18 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
|
||||
- [x] Keep it simple, smart
|
||||
- [x] Open source
|
||||
- [x] Adapt to common browsers
|
||||
- [x] Chrome/Edge/Firefox/Kiwi/Orion
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [ ] Safari
|
||||
- [x] Safari (Mac)
|
||||
- [x] Thunderbird
|
||||
- [x] Supports multiple translation services
|
||||
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
|
||||
- [x] Google/Microsoft
|
||||
- [x] Baidu/Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] Custom translation interface
|
||||
- [x] Covers common translation scenarios
|
||||
- [x] Web bilingual translation
|
||||
@@ -50,6 +60,8 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
|
||||
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] Safari (Mac) Compiled by a third party, not verified, obtained by yourself: https://www.nodeloc.com/t/topic/54245
|
||||
- [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] GreaseMonkey Script
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
@@ -67,16 +79,112 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
|
||||
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
|
||||
- Deploy and manage by yourself.
|
||||
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
||||
- A word-marking translation plug-in used with this project.
|
||||
- Supports query of English words, sentences and Chinese characters.
|
||||
- Supports history records and word collections.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### How to Turn Off Automatic Translation
|
||||
|
||||
You can achieve this through `Rules Setting` with the following methods:
|
||||
|
||||
- Personal Rules: RULES-> Global Rule -> Translate Switch -> Disaabled
|
||||
- Subscription Rules: SUBSCRIBE -> Select the third option `kiss-rules-off.json`
|
||||
- Override Subscription Rules: OVERWRITE -> Translate Switch -> Disaabled
|
||||
- Add a Personal Rule for a Specific Website: Translate Switch -> Disaabled
|
||||
|
||||
### How to Set Keyboard Shortcuts
|
||||
|
||||
Set this in the extension management page, for example:
|
||||
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
### How to Turn Off Selection Translation
|
||||
|
||||
Set this in the `Rules Setting`: RULES -> Global Rule -> If translate selected -> Disable
|
||||
|
||||
### How to Set it to Show Only the Translation
|
||||
|
||||
Set this in the `Rules Setting`: RULES -> Global Rule -> Show Only Translations -> Enable
|
||||
|
||||
### How to Set Mouse Hover Translation
|
||||
|
||||
Set this in the `Rules Setting`: RULES -> Global Rule -> TTrigger Mode
|
||||
|
||||
### Why are some web pages not fully translated?
|
||||
|
||||
This extension's webpage translation is based on CSS selectors. Generic rules cannot adapt to all websites, and sometimes you need to manually add site-specific rules. If you don't know how to write rules, you can seek help here:
|
||||
https://github.com/fishjar/kiss-rules/issues
|
||||
|
||||
### What is the priority order of rule settings?
|
||||
|
||||
Personal Rules > Override Subscription Rules > Subscription Rules > Global Rules
|
||||
|
||||
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||
|
||||
### Why are YouTube subtitles translated in broken sentences?
|
||||
|
||||
This extension has no special development for video content. Support for YouTube is also treated as regular webpage translation. Auto-generated subtitles are streamed and output progressively, resulting in poorer support.
|
||||
|
||||
To disable this extension's subtitle translation, add a rule. Reference: https://github.com/fishjar/kiss-translator/issues/62
|
||||
|
||||
### Local Ollama interface cannot be used
|
||||
|
||||
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### Custom API doesn't work in Tampermonkey scripts
|
||||
|
||||
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
|
||||
|
||||
### How to Set Up Hook Functions for Custom Interfaces
|
||||
|
||||
The custom interface feature is highly flexible and can theoretically integrate with any translation interface.
|
||||
|
||||
Example of a Request Hook function:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Request Hook
|
||||
* @param {string} text Text to be translated
|
||||
* @param {string} from Source language
|
||||
* @param {string} to Target language
|
||||
* @param {string} url Translation interface URL
|
||||
* @param {string} key Translation interface API key
|
||||
* @returns {Array[string, object]} [Interface URL, request object]
|
||||
*/
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
method: "POST",
|
||||
body: { text, to },
|
||||
}]
|
||||
```
|
||||
|
||||
Example of a Response Hook function:
|
||||
|
||||
```js
|
||||
* Response Hook
|
||||
* @param {string} res JSON data returned by the interface
|
||||
* @param {string} text Text to be translated
|
||||
* @param {string} from Source language
|
||||
* @param {string} to Target language
|
||||
* @returns {Array[string, boolean]} [Translated text, whether target language is same as source]
|
||||
* Note: If the second return value is true (target language same as source),
|
||||
* the translation will not be displayed on the page,
|
||||
* If the parameters are incomplete, it is recommended to return false directly
|
||||
*/
|
||||
(res, text, from, to) => [res.text, to === res.src]
|
||||
```
|
||||
|
||||
For more custom interface examples, refer to: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
git checkout dev # Submit a PR suggestion to push to the dev branch
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
119
README.md
119
README.md
@@ -1,5 +1,7 @@
|
||||
# 简约翻译
|
||||
|
||||
[English](README.en.md) | 简体中文
|
||||
|
||||
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
@@ -9,10 +11,18 @@
|
||||
- [x] 保持简约
|
||||
- [x] 开放源代码
|
||||
- [x] 适配常见浏览器
|
||||
- [x] Chrome/Edge/Firefox/Kiwi/Orion
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [ ] Safari
|
||||
- [x] Safari (Mac)
|
||||
- [x] Thunderbird
|
||||
- [x] 支持多种翻译服务
|
||||
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
|
||||
- [x] Google/Microsoft
|
||||
- [x] Baidu/Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] 自定义翻译接口
|
||||
- [x] 覆盖常见翻译场景
|
||||
- [x] 网页双语对照翻译
|
||||
@@ -50,6 +60,8 @@
|
||||
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] Safari (Mac) 第三方编译,未作验证,自行获取: https://www.nodeloc.com/t/topic/54245
|
||||
- [x] Thunderbird [下载地址](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] 油猴脚本
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
@@ -67,16 +79,111 @@
|
||||
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
|
||||
- 自己部署,自己管理。
|
||||
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
||||
- 搭配本项目一起使用的划词翻译插件。
|
||||
- 支持英文单词、句子、汉字的查询。
|
||||
- 支持历史记录、单词收藏。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何关闭自动翻译
|
||||
|
||||
通过规则设置,以下方法均可实现:
|
||||
|
||||
- 个人规则:全局规则 -> 开启翻译 -> 默认关闭
|
||||
- 订阅规则:选择第三个 `kiss-rules-off.json`
|
||||
- 覆写订阅规则:开启翻译 -> 默认关闭
|
||||
- 添加一条针对某个网站的个人规则:开启翻译 -> 默认关闭
|
||||
|
||||
### 如何设置快捷键
|
||||
|
||||
在插件管理那里设置,例如:
|
||||
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
### 如何关闭划词翻译
|
||||
|
||||
通过规则设置:个人规则 -> 全局规则 -> 是否启用划词翻译 -> 禁用
|
||||
|
||||
### 如何设置仅显示译文
|
||||
|
||||
通过规则设置:个人规则 -> 全局规则 -> 仅显示译文 -> 启用
|
||||
|
||||
### 如何设置鼠标悬停翻译
|
||||
|
||||
通过规则设置:个人规则 -> 全局规则 -> 触发方式
|
||||
|
||||
### 为什么有些网页翻译不全
|
||||
|
||||
本插件的网页翻译是基于CSS选择器的,通用规则不能适配所有网页,有时需要自行添加相应网站的单独规则。如果不会写规则,可以到这里求助: https://github.com/fishjar/kiss-rules/issues
|
||||
|
||||
### 规则设置的优先级是如何的
|
||||
|
||||
个人规则 > 覆写订阅规则 > 订阅规则 > 全局规则
|
||||
|
||||
其中全局规则优先级最低,但非常重要,相当于默认规则。
|
||||
|
||||
### 为什么油管字幕一句话会断开翻译
|
||||
|
||||
本插件目前没有针对视频做特殊开发,对油管的支持也是当做网页翻译看待,自动生成字幕是流式生成并输出的,所以支持较差。
|
||||
|
||||
如果需要关闭本插件的字幕翻译,增加一条规则即可,参考:https://github.com/fishjar/kiss-translator/issues/62
|
||||
|
||||
### 本地的Ollama接口不能使用
|
||||
|
||||
如果出现403的情况,参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### 填写的接口在油猴脚本不能使用
|
||||
|
||||
油猴脚本需要增加域名白名单,否则不能发出请求。
|
||||
|
||||
### 如何设置自定义接口的hook函数
|
||||
|
||||
自定义接口功能非常灵活,理论可以接入任何翻译接口。
|
||||
|
||||
Request Hook 函数示例如下:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Request Hook
|
||||
* @param {string} text 需要翻译的原文
|
||||
* @param {string} from 原文语言
|
||||
* @param {string} to 译文语言
|
||||
* @param {string} url 翻译接口地址
|
||||
* @param {string} key 翻译接口密钥
|
||||
* @returns {Array[string, object]} [接口地址, 请求参数对象]
|
||||
*/
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
method: "POST",
|
||||
body: { text, to },
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook 函数示例如下:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Request Hook
|
||||
* @param {string} res 接口返回的json数据
|
||||
* @param {string} text 需要翻译的原文
|
||||
* @param {string} from 原文语言
|
||||
* @param {string} to 译文语言
|
||||
* @returns {Array[string, boolean]} [译文, 译文语言与原文语言是否相同]
|
||||
* 注:如果返回值第二个值为true(译文语言与原文语言相同)则译文不会在页面显示,
|
||||
* 参数不全的情况建议直接返回false
|
||||
*/
|
||||
(res, text, from, to) => [res.text, to === res.src]
|
||||
```
|
||||
|
||||
更多的自定义接口示例,请参考: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
|
||||
|
||||
## 开发指引
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
git checkout dev # 提交PR建议推送到dev分支
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
@@ -92,6 +92,7 @@ const userscriptWebpack = (config, env) => {
|
||||
// @grant GM.info
|
||||
// @grant unsafeWindow
|
||||
// @connect translate.googleapis.com
|
||||
// @connect translate-pa.googleapis.com
|
||||
// @connect api-edge.cognitive.microsofttranslator.com
|
||||
// @connect edge.microsoft.com
|
||||
// @connect api-free.deepl.com
|
||||
@@ -108,10 +109,10 @@ const userscriptWebpack = (config, env) => {
|
||||
// @connect dav.jianguoyun.com
|
||||
// @connect fanyi.baidu.com
|
||||
// @connect transmart.qq.com
|
||||
// @connect localhost:3000
|
||||
// @connect 127.0.0.1:3000
|
||||
// @connect localhost:1188
|
||||
// @connect 127.0.0.1:1188
|
||||
// @connect niutrans.com
|
||||
// @connect translate.volcengine.com
|
||||
// @connect localhost
|
||||
// @connect 127.0.0.1
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
346
custom-api.md
Normal file
346
custom-api.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# 自定义接口示例
|
||||
|
||||
以下示例为网友提供,仅供学习参考。
|
||||
|
||||
## 本地运行 Seed-X-PPO-7B 量化模型
|
||||
|
||||
> 由网友 emptyghost6 提供,来源:https://linux.do/t/topic/828257
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
http://localhost:8000/v1/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => {
|
||||
// 模型支持的语言代码到完整名称的映射
|
||||
const langFullNameMap = {
|
||||
ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
|
||||
cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
|
||||
da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
|
||||
de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
|
||||
en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
|
||||
es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
|
||||
fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
|
||||
};
|
||||
|
||||
// 将 Hook 系统的语言代码转换为模型 API 支持的代码
|
||||
const getModelLangCode = (lang) => {
|
||||
if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
|
||||
return lang;
|
||||
};
|
||||
|
||||
const sourceLangCode = getModelLangCode(from);
|
||||
const targetLangCode = getModelLangCode(to);
|
||||
|
||||
const sourceLangName = langFullNameMap[sourceLangCode] || from;
|
||||
const targetLangName = langFullNameMap[targetLangCode] || to;
|
||||
|
||||
const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;
|
||||
|
||||
// 构建请求体对象
|
||||
const bodyObject = {
|
||||
model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
|
||||
prompt: prompt,
|
||||
max_tokens: 2048,
|
||||
temperature: 0.0,
|
||||
};
|
||||
|
||||
// 返回最终的请求配置
|
||||
return [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// 关键改动:将 JavaScript 对象转换为 JSON 字符串
|
||||
body: JSON.stringify(bodyObject),
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => {
|
||||
// 检查返回是否有效
|
||||
if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {
|
||||
|
||||
// 提取译文并去除可能存在的前后空格
|
||||
const translatedText = res.choices[0].text.trim();
|
||||
|
||||
// 比较原文与译文,相同为 true,否则为 false。
|
||||
const areTextsIdentical = text.trim() === translatedText;
|
||||
|
||||
// 返回数组:[翻译后的文本, 是否与原文相同]
|
||||
return [translatedText, areTextsIdentical];
|
||||
}
|
||||
// 如果响应格式不正确或没有结果,则抛出错误
|
||||
throw new Error("Invalid API response format or no translation found.");
|
||||
}
|
||||
```
|
||||
|
||||
## 接入 openrouter
|
||||
|
||||
> 由网友 Rick Sanchez 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://openrouter.ai/api/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${key}`,
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": //可自定义你的提示词
|
||||
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.
|
||||
|
||||
## Translation Rules
|
||||
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
|
||||
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
|
||||
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
|
||||
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
|
||||
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
|
||||
6. Maintain the same level of formality or informality as the original.
|
||||
|
||||
Source Text: ${text}
|
||||
|
||||
Translated Text:`
|
||||
}
|
||||
]
|
||||
})
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [
|
||||
res.choices?.[0]?.message?.content ?? "",
|
||||
false
|
||||
]
|
||||
```
|
||||
|
||||
## 接入 gemini-2.5-flash, 关闭思考模式, 去审查
|
||||
|
||||
> 由网友 Rick Sanchez 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://generativelanguage.googleapis.com/v1beta/models
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"generationConfig": {
|
||||
"temperature": 0.8,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
|
||||
},
|
||||
},
|
||||
"safetySettings": [
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
}
|
||||
],
|
||||
"contents": [{
|
||||
"parts": [{
|
||||
"text": `自定义提示词`
|
||||
}]
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [
|
||||
res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
|
||||
false
|
||||
]
|
||||
```
|
||||
|
||||
## 接入 Qwen-MT
|
||||
|
||||
> 由网友 atom 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => {
|
||||
const mapLanguageCode = (lang) => ({
|
||||
'zh-CN': 'zh',
|
||||
'zh-TW': 'zh_tw',
|
||||
})[lang] || lang;
|
||||
|
||||
const targetLang = mapLanguageCode(to);
|
||||
|
||||
return [
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "qwen-mt-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": text
|
||||
}
|
||||
],
|
||||
"translation_options": {
|
||||
"source_lang": "auto",
|
||||
"target_lang": targetLang
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
|
||||
```
|
||||
|
||||
|
||||
## 接入 deepl 接口
|
||||
|
||||
> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
target_lang: "ZH",
|
||||
source_lang: "auto",
|
||||
}),
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
|
||||
```
|
||||
|
||||
## 接入智谱AI大模型
|
||||
|
||||
> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": key
|
||||
},
|
||||
"body": JSON.stringify({
|
||||
"model": "glm-4-flash",
|
||||
"messages": [
|
||||
{
|
||||
"role":"system",
|
||||
"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
|
||||
}
|
||||
]
|
||||
})
|
||||
}]
|
||||
```
|
||||
|
||||
## 接入谷歌新接口
|
||||
|
||||
> 由网友 Bush2021 提供,来源:https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://translate-pa.googleapis.com/v1/translateHtml
|
||||
```
|
||||
|
||||
KEY
|
||||
|
||||
```sh
|
||||
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json+protobuf",
|
||||
"X-Goog-API-Key": key
|
||||
},
|
||||
body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
|
||||
```
|
||||
|
||||
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.8.8",
|
||||
"version": "1.9.2",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"sval": "^0.5.2",
|
||||
"webdav": "^5.3.0",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
@@ -25,13 +26,16 @@
|
||||
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
|
||||
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
|
||||
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
||||
"build:thunderbird": "rm -rf build/thunderbird && BUILD_PATH=./build/thunderbird REACT_APP_CLIENT=thunderbird react-app-rewired build && rm build/thunderbird/content.html && cp ./build/thunderbird/manifest.thunderbird.json ./build/thunderbird/manifest.json && rm build/*/manifest.thunderbird.json",
|
||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
|
||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||
"build:rules": "babel-node src/rules.js",
|
||||
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||
"pack": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .",
|
||||
"build": "pnpm format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||
"zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && zip -r userscript.zip userscript && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
|
||||
"build+zip": "pnpm build && pnpm zip",
|
||||
"format": "prettier --write \"**/*.{js,json,html}\"",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
@@ -43,7 +47,8 @@
|
||||
"globals": {
|
||||
"GM": true,
|
||||
"unsafeWindow": true,
|
||||
"globalThis": true
|
||||
"globalThis": true,
|
||||
"messenger": true
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -63,6 +68,7 @@
|
||||
"@babel/node": "^7.22.19",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"prettier": "3.6.2",
|
||||
"react-app-rewired": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
12138
pnpm-lock.yaml
generated
12138
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// (() => {
|
||||
// var shadow = document.querySelector("#shadow1");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
@@ -54,8 +54,8 @@
|
||||
// }, 1000);
|
||||
|
||||
setTimeout(function () {
|
||||
var el = document.querySelector("h2>p>span");
|
||||
el.innerText = "hello world";
|
||||
var el = document.querySelector('h2>p>span');
|
||||
el.innerText = 'hello world';
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
@@ -105,28 +105,26 @@
|
||||
<br />
|
||||
<div id="content">
|
||||
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is
|
||||
coming to Google Cloud with the C3A instances.
|
||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is coming to
|
||||
Google Cloud with the C3A instances.
|
||||
<br />
|
||||
But these upcoming instances for now are only in private preview form.
|
||||
<br />
|
||||
<br />
|
||||
Needless to say I also haven't had any AmpereOne access to check out the
|
||||
performance and power efficiency of these new Arm server processors from
|
||||
Ampere Computing.
|
||||
performance and power efficiency of these new Arm server processors from Ampere
|
||||
Computing.
|
||||
<br />
|
||||
</div>
|
||||
<h2>
|
||||
<p>
|
||||
<span
|
||||
>React is a JavaScript library for building user interfaces.</span
|
||||
>
|
||||
<span>React is a JavaScript library for building user interfaces.</span>
|
||||
</p>
|
||||
</h2>
|
||||
<hr />
|
||||
<input id="input1" style="width: 80%;" />
|
||||
<input id="input1" style="width: 80%" />
|
||||
<hr />
|
||||
<textarea id="textarea1" style="width: 80%;">test</textarea>
|
||||
<textarea id="textarea1" style="width: 80%">test</textarea>
|
||||
<hr />
|
||||
<div id="addtitle"></div>
|
||||
<h2>Shadow 1</h2>
|
||||
@@ -166,15 +164,47 @@
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
React Server Components (or RSC) is a new application architecture
|
||||
designed by the React team.
|
||||
React Server Components (or RSC) is a new application architecture designed by the
|
||||
React team.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe1"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="http://localhost:3000/index.html"
|
||||
></iframe>
|
||||
src="http://localhost:3000/index.html"></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>We’ve first shared our research on RSC in an introductory talk and an RFC.</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -208,52 +238,10 @@
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
We’ve first shared our research on RSC in an introductory talk and an
|
||||
RFC.
|
||||
To recap them, we are introducing a new kind of component—Server Components—that
|
||||
run ahead of time and are excluded from your JavaScript bundle.
|
||||
</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
To recap them, we are introducing a new kind of component—Server
|
||||
Components—that run ahead of time and are excluded from your JavaScript
|
||||
bundle.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe2"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="https://react.dev/"
|
||||
></iframe>
|
||||
<iframe id="iframe2" width="800px" height="600px" src="https://react.dev/"></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -288,15 +276,14 @@
|
||||
<br />
|
||||
<div class="cont cont1">
|
||||
<h2>
|
||||
Server Components can run during the build, letting you read from the
|
||||
filesystem or fetch static content.
|
||||
Server Components can run during the build, letting you read from the filesystem
|
||||
or fetch static content.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
They can also run on the server, letting you access your data layer
|
||||
without having to build an API. You can pass data by props from
|
||||
Server Components to the interactive Client Components in the
|
||||
browser.
|
||||
They can also run on the server, letting you access your data layer without
|
||||
having to build an API. You can pass data by props from Server Components to
|
||||
the interactive Client Components in the browser.
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
@@ -315,14 +302,14 @@
|
||||
<br />
|
||||
<div class="cont cont2">
|
||||
<h2>
|
||||
Since our last update, we have merged the React Server Components RFC
|
||||
to ratify the proposal.
|
||||
Since our last update, we have merged the React Server Components RFC to ratify
|
||||
the proposal.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
RSC combines the simple “request/response” mental model of
|
||||
server-centric Multi-Page Apps with the seamless interactivity of
|
||||
client-centric Single-Page Apps, giving you the best of both worlds.
|
||||
RSC combines the simple “request/response” mental model of server-centric
|
||||
Multi-Page Apps with the seamless interactivity of client-centric Single-Page
|
||||
Apps, giving you the best of both worlds.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.8.8",
|
||||
"version": "1.9.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.8.8",
|
||||
"version": "1.9.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -45,12 +45,7 @@
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"scripting",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
|
||||
78
public/manifest.thunderbird.json
Normal file
78
public/manifest.thunderbird.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.9.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "yugang2002@gmail.com",
|
||||
"strict_min_version": "78.0"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"storage",
|
||||
"menus",
|
||||
"messagesModify",
|
||||
"scripting",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
"48": "images/logo48.png",
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"default_title": "__MSG_app_name__",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import queryString from "query-string";
|
||||
import { fetchData } from "../libs/fetch";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
@@ -9,9 +10,17 @@ import {
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
@@ -19,6 +28,8 @@ import {
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
URL_CACHE_TRAN,
|
||||
KV_SALT_SYNC,
|
||||
URL_GOOGLE_TRAN,
|
||||
URL_MICROSOFT_LANGDETECT,
|
||||
URL_BAIDU_LANGDETECT,
|
||||
URL_BAIDU_SUGGEST,
|
||||
URL_BAIDU_TTS,
|
||||
@@ -26,8 +37,12 @@ import {
|
||||
URL_TENCENT_TRANSMART,
|
||||
OPT_LANGS_TENCENT,
|
||||
OPT_LANGS_SPECIAL,
|
||||
OPT_LANGS_MICROSOFT,
|
||||
} from "../config";
|
||||
import { sha256 } from "../libs/utils";
|
||||
import interpreter from "../libs/interpreter";
|
||||
import { msAuth } from "../libs/auth";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
* 同步数据
|
||||
@@ -53,6 +68,52 @@ export const apiSyncData = async (url, key, data) =>
|
||||
*/
|
||||
export const apiFetch = (url) => fetchData(url);
|
||||
|
||||
/**
|
||||
* Google语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiGoogleLangdetect = async (text) => {
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
dj: 1,
|
||||
ie: "UTF-8",
|
||||
sl: "auto",
|
||||
tl: "zh-CN",
|
||||
q: text,
|
||||
};
|
||||
const input = `${URL_GOOGLE_TRAN}?${queryString.stringify(params)}`;
|
||||
const res = await fetchData(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
return res.src;
|
||||
};
|
||||
|
||||
/**
|
||||
* Microsoft语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiMicrosoftLangdetect = async (text) => {
|
||||
const [token] = await msAuth();
|
||||
const res = await fetchData(URL_MICROSOFT_LANGDETECT, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
return OPT_LANGS_MICROSOFT.get(res[0].language) ?? res[0].language;
|
||||
};
|
||||
|
||||
/**
|
||||
* 百度语言识别
|
||||
* @param {*} text
|
||||
@@ -166,7 +227,7 @@ export const apiTranslate = async ({
|
||||
OPT_LANGS_SPECIAL[translator].get("auto");
|
||||
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
||||
if (!to) {
|
||||
console.log(`[trans] target lang: ${toLang} not support`);
|
||||
kissLog(`target lang: ${toLang} not support`, "translate");
|
||||
return [trText, isSame];
|
||||
}
|
||||
|
||||
@@ -202,6 +263,10 @@ export const apiTranslate = async ({
|
||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
||||
isSame = to === res.src;
|
||||
break;
|
||||
case OPT_TRANS_GOOGLE_2:
|
||||
trText = res?.[0]?.[0] || "";
|
||||
isSame = to === res.src;
|
||||
break;
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
trText = res
|
||||
.map((item) => item.translations.map((item) => item.text).join(" "))
|
||||
@@ -240,10 +305,17 @@ export const apiTranslate = async ({
|
||||
}
|
||||
break;
|
||||
case OPT_TRANS_TENCENT:
|
||||
trText = res.auto_translation;
|
||||
trText = res?.auto_translation?.[0];
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_VOLCENGINE:
|
||||
trText = res?.translation || "";
|
||||
isSame = to === res?.detected_language;
|
||||
break;
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_OPENAI_2:
|
||||
case OPT_TRANS_OPENAI_3:
|
||||
case OPT_TRANS_GEMINI_2:
|
||||
trText = res?.choices?.map((item) => item.message.content).join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
@@ -253,36 +325,39 @@ export const apiTranslate = async ({
|
||||
.join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CLAUDE:
|
||||
trText = res?.content?.map((item) => item.text).join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
trText = res?.result?.translated_text;
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_OLLAMA:
|
||||
case OPT_TRANS_OLLAMA_2:
|
||||
case OPT_TRANS_OLLAMA_3:
|
||||
const { thinkIgnore = "" } = apiSetting;
|
||||
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
|
||||
if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||
trText = res?.response.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||
} else {
|
||||
trText = res?.response;
|
||||
}
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
case OPT_TRANS_CUSTOMIZE_2:
|
||||
case OPT_TRANS_CUSTOMIZE_3:
|
||||
case OPT_TRANS_CUSTOMIZE_4:
|
||||
case OPT_TRANS_CUSTOMIZE_5:
|
||||
trText = res.text;
|
||||
isSame = to === res.from;
|
||||
|
||||
const { customOption } = apiSetting;
|
||||
if (customOption?.trim()) {
|
||||
try {
|
||||
const opt = JSON.parse(customOption);
|
||||
const textPattern = opt.resPattern?.text;
|
||||
const fromPattern = opt.resPattern?.from;
|
||||
if (textPattern) {
|
||||
trText = textPattern.split(".").reduce((pre, cur) => pre[cur], res);
|
||||
}
|
||||
if (fromPattern) {
|
||||
isSame =
|
||||
to === fromPattern.split(".").reduce((pre, cur) => pre[cur], res);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`custom option parse err: ${err}`);
|
||||
}
|
||||
const { resHook } = apiSetting;
|
||||
if (resHook?.trim()) {
|
||||
interpreter.run(`exports.resHook = ${resHook}`);
|
||||
[trText, isSame] = interpreter.exports.resHook(res, text, from, to);
|
||||
} else {
|
||||
trText = res.text;
|
||||
isSame = to === res.from;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import queryString from "query-string";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
@@ -8,9 +9,17 @@ import {
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
@@ -18,15 +27,18 @@ import {
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
URL_MICROSOFT_TRAN,
|
||||
URL_TENCENT_TRANSMART,
|
||||
URL_VOLCENGINE_TRAN,
|
||||
INPUT_PLACE_URL,
|
||||
INPUT_PLACE_FROM,
|
||||
INPUT_PLACE_TO,
|
||||
INPUT_PLACE_TEXT,
|
||||
INPUT_PLACE_KEY,
|
||||
INPUT_PLACE_MODEL,
|
||||
} from "../config";
|
||||
import { msAuth } from "../libs/auth";
|
||||
import { genDeeplFree } from "./deepl";
|
||||
import { genBaidu } from "./baidu";
|
||||
import interpreter from "../libs/interpreter";
|
||||
|
||||
const keyMap = new Map();
|
||||
const urlMap = new Map();
|
||||
@@ -72,6 +84,20 @@ const genGoogle = ({ text, from, to, url, key }) => {
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
const genGoogle2 = ({ text, from, to, url, key }) => {
|
||||
const body = JSON.stringify([[[text], from, to], "wt_lib"]);
|
||||
const init = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json+protobuf",
|
||||
"X-Goog-API-Key": key,
|
||||
},
|
||||
body,
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genMicrosoft = async ({ text, from, to }) => {
|
||||
const [token] = await msAuth();
|
||||
const params = {
|
||||
@@ -156,10 +182,14 @@ const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
|
||||
const genTencent = ({ text, from, to }) => {
|
||||
const data = {
|
||||
header: {
|
||||
fn: "auto_translation_block",
|
||||
fn: "auto_translation",
|
||||
client_key:
|
||||
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
|
||||
},
|
||||
type: "plain",
|
||||
model_category: "normal",
|
||||
source: {
|
||||
text_block: text,
|
||||
text_list: [text],
|
||||
lang: from,
|
||||
},
|
||||
target: {
|
||||
@@ -170,6 +200,9 @@ const genTencent = ({ text, from, to }) => {
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
|
||||
referer: "https://transmart.qq.com/zh-CN/index",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
@@ -178,25 +211,63 @@ const genTencent = ({ text, from, to }) => {
|
||||
return [URL_TENCENT_TRANSMART, init];
|
||||
};
|
||||
|
||||
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
|
||||
prompt = prompt
|
||||
const genVolcengine = ({ text, from, to }) => {
|
||||
const data = {
|
||||
source_language: from,
|
||||
target_language: to,
|
||||
text: text,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [URL_VOLCENGINE_TRAN, init];
|
||||
};
|
||||
|
||||
const genOpenAI = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
// 兼容历史上作为systemPrompt的prompt,如果prompt中不包含带翻译文本,则添加文本到prompt末尾
|
||||
// if (!prompt.includes(INPUT_PLACE_TEXT)) {
|
||||
// prompt += `\nSource Text: ${INPUT_PLACE_TEXT}`;
|
||||
// }
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to);
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
temperature,
|
||||
max_completion_tokens: maxTokens,
|
||||
};
|
||||
|
||||
const init = {
|
||||
@@ -212,26 +283,50 @@ const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genGemini = ({ text, from, to, url, key, prompt, model }) => {
|
||||
prompt = prompt
|
||||
const genGemini = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
url = url
|
||||
.replaceAll(INPUT_PLACE_MODEL, model)
|
||||
.replaceAll(INPUT_PLACE_KEY, key);
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
contents: [
|
||||
{
|
||||
// role: "user",
|
||||
parts: [
|
||||
{
|
||||
text: prompt,
|
||||
},
|
||||
],
|
||||
system_instruction: {
|
||||
parts: {
|
||||
text: systemPrompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
contents: {
|
||||
role: "user",
|
||||
parts: {
|
||||
text: userPrompt,
|
||||
},
|
||||
},
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature,
|
||||
// topP: 0.8,
|
||||
// topK: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const input = `${url}/${model}:generateContent?key=${key}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
@@ -240,7 +335,145 @@ const genGemini = ({ text, from, to, url, key, prompt, model }) => {
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [input, init];
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genGemini2 = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genClaude = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"x-api-key": key,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genOllama = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
think,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
}) => {
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
think: think,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genCloudflareAI = ({ text, from, to, url, key }) => {
|
||||
@@ -262,20 +495,27 @@ const genCloudflareAI = ({ text, from, to, url, key }) => {
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genCustom = ({ text, from, to, url, key, customOption }) => {
|
||||
const replaceInput = (str) =>
|
||||
str
|
||||
.replaceAll(INPUT_PLACE_URL, url)
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text.replaceAll(`"`, `\n`))
|
||||
.replaceAll(INPUT_PLACE_KEY, key);
|
||||
const genCustom = ({ text, from, to, url, key, reqHook }) => {
|
||||
url = url
|
||||
.replaceAll(INPUT_PLACE_URL, url)
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text)
|
||||
.replaceAll(INPUT_PLACE_KEY, key);
|
||||
let init = {};
|
||||
|
||||
if (reqHook?.trim()) {
|
||||
interpreter.run(`exports.reqHook = ${reqHook}`);
|
||||
[url, init] = interpreter.exports.reqHook(text, from, to, url, key);
|
||||
return [url, init];
|
||||
}
|
||||
|
||||
const data = {
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
const init = {
|
||||
init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
@@ -285,23 +525,6 @@ const genCustom = ({ text, from, to, url, key, customOption }) => {
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
url = replaceInput(url);
|
||||
|
||||
if (customOption?.trim()) {
|
||||
try {
|
||||
const opt = JSON.parse(replaceInput(customOption));
|
||||
opt.url && (url = opt.url);
|
||||
opt.headers && (init.headers = opt.headers);
|
||||
opt.method && (init.method = opt.method);
|
||||
if (init.method === "GET") {
|
||||
delete init.body;
|
||||
} else {
|
||||
opt.body && (init.body = JSON.stringify(opt.body));
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`custom option parse err: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
@@ -317,9 +540,21 @@ export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
|
||||
switch (translator) {
|
||||
case OPT_TRANS_DEEPL:
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_OPENAI_2:
|
||||
case OPT_TRANS_OPENAI_3:
|
||||
case OPT_TRANS_GEMINI:
|
||||
case OPT_TRANS_GEMINI_2:
|
||||
case OPT_TRANS_CLAUDE:
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
case OPT_TRANS_OLLAMA:
|
||||
case OPT_TRANS_OLLAMA_2:
|
||||
case OPT_TRANS_OLLAMA_3:
|
||||
case OPT_TRANS_NIUTRANS:
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
case OPT_TRANS_CUSTOMIZE_2:
|
||||
case OPT_TRANS_CUSTOMIZE_3:
|
||||
case OPT_TRANS_CUSTOMIZE_4:
|
||||
case OPT_TRANS_CUSTOMIZE_5:
|
||||
args.key = keyPick(translator, args.key, keyMap);
|
||||
break;
|
||||
case OPT_TRANS_DEEPLX:
|
||||
@@ -331,6 +566,8 @@ export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
return genGoogle(args);
|
||||
case OPT_TRANS_GOOGLE_2:
|
||||
return genGoogle2(args);
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
return genMicrosoft(args);
|
||||
case OPT_TRANS_DEEPL:
|
||||
@@ -345,12 +582,24 @@ export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
|
||||
return genBaidu(args);
|
||||
case OPT_TRANS_TENCENT:
|
||||
return genTencent(args);
|
||||
case OPT_TRANS_VOLCENGINE:
|
||||
return genVolcengine(args);
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_OPENAI_2:
|
||||
case OPT_TRANS_OPENAI_3:
|
||||
return genOpenAI(args);
|
||||
case OPT_TRANS_GEMINI:
|
||||
return genGemini(args);
|
||||
case OPT_TRANS_GEMINI_2:
|
||||
return genGemini2(args);
|
||||
case OPT_TRANS_CLAUDE:
|
||||
return genClaude(args);
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
return genCloudflareAI(args);
|
||||
case OPT_TRANS_OLLAMA:
|
||||
case OPT_TRANS_OLLAMA_2:
|
||||
case OPT_TRANS_OLLAMA_3:
|
||||
return genOllama(args);
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
case OPT_TRANS_CUSTOMIZE_2:
|
||||
case OPT_TRANS_CUSTOMIZE_3:
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CMD_TOGGLE_STYLE,
|
||||
CMD_OPEN_OPTIONS,
|
||||
CMD_OPEN_TRANBOX,
|
||||
CLIENT_THUNDERBIRD,
|
||||
} from "./config";
|
||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||
import { trySyncSettingAndRules } from "./libs/sync";
|
||||
@@ -46,7 +47,7 @@ async function addContextMenus(contextMenuType = 1) {
|
||||
try {
|
||||
await browser.contextMenus.removeAll();
|
||||
} catch (err) {
|
||||
//
|
||||
kissLog(err, "remove contextMenus");
|
||||
}
|
||||
|
||||
switch (contextMenuType) {
|
||||
@@ -123,12 +124,26 @@ async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册邮件显示脚本
|
||||
*/
|
||||
async function registerMsgDisplayScript() {
|
||||
await messenger.messageDisplayScripts.register({
|
||||
js: [{ file: "/content.js" }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安装
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
tryInitDefaultData();
|
||||
|
||||
//在thunderbird中注册脚本
|
||||
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||
registerMsgDisplayScript();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
addContextMenus();
|
||||
|
||||
@@ -151,6 +166,11 @@ browser.runtime.onStartup.addListener(async () => {
|
||||
tryClearCaches();
|
||||
}
|
||||
|
||||
//在thunderbird中注册脚本
|
||||
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||
registerMsgDisplayScript();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
// firefox重启后菜单会消失,故重复添加
|
||||
addContextMenus(contextMenuType);
|
||||
|
||||
@@ -72,7 +72,7 @@ function runtimeListener(translator) {
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { data: translator.rule };
|
||||
return { rule: translator.rule, setting: translator.setting };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,14 +135,18 @@ async function showFab(translator) {
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
function showTransbox({
|
||||
contextMenuType,
|
||||
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
||||
transApis,
|
||||
darkMode,
|
||||
uiLang,
|
||||
}) {
|
||||
if (!tranboxSetting?.transOpen) {
|
||||
function showTransbox(
|
||||
{
|
||||
contextMenuType,
|
||||
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
||||
transApis,
|
||||
darkMode,
|
||||
uiLang,
|
||||
langDetector,
|
||||
},
|
||||
{ transSelected }
|
||||
) {
|
||||
if (transSelected === "false") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,6 +176,7 @@ function showTransbox({
|
||||
tranboxSetting={tranboxSetting}
|
||||
transApis={transApis}
|
||||
uiLang={uiLang}
|
||||
langDetector={langDetector}
|
||||
/>
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
@@ -249,7 +254,7 @@ export async function run(isUserscript = false) {
|
||||
inputTranslate(setting);
|
||||
|
||||
// 划词翻译
|
||||
showTransbox(setting);
|
||||
showTransbox(setting, rule);
|
||||
|
||||
// 浮球按钮
|
||||
await showFab(translator);
|
||||
|
||||
@@ -42,7 +42,8 @@ const customApiLangs = `["en", "English - English"],
|
||||
["vi", "Vietnamese - Tiếng Việt"],
|
||||
`;
|
||||
|
||||
const customDefaultOption = `{
|
||||
const customApiHelpZH = `// 请求数据默认格式
|
||||
{
|
||||
"url": "{{url}}",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
@@ -50,18 +51,12 @@ const customDefaultOption = `{
|
||||
"Authorization": "Bearer {{key}}"
|
||||
},
|
||||
"body": {
|
||||
"text": "{{text}}",
|
||||
"from": "{{from}}",
|
||||
"to": "{{to}}"
|
||||
"text": "{{text}}", // 待翻译文字
|
||||
"from": "{{from}}", // 文字的语言(可能为空)
|
||||
"to": "{{to}}", // 目标语言
|
||||
},
|
||||
"resPattern": {
|
||||
"text": "text",
|
||||
"from": "from"
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
const customApiHelpZH = `// 自定义选项范例
|
||||
${customDefaultOption}
|
||||
|
||||
// 返回数据默认格式
|
||||
{
|
||||
@@ -70,20 +65,72 @@ ${customDefaultOption}
|
||||
to: "", // 目标语言(可选)
|
||||
}
|
||||
|
||||
|
||||
// Hook 范例
|
||||
// URL
|
||||
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||
|
||||
// Request Hook
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
body: null,
|
||||
}]
|
||||
|
||||
// Response Hook
|
||||
// 其中返回数组第一个值表示译文字符串,第二个值为布尔值,表示原文语言与目标语言是否相同
|
||||
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
|
||||
|
||||
|
||||
// 支持的语言代码如下
|
||||
${customApiLangs}
|
||||
`;
|
||||
|
||||
const customApiHelpEN = `// Example of custom options
|
||||
${customDefaultOption}
|
||||
const customApiHelpEN = `// Default request
|
||||
{
|
||||
"url": "{{url}}",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": "Bearer {{key}}"
|
||||
},
|
||||
"body": {
|
||||
"text": "{{text}}", // Text to be translated
|
||||
"from": "{{from}}", // The language of the text (may be empty)
|
||||
"to": "{{to}}", // Target language
|
||||
},
|
||||
}
|
||||
|
||||
// Return data default format
|
||||
|
||||
// Default response
|
||||
{
|
||||
text: "", // translated text
|
||||
from: "", // Recognized source language
|
||||
to: "", // Target language (optional)
|
||||
}
|
||||
|
||||
|
||||
/// Hook Example
|
||||
// URL
|
||||
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||
|
||||
// Request Hook
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
body: null,
|
||||
}]
|
||||
|
||||
// Response Hook
|
||||
// In the returned array, the first value is the translated string, while the second value is a boolean
|
||||
// that indicates whether the source language is the same as the target language.
|
||||
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
|
||||
|
||||
|
||||
// The supported language codes are as follows
|
||||
${customApiLangs}
|
||||
`;
|
||||
@@ -153,6 +200,22 @@ export const I18N = {
|
||||
zh: `最大并发请求数量 (1-100)`,
|
||||
en: `Maximum Number Of Concurrent Requests (1-100)`,
|
||||
},
|
||||
if_think: {
|
||||
zh: `启用或禁用模型的深度思考能力`,
|
||||
en: `Enable or disable the model’s thinking behavior `,
|
||||
},
|
||||
think: {
|
||||
zh: `启用深度思考`,
|
||||
en: `enable thinking`,
|
||||
},
|
||||
nothink: {
|
||||
zh: `禁用深度思考`,
|
||||
en: `disable thinking`,
|
||||
},
|
||||
think_ignore: {
|
||||
zh: `忽略以下模型的<think>输出,逗号(,)分割,当模型支持思考但ollama不支持时需要填写本参数`,
|
||||
en: `Ignore the <think> block for the following models, comma (,) separated`,
|
||||
},
|
||||
fetch_interval: {
|
||||
zh: `每次请求间隔时间 (0-5000ms)`,
|
||||
en: `Time Between Requests (0-5000ms)`,
|
||||
@@ -161,6 +224,10 @@ export const I18N = {
|
||||
zh: `重新翻译间隔时间 (100-5000ms)`,
|
||||
en: `Retranslation Interval (100-5000ms)`,
|
||||
},
|
||||
http_timeout: {
|
||||
zh: `请求超时时间 (5000-30000ms)`,
|
||||
en: `Request Timeout Time (5000-30000ms)`,
|
||||
},
|
||||
min_translate_length: {
|
||||
zh: `最小翻译字符数 (1-100)`,
|
||||
en: `Minimum number Of Translated Characters (1-100)`,
|
||||
@@ -306,12 +373,20 @@ export const I18N = {
|
||||
en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`,
|
||||
},
|
||||
sync_warn: {
|
||||
zh: `涉及隐私数据的同步请谨慎选择第三方同步服务,建议自行搭建 kiss-worker 或 WebDAV 服务。`,
|
||||
en: `When synchronizing data that involves privacy, please be cautious about choosing third-party sync services. It is recommended to set up your own sync service using kiss-worker or WebDAV.`,
|
||||
},
|
||||
sync_warn_2: {
|
||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
||||
},
|
||||
about_sync_api: {
|
||||
zh: `查看关于数据同步接口部署`,
|
||||
en: `View About Data Synchronization Interface Deployment`,
|
||||
zh: `自建kiss-wroker数据同步服务`,
|
||||
en: `Self-hosting a Kiss-worker data sync service`,
|
||||
},
|
||||
about_api: {
|
||||
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`,
|
||||
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
||||
},
|
||||
about_api_proxy: {
|
||||
zh: `查看自建一个翻译接口代理`,
|
||||
@@ -394,8 +469,8 @@ export const I18N = {
|
||||
en: `Keep unchanged selector`,
|
||||
},
|
||||
keep_selector_helper: {
|
||||
zh: `1、遵循CSS选择器语法。2、子元素选择器用“>>>”隔开。`,
|
||||
en: `1. Follow CSS selector syntax. 2. Sub-element selectors are separated by ">>>".`,
|
||||
zh: `1、遵循CSS选择器语法。`,
|
||||
en: `1. Follow CSS selector syntax.`,
|
||||
},
|
||||
terms: {
|
||||
zh: `专业术语`,
|
||||
@@ -623,7 +698,7 @@ export const I18N = {
|
||||
},
|
||||
use_simple_style: {
|
||||
zh: `使用简洁界面`,
|
||||
en: `Click outside to close the pop-up window`,
|
||||
en: `Use a simple interface`,
|
||||
},
|
||||
show: {
|
||||
zh: `显示`,
|
||||
@@ -821,6 +896,10 @@ export const I18N = {
|
||||
zh: `更多`,
|
||||
en: `More`,
|
||||
},
|
||||
less: {
|
||||
zh: `更少`,
|
||||
en: `Less`,
|
||||
},
|
||||
fixer_selector: {
|
||||
zh: `网页修复选择器`,
|
||||
en: `Fixer Selector`,
|
||||
@@ -873,4 +952,44 @@ export const I18N = {
|
||||
zh: `翻译框跟随选中文本`,
|
||||
en: `Transbox Follow Selection`,
|
||||
},
|
||||
translate_start_hook: {
|
||||
zh: `翻译开始钩子函数`,
|
||||
en: `Translate Start Hook`,
|
||||
},
|
||||
translate_start_hook_helper: {
|
||||
zh: `翻译开始时运行,入参为: 翻译节点,原文文本。`,
|
||||
en: `Run when translation starts, the input parameters are: translation node, original text.`,
|
||||
},
|
||||
translate_end_hook: {
|
||||
zh: `翻译完成钩子函数`,
|
||||
en: `Translate End Hook`,
|
||||
},
|
||||
translate_end_hook_helper: {
|
||||
zh: `翻译完成时运行,入参为: 翻译节点,原文文本,译文文本,保留元素。`,
|
||||
en: `Run when the translation is completed, the input parameters are: translation node, original text, translation text, retained elements.`,
|
||||
},
|
||||
translate_remove_hook: {
|
||||
zh: `翻译移除钩子函数`,
|
||||
en: `Translate Removed Hook`,
|
||||
},
|
||||
translate_remove_hook_helper: {
|
||||
zh: `翻译移除时运行,入参为: 翻译节点。`,
|
||||
en: `Run when translation is removed, the input parameters are: translation node.`,
|
||||
},
|
||||
english_dict: {
|
||||
zh: `英文词典`,
|
||||
en: `English Dictionary`,
|
||||
},
|
||||
api_name: {
|
||||
zh: `接口名称`,
|
||||
en: `API Name`,
|
||||
},
|
||||
is_disabled: {
|
||||
zh: `是否禁用`,
|
||||
en: `Is Disabled`,
|
||||
},
|
||||
translate_selected: {
|
||||
zh: `是否启用划词翻译`,
|
||||
en: `If translate selected`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,7 +39,13 @@ export const CLIENT_CHROME = "chrome";
|
||||
export const CLIENT_EDGE = "edge";
|
||||
export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||
export const CLIENT_THUNDERBIRD = "thunderbird";
|
||||
export const CLIENT_EXTS = [
|
||||
CLIENT_CHROME,
|
||||
CLIENT_EDGE,
|
||||
CLIENT_FIREFOX,
|
||||
CLIENT_THUNDERBIRD,
|
||||
];
|
||||
|
||||
export const KV_RULES_KEY = "kiss-rules.json";
|
||||
export const KV_WORDS_KEY = "kiss-words.json";
|
||||
@@ -78,9 +84,20 @@ export const URL_RAW_PREFIX =
|
||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||
|
||||
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
||||
|
||||
// api.cognitive.microsofttranslator.com
|
||||
export const URL_MICROSOFT_TRAN =
|
||||
"https://api-edge.cognitive.microsofttranslator.com/translate";
|
||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
||||
export const URL_MICROSOFT_LANGDETECT =
|
||||
"https://api-edge.cognitive.microsofttranslator.com/detect?api-version=3.0";
|
||||
|
||||
export const URL_GOOGLE_TRAN =
|
||||
"https://translate.googleapis.com/translate_a/single";
|
||||
export const URL_GOOGLE_TRAN2 =
|
||||
"https://translate-pa.googleapis.com/v1/translateHtml";
|
||||
export const DEFAULT_GOOGLE_API_KEY = "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520";
|
||||
|
||||
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
|
||||
export const URL_BAIDU_SUGGEST = "https://fanyi.baidu.com/sug";
|
||||
export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts";
|
||||
@@ -89,13 +106,18 @@ export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
|
||||
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
|
||||
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
|
||||
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
|
||||
export const URL_VOLCENGINE_TRAN =
|
||||
"https://translate.volcengine.com/crx/translate/v1";
|
||||
export const URL_NIUTRANS_REG =
|
||||
"https://niutrans.com/login?active=3&userSource=kiss-translator";
|
||||
|
||||
export const DEFAULT_USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||
|
||||
export const OPT_DICT_BAIDU = "Baidu";
|
||||
|
||||
export const OPT_TRANS_GOOGLE = "Google";
|
||||
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||
export const OPT_TRANS_DEEPL = "DeepL";
|
||||
export const OPT_TRANS_DEEPLX = "DeepLX";
|
||||
@@ -103,9 +125,17 @@ export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
||||
export const OPT_TRANS_NIUTRANS = "NiuTrans";
|
||||
export const OPT_TRANS_BAIDU = "Baidu";
|
||||
export const OPT_TRANS_TENCENT = "Tencent";
|
||||
export const OPT_TRANS_VOLCENGINE = "Volcengine";
|
||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||
export const OPT_TRANS_OPENAI_2 = "OpenAI2";
|
||||
export const OPT_TRANS_OPENAI_3 = "OpenAI3";
|
||||
export const OPT_TRANS_GEMINI = "Gemini";
|
||||
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
||||
export const OPT_TRANS_CLAUDE = "Claude";
|
||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||
export const OPT_TRANS_OLLAMA = "Ollama";
|
||||
export const OPT_TRANS_OLLAMA_2 = "Ollama2";
|
||||
export const OPT_TRANS_OLLAMA_3 = "Ollama3";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
export const OPT_TRANS_CUSTOMIZE_2 = "Custom2";
|
||||
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
|
||||
@@ -113,16 +143,25 @@ export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
|
||||
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
|
||||
export const OPT_TRANS_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
@@ -130,6 +169,13 @@ export const OPT_TRANS_ALL = [
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
];
|
||||
|
||||
export const OPT_LANGDETECTOR_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
];
|
||||
|
||||
export const OPT_LANGS_TO = [
|
||||
["en", "English - English"],
|
||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||
@@ -172,6 +218,7 @@ export const OPT_LANGS_TO = [
|
||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||
export const OPT_LANGS_SPECIAL = {
|
||||
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||
[OPT_TRANS_GOOGLE_2]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||
[OPT_TRANS_MICROSOFT]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
@@ -192,7 +239,7 @@ export const OPT_LANGS_SPECIAL = {
|
||||
]),
|
||||
[OPT_TRANS_DEEPLX]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", ""],
|
||||
["auto", "auto"],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
@@ -202,6 +249,12 @@ export const OPT_LANGS_SPECIAL = {
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "cht"],
|
||||
]),
|
||||
[OPT_TRANS_VOLCENGINE]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_BAIDU]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["zh-CN", "zh"],
|
||||
@@ -254,9 +307,30 @@ export const OPT_LANGS_SPECIAL = {
|
||||
[OPT_TRANS_OPENAI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OPENAI_2]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OPENAI_3]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_GEMINI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_GEMINI_2]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CLAUDE]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OLLAMA]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OLLAMA_2]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OLLAMA_3]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CLOUDFLAREAI]: new Map([
|
||||
["auto", ""],
|
||||
["zh-CN", "chinese"],
|
||||
@@ -293,6 +367,12 @@ export const OPT_LANGS_SPECIAL = {
|
||||
]),
|
||||
};
|
||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||
export const OPT_LANGS_MICROSOFT = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_MICROSOFT].entries()).map(([k, v]) => [
|
||||
v,
|
||||
k,
|
||||
])
|
||||
);
|
||||
export const OPT_LANGS_BAIDU = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
|
||||
v,
|
||||
@@ -359,10 +439,11 @@ export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
|
||||
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
|
||||
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
||||
|
||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
|
||||
export const DEFAULT_TRANS_TAG = "span";
|
||||
export const DEFAULT_TRANS_TAG = "font";
|
||||
export const DEFAULT_SELECT_STYLE =
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
|
||||
@@ -387,10 +468,14 @@ export const GLOBLA_RULE = {
|
||||
transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译
|
||||
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
|
||||
transTitle: "false", // 是否同时翻译页面标题
|
||||
transSelected: "true", // 是否启用划词翻译
|
||||
detectRemote: "false", // 是否使用远程语言检测
|
||||
skipLangs: [], // 不翻译的语言
|
||||
fixerSelector: "", // 修复函数选择器
|
||||
fixerFunc: "-", // 修复函数
|
||||
transStartHook: "", // 钩子函数
|
||||
transEndHook: "", // 钩子函数
|
||||
transRemoveHook: "", // 钩子函数
|
||||
};
|
||||
|
||||
// 输入框翻译
|
||||
@@ -422,7 +507,7 @@ export const OPT_TRANBOX_TRIGGER_ALL = [
|
||||
];
|
||||
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
||||
export const DEFAULT_TRANBOX_SETTING = {
|
||||
transOpen: true,
|
||||
// transOpen: true, // 是否启用划词翻译(作废,移至rule)
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "zh-CN",
|
||||
@@ -438,6 +523,7 @@ export const DEFAULT_TRANBOX_SETTING = {
|
||||
followSelection: false, // 翻译框是否跟随选中文本
|
||||
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||
extStyles: "", // 附加样式
|
||||
enDict: OPT_DICT_BAIDU, // 英文词典
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
@@ -456,48 +542,120 @@ export const DEFAULT_SUBRULES_LIST = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_HTTP_TIMEOUT = 5000; // 调用超时时间
|
||||
|
||||
// 翻译接口
|
||||
const defaultCustomApi = {
|
||||
url: "",
|
||||
key: "",
|
||||
customOption: "",
|
||||
customOption: "", // (作废)
|
||||
reqHook: "", // request 钩子函数
|
||||
resHook: "", // response 钩子函数
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: "",
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
};
|
||||
const defaultOpenaiApi = {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
key: "",
|
||||
model: "gpt-4",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 256,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: "",
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
};
|
||||
const defaultOllamaApi = {
|
||||
url: "http://localhost:11434/api/generate",
|
||||
key: "",
|
||||
model: "llama3.1",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
think: false,
|
||||
thinkIgnore: `qwen3,deepseek-r1`,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: "",
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
};
|
||||
export const DEFAULT_TRANS_APIS = {
|
||||
[OPT_TRANS_GOOGLE]: {
|
||||
url: "https://translate.googleapis.com/translate_a/single",
|
||||
url: URL_GOOGLE_TRAN,
|
||||
key: "",
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||
apiName: OPT_TRANS_GOOGLE, // 接口自定义名称
|
||||
isDisabled: false, // 是否禁用
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT, // 超时时间
|
||||
},
|
||||
[OPT_TRANS_GOOGLE_2]: {
|
||||
url: URL_GOOGLE_TRAN2,
|
||||
key: DEFAULT_GOOGLE_API_KEY,
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_GOOGLE_2,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_MICROSOFT]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_MICROSOFT,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_BAIDU]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_BAIDU,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_TENCENT]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_TENCENT,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_VOLCENGINE]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_VOLCENGINE,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_DEEPL]: {
|
||||
url: "https://api-free.deepl.com/v2/translate",
|
||||
key: "",
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_DEEPL,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_DEEPLFREE]: {
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_DEEPLFREE,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_DEEPLX]: {
|
||||
url: "http://localhost:1188/translate",
|
||||
key: "",
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_DEEPLX,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_NIUTRANS]: {
|
||||
url: "https://api.niutrans.com/NiuTransServer/translation",
|
||||
@@ -506,29 +664,67 @@ export const DEFAULT_TRANS_APIS = {
|
||||
memoryNo: "",
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_NIUTRANS,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
key: "",
|
||||
model: "gpt-4",
|
||||
prompt: `You will be provided with a sentence in ${INPUT_PLACE_FROM}, and your task is to translate it into ${INPUT_PLACE_TO}.`,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: defaultOpenaiApi,
|
||||
[OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
|
||||
[OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
|
||||
[OPT_TRANS_GEMINI]: {
|
||||
url: "https://generativelanguage.googleapis.com/v1/models",
|
||||
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
||||
key: "",
|
||||
model: "gemini-pro",
|
||||
prompt: `Translate the following text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}:\n\n${INPUT_PLACE_TEXT}`,
|
||||
model: "gemini-2.5-flash",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 2048,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_GEMINI,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_GEMINI_2]: {
|
||||
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
||||
key: "",
|
||||
model: "gemini-2.0-flash",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 2048,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_GEMINI_2,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_CLAUDE]: {
|
||||
url: "https://api.anthropic.com/v1/messages",
|
||||
key: "",
|
||||
model: "claude-3-haiku-20240307",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 1024,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_CLAUDE,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_CLOUDFLAREAI]: {
|
||||
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
|
||||
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
|
||||
key: "",
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_CLOUDFLAREAI,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_OLLAMA]: defaultOllamaApi,
|
||||
[OPT_TRANS_OLLAMA_2]: defaultOllamaApi,
|
||||
[OPT_TRANS_OLLAMA_3]: defaultOllamaApi,
|
||||
[OPT_TRANS_CUSTOMIZE]: defaultCustomApi,
|
||||
[OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi,
|
||||
[OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
|
||||
@@ -568,6 +764,7 @@ export const DEFAULT_SETTING = {
|
||||
minLength: TRANS_MIN_LENGTH,
|
||||
maxLength: TRANS_MAX_LENGTH,
|
||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
// injectWebfix: true, // 是否注入修复补丁(作废)
|
||||
@@ -589,6 +786,7 @@ export const DEFAULT_SETTING = {
|
||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
||||
transInterval: 500, // 翻译间隔时间
|
||||
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
@@ -26,10 +26,14 @@ export const DEFAULT_RULE = {
|
||||
transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译
|
||||
transTag: GLOBAL_KEY, // 译文元素标签
|
||||
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
|
||||
transSelected: GLOBAL_KEY, // 是否启用划词翻译
|
||||
detectRemote: GLOBAL_KEY, // 是否使用远程语言检测
|
||||
skipLangs: [], // 不翻译的语言
|
||||
fixerSelector: "", // 修复函数选择器
|
||||
fixerFunc: GLOBAL_KEY, // 修复函数
|
||||
transStartHook: "", // 钩子函数
|
||||
transEndHook: "", // 钩子函数
|
||||
transRemoveHook: "", // 钩子函数
|
||||
};
|
||||
|
||||
const DEFAULT_DIY_STYLE = `color: #666;
|
||||
|
||||
@@ -30,7 +30,14 @@ export function useTranslate(q, rule, setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deLang = await tryDetectLang(q, detectRemote === "true");
|
||||
let deLang = "";
|
||||
if (fromLang === "auto") {
|
||||
deLang = await tryDetectLang(
|
||||
q,
|
||||
detectRemote === "true",
|
||||
setting.langDetector
|
||||
);
|
||||
}
|
||||
if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
|
||||
setSamelang(true);
|
||||
} else {
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { isExt, isGm } from "./client";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { taskPool } from "./pool";
|
||||
import { getSettingWithDefault } from "./storage";
|
||||
|
||||
import {
|
||||
MSG_FETCH,
|
||||
MSG_GET_HTTPCACHE,
|
||||
CACHE_NAME,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
DEFAULT_HTTP_TIMEOUT,
|
||||
} from "../config";
|
||||
import { isBg } from "./browser";
|
||||
import { genTransReq } from "../apis/trans";
|
||||
import { kissLog } from "./log";
|
||||
import { blobToBase64 } from "./utils";
|
||||
|
||||
const TIMEOUT = 5000;
|
||||
|
||||
/**
|
||||
* 构造缓存 request
|
||||
* @param {*} input
|
||||
@@ -39,7 +40,10 @@ const newCacheReq = async (input, init) => {
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
export const fetchGM = async (
|
||||
input,
|
||||
{ method = "GET", headers, body, timeout } = {}
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
GM.xmlHttpRequest({
|
||||
method,
|
||||
@@ -47,7 +51,7 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
headers,
|
||||
data: body,
|
||||
// withCredentials: true,
|
||||
timeout: TIMEOUT,
|
||||
timeout,
|
||||
onload: ({ response, responseHeaders, status, statusText }) => {
|
||||
const headers = {};
|
||||
responseHeaders.split("\n").forEach((line) => {
|
||||
@@ -81,35 +85,60 @@ export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
|
||||
throw new Error("url is empty");
|
||||
}
|
||||
|
||||
if (isGm) {
|
||||
let info;
|
||||
if (window.KISS_GM) {
|
||||
info = await window.KISS_GM.getInfo();
|
||||
} else {
|
||||
info = GM.info;
|
||||
}
|
||||
|
||||
// Tampermonkey --> .connects
|
||||
// Violentmonkey --> .connect
|
||||
const connects = info?.script?.connects || info?.script?.connect || [];
|
||||
const url = new URL(input);
|
||||
const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||
|
||||
if (isSafe) {
|
||||
const { body, headers, status, statusText } = window.KISS_GM
|
||||
? await window.KISS_GM.fetch(input, init)
|
||||
: await fetchGM(input, init);
|
||||
|
||||
return new Response(body, {
|
||||
headers: new Headers(headers),
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
let timeout = apiSetting?.httpTimeout || DEFAULT_HTTP_TIMEOUT;
|
||||
if (!apiSetting) {
|
||||
try {
|
||||
timeout = (await getSettingWithDefault()).httpTimeout;
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
if (AbortSignal?.timeout) {
|
||||
Object.assign(init, { signal: AbortSignal.timeout(TIMEOUT) });
|
||||
if (isGm) {
|
||||
// let info;
|
||||
// if (window.KISS_GM) {
|
||||
// info = await window.KISS_GM.getInfo();
|
||||
// } else {
|
||||
// info = GM.info;
|
||||
// }
|
||||
|
||||
// Tampermonkey --> .connects
|
||||
// Violentmonkey --> .connect
|
||||
// const connects = info?.script?.connects || info?.script?.connect || [];
|
||||
// const url = new URL(input);
|
||||
// const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||
|
||||
// if (isSafe) {
|
||||
// // todo: 自定义接口 init 可能包含了 signal
|
||||
// Object.assign(init, { timeout });
|
||||
|
||||
// const { body, headers, status, statusText } = window.KISS_GM
|
||||
// ? await window.KISS_GM.fetch(input, init)
|
||||
// : await fetchGM(input, init);
|
||||
|
||||
// return new Response(body, {
|
||||
// headers: new Headers(headers),
|
||||
// status,
|
||||
// statusText,
|
||||
// });
|
||||
// }
|
||||
|
||||
// todo: 自定义接口 init 可能包含了 signal
|
||||
Object.assign(init, { timeout });
|
||||
|
||||
const { body, headers, status, statusText } = window.KISS_GM
|
||||
? await window.KISS_GM.fetch(input, init)
|
||||
: await fetchGM(input, init);
|
||||
|
||||
return new Response(body, {
|
||||
headers: new Headers(headers),
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
}
|
||||
|
||||
if (AbortSignal?.timeout && !init.signal) {
|
||||
Object.assign(init, { signal: AbortSignal.timeout(timeout) });
|
||||
}
|
||||
|
||||
return fetch(input, init);
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { CACHE_NAME } from "../config";
|
||||
import {
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
} from "../config";
|
||||
import { browser } from "./browser";
|
||||
import { apiBaiduLangdetect } from "../apis";
|
||||
import {
|
||||
apiGoogleLangdetect,
|
||||
apiMicrosoftLangdetect,
|
||||
apiBaiduLangdetect,
|
||||
apiTencentLangdetect,
|
||||
} from "../apis";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
const langdetectMap = {
|
||||
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
|
||||
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除缓存数据
|
||||
*/
|
||||
@@ -19,12 +37,16 @@ export const tryClearCaches = async () => {
|
||||
* @param {*} q
|
||||
* @returns
|
||||
*/
|
||||
export const tryDetectLang = async (q, useRemote = false) => {
|
||||
export const tryDetectLang = async (
|
||||
q,
|
||||
useRemote = false,
|
||||
langDetector = OPT_TRANS_MICROSOFT
|
||||
) => {
|
||||
let lang = "";
|
||||
|
||||
if (useRemote) {
|
||||
try {
|
||||
lang = await apiBaiduLangdetect(q);
|
||||
lang = await langdetectMap[langDetector](q);
|
||||
} catch (err) {
|
||||
kissLog(err, "detect lang remote");
|
||||
}
|
||||
|
||||
16
src/libs/interpreter.js
Normal file
16
src/libs/interpreter.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Sval from "sval";
|
||||
|
||||
const interpreter = new Sval({
|
||||
// ECMA Version of the code
|
||||
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
|
||||
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
|
||||
// or "latest"
|
||||
ecmaVer: "latest",
|
||||
// Code source type
|
||||
// "script" or "module"
|
||||
sourceType: "script",
|
||||
// Whether the code runs in a sandbox
|
||||
sandBox: true,
|
||||
});
|
||||
|
||||
export default interpreter;
|
||||
@@ -74,6 +74,9 @@ export const matchRule = async (
|
||||
"injectJs",
|
||||
"injectCss",
|
||||
"fixerSelector",
|
||||
"transStartHook",
|
||||
"transEndHook",
|
||||
"transRemoveHook",
|
||||
].forEach((key) => {
|
||||
if (!rule[key]?.trim()) {
|
||||
rule[key] = globalRule[key];
|
||||
@@ -89,6 +92,7 @@ export const matchRule = async (
|
||||
"transTiming",
|
||||
"transTag",
|
||||
"transTitle",
|
||||
"transSelected",
|
||||
"detectRemote",
|
||||
"fixerFunc",
|
||||
].forEach((key) => {
|
||||
@@ -158,10 +162,14 @@ export const checkRules = (rules) => {
|
||||
transTiming,
|
||||
transTag,
|
||||
transTitle,
|
||||
transSelected,
|
||||
detectRemote,
|
||||
skipLangs,
|
||||
fixerSelector,
|
||||
fixerFunc,
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
transRemoveHook,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
@@ -182,9 +190,14 @@ export const checkRules = (rules) => {
|
||||
transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
|
||||
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
|
||||
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
|
||||
transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected),
|
||||
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
|
||||
skipLangs: type(skipLangs) === "array" ? skipLangs : [],
|
||||
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
|
||||
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
|
||||
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
|
||||
transRemoveHook:
|
||||
type(transRemoveHook) === "string" ? transRemoveHook : "",
|
||||
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const loadingSvg = `
|
||||
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
|
||||
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%; max-width: 24; max-height: 24;">
|
||||
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
|
||||
@@ -18,13 +18,14 @@ import {
|
||||
} from "../config";
|
||||
import Content from "../views/Content";
|
||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||
import { debounce, genEventName } from "./utils";
|
||||
import { debounce, genEventName, getHtmlText } from "./utils";
|
||||
import { runFixer } from "./webfix";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { isExt } from "./client";
|
||||
import { injectInlineJs, injectInternalCss } from "./injector";
|
||||
import { kissLog } from "./log";
|
||||
import interpreter from "./interpreter";
|
||||
|
||||
/**
|
||||
* 翻译类
|
||||
@@ -52,7 +53,7 @@ export class Translator {
|
||||
];
|
||||
_eventName = genEventName();
|
||||
_mouseoverNode = null;
|
||||
_keepSelector = [null, null];
|
||||
_keepSelector = "";
|
||||
_terms = [];
|
||||
_docTitle = "";
|
||||
|
||||
@@ -124,9 +125,7 @@ export class Translator {
|
||||
this._setting = setting;
|
||||
this._rule = rule;
|
||||
|
||||
this._keepSelector = (rule.keepSelector || "")
|
||||
.split(SHADOW_KEY)
|
||||
.map((item) => item.trim());
|
||||
this._keepSelector = rule.keepSelector || "";
|
||||
this._terms = (rule.terms || "")
|
||||
.split(/\n|;/)
|
||||
.map((item) => item.split(",").map((item) => item.trim()))
|
||||
@@ -405,6 +404,7 @@ export class Translator {
|
||||
// 移除键盘监听
|
||||
window.removeEventListener("keydown", this._handleKeydown);
|
||||
|
||||
const { transRemoveHook } = this._rule;
|
||||
this._tranNodes.forEach((innerHTML, node) => {
|
||||
if (
|
||||
!this._rule.transTiming ||
|
||||
@@ -420,10 +420,17 @@ export class Translator {
|
||||
}
|
||||
|
||||
// 移除/恢复元素
|
||||
if (innerHTML && this._rule.transOnly === "true") {
|
||||
node.innerHTML = innerHTML;
|
||||
} else {
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
if (innerHTML) {
|
||||
if (this._rule.transOnly === "true") {
|
||||
node.innerHTML = innerHTML;
|
||||
} else {
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
}
|
||||
// 钩子函数
|
||||
if (transRemoveHook?.trim()) {
|
||||
interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`);
|
||||
interpreter.exports.transRemoveHook(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -468,45 +475,41 @@ export class Translator {
|
||||
return;
|
||||
}
|
||||
|
||||
const preText = this._tranNodes.get(el);
|
||||
const curText = el.innerText.trim();
|
||||
// const traText = traEl.innerText.trim();
|
||||
|
||||
// todo
|
||||
// 1. traText when loading
|
||||
// 2. replace startsWith
|
||||
if (curText.startsWith(preText)) {
|
||||
const preText = getHtmlText(this._tranNodes.get(el));
|
||||
const curText = getHtmlText(el.innerHTML, APP_LCNAME);
|
||||
if (preText === curText) {
|
||||
return;
|
||||
}
|
||||
|
||||
traEl.remove();
|
||||
}
|
||||
|
||||
// 缓存已翻译元素
|
||||
this._tranNodes.set(el, el.innerHTML);
|
||||
|
||||
let q = el.innerText.trim();
|
||||
if (this._rule.transOnly === "true") {
|
||||
this._tranNodes.set(el, el.innerHTML);
|
||||
} else {
|
||||
this._tranNodes.set(el, q);
|
||||
}
|
||||
const keeps = [];
|
||||
|
||||
// 翻译开始钩子函数
|
||||
const { transStartHook } = this._rule;
|
||||
if (transStartHook?.trim()) {
|
||||
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||
interpreter.exports.transStartHook(el, q);
|
||||
}
|
||||
|
||||
// 保留元素
|
||||
const [matchSelector, subSelector] = this._keepSelector;
|
||||
if (matchSelector || subSelector) {
|
||||
const keepSelector = this._keepSelector.trim();
|
||||
if (keepSelector) {
|
||||
let text = "";
|
||||
el.childNodes.forEach((child) => {
|
||||
if (
|
||||
child.nodeType === 1 &&
|
||||
((matchSelector && child.matches(matchSelector)) ||
|
||||
(subSelector && child.querySelector(subSelector)))
|
||||
) {
|
||||
if (child.nodeType === 1 && child.matches(keepSelector)) {
|
||||
if (child.nodeName === "IMG") {
|
||||
child.style.cssText += `width: ${child.width}px;`;
|
||||
child.style.cssText += `height: ${child.height}px;`;
|
||||
}
|
||||
text += `[${keeps.length}]`;
|
||||
keeps.push(child.outerHTML);
|
||||
} else {
|
||||
} else if (child.nodeType === 1 || child.nodeType === 3) {
|
||||
text += child.textContent;
|
||||
}
|
||||
});
|
||||
@@ -538,18 +541,22 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
traEl = document.createElement(APP_LCNAME);
|
||||
traEl.style.visibility = "visible";
|
||||
// if (this._rule.transOnly === "true") {
|
||||
// el.innerHTML = "";
|
||||
// }
|
||||
// 附加样式
|
||||
const { selectStyle, parentStyle } = this._rule;
|
||||
el.appendChild(traEl);
|
||||
el.style.cssText += selectStyle;
|
||||
if (el.parentElement) {
|
||||
el.parentElement.style.cssText += parentStyle;
|
||||
}
|
||||
|
||||
// 插入译文节点
|
||||
traEl = document.createElement(APP_LCNAME);
|
||||
traEl.style.visibility = "visible";
|
||||
// if (this._rule.transOnly === "true") {
|
||||
// el.innerHTML = "";
|
||||
// }
|
||||
el.appendChild(traEl);
|
||||
|
||||
// 渲染译文节点
|
||||
const root = createRoot(traEl);
|
||||
root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,16 @@ export const limitNumber = (num, min = 0, max = 100) => {
|
||||
return number;
|
||||
};
|
||||
|
||||
export const limitFloat = (num, min = 0, max = 100) => {
|
||||
const number = parseFloat(num);
|
||||
if (Number.isNaN(number) || number < min) {
|
||||
return min;
|
||||
} else if (number > max) {
|
||||
return max;
|
||||
}
|
||||
return number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 匹配是否为数组中的值
|
||||
* @param {*} arr
|
||||
@@ -240,3 +250,20 @@ export const blobToBase64 = (blob) => {
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取html内的文本
|
||||
* @param {*} htmlStr
|
||||
* @param {*} skipTag
|
||||
* @returns
|
||||
*/
|
||||
export const getHtmlText = (htmlStr, skipTag = "") => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlStr, "text/html");
|
||||
|
||||
if (skipTag) {
|
||||
doc.querySelectorAll(skipTag).forEach((el) => el.remove());
|
||||
}
|
||||
|
||||
return doc.body.innerText.trim();
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { useTranslate } from "../../hooks/Translate";
|
||||
import { styled, css } from "@mui/material/styles";
|
||||
import { APP_LCNAME } from "../../config";
|
||||
import interpreter from "../../libs/interpreter";
|
||||
|
||||
const LINE_STYLES = {
|
||||
[OPT_STYLE_LINE]: "solid",
|
||||
@@ -85,8 +86,15 @@ const StyledSpan = styled("span")`
|
||||
export default function Content({ q, keeps, translator, $el }) {
|
||||
const [rule, setRule] = useState(translator.rule);
|
||||
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
|
||||
const { transOpen, textStyle, bgColor, textDiyStyle, transOnly, transTag } =
|
||||
rule;
|
||||
const {
|
||||
transOpen,
|
||||
textStyle,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
transOnly,
|
||||
transTag,
|
||||
transEndHook,
|
||||
} = rule;
|
||||
|
||||
const { newlineLength } = translator.setting;
|
||||
|
||||
@@ -107,6 +115,14 @@ export default function Content({ q, keeps, translator, $el }) {
|
||||
};
|
||||
}, [translator.eventName]);
|
||||
|
||||
useEffect(() => {
|
||||
// 运行钩子函数
|
||||
if (text && transEndHook?.trim()) {
|
||||
interpreter.run(`exports.transEndHook = ${transEndHook}`);
|
||||
interpreter.exports.transEndHook($el, q, text, keeps);
|
||||
}
|
||||
}, [$el, q, text, keeps, transEndHook]);
|
||||
|
||||
const gap = useMemo(() => {
|
||||
if (transOnly === "true") {
|
||||
return "";
|
||||
|
||||
@@ -2,6 +2,9 @@ import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
@@ -10,15 +13,27 @@ import {
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
URL_KISS_PROXY,
|
||||
URL_NIUTRANS_REG,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_HTTP_TIMEOUT,
|
||||
} from "../../config";
|
||||
import { useState } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
@@ -33,7 +48,7 @@ import { useApi } from "../../hooks/Api";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { limitNumber, limitFloat } from "../../libs/utils";
|
||||
|
||||
function TestButton({ translator, api }) {
|
||||
const i18n = useI18n();
|
||||
@@ -51,7 +66,7 @@ function TestButton({ translator, api }) {
|
||||
useCache: false,
|
||||
});
|
||||
if (!text) {
|
||||
throw new Error("empty reault");
|
||||
throw new Error("empty result");
|
||||
}
|
||||
alert.success(i18n("test_success"));
|
||||
} catch (err) {
|
||||
@@ -102,19 +117,27 @@ function TestButton({ translator, api }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ApiFields({ translator }) {
|
||||
function ApiFields({ translator, api, updateApi, resetApi }) {
|
||||
const i18n = useI18n();
|
||||
const { api, updateApi, resetApi } = useApi(translator);
|
||||
const {
|
||||
url = "",
|
||||
key = "",
|
||||
model = "",
|
||||
prompt = "",
|
||||
systemPrompt = "",
|
||||
userPrompt = "",
|
||||
think = false,
|
||||
thinkIgnore = "",
|
||||
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||
dictNo = "",
|
||||
memoryNo = "",
|
||||
customOption = "",
|
||||
reqHook = "",
|
||||
resHook = "",
|
||||
temperature = 0,
|
||||
maxTokens = 256,
|
||||
apiName = "",
|
||||
isDisabled = false,
|
||||
} = api;
|
||||
|
||||
const handleChange = (e) => {
|
||||
@@ -126,6 +149,15 @@ function ApiFields({ translator }) {
|
||||
case "fetchInterval":
|
||||
value = limitNumber(value, 0, 5000);
|
||||
break;
|
||||
case "httpTimeout":
|
||||
value = limitNumber(value, 5000, 30000);
|
||||
break;
|
||||
case "temperature":
|
||||
value = limitFloat(value, 0, 2);
|
||||
break;
|
||||
case "maxTokens":
|
||||
value = limitNumber(value, 0, 2 ** 15);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateApi({
|
||||
@@ -133,19 +165,32 @@ function ApiFields({ translator }) {
|
||||
});
|
||||
};
|
||||
|
||||
const buildinTranslators = [
|
||||
const builtinTranslators = [
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
];
|
||||
|
||||
const mulkeysTranslators = [
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
];
|
||||
|
||||
const keyHelper =
|
||||
@@ -164,7 +209,15 @@ function ApiFields({ translator }) {
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
{!buildinTranslators.includes(translator) && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("api_name")}
|
||||
name="apiName"
|
||||
value={apiName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
{!builtinTranslators.includes(translator) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -191,7 +244,10 @@ function ApiFields({ translator }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && (
|
||||
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
||||
translator.startsWith(OPT_TRANS_OLLAMA) ||
|
||||
translator === OPT_TRANS_CLAUDE ||
|
||||
translator.startsWith(OPT_TRANS_GEMINI)) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -202,13 +258,69 @@ function ApiFields({ translator }) {
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"PROMPT"}
|
||||
name="prompt"
|
||||
value={prompt}
|
||||
label={"SYSTEM PROMPT"}
|
||||
name="systemPrompt"
|
||||
value={systemPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"USER PROMPT"}
|
||||
name="userPrompt"
|
||||
value={userPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{translator.startsWith(OPT_TRANS_OLLAMA) && (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="think"
|
||||
value={think}
|
||||
label={i18n("if_think")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("nothink")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("think")}</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("think_ignore")}
|
||||
name="thinkIgnore"
|
||||
value={thinkIgnore}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
||||
translator === OPT_TRANS_CLAUDE ||
|
||||
translator === OPT_TRANS_GEMINI ||
|
||||
translator === OPT_TRANS_GEMINI_2) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Temperature"}
|
||||
type="number"
|
||||
name="temperature"
|
||||
value={temperature}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Max Tokens"}
|
||||
type="number"
|
||||
name="maxTokens"
|
||||
value={maxTokens}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -232,15 +344,26 @@ function ApiFields({ translator }) {
|
||||
)}
|
||||
|
||||
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("custom_option")}
|
||||
name="customOption"
|
||||
value={customOption}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Request Hook"}
|
||||
name="reqHook"
|
||||
value={reqHook}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Response Hook"}
|
||||
name="resHook"
|
||||
value={resHook}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
@@ -261,6 +384,29 @@ function ApiFields({ translator }) {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("http_timeout")}
|
||||
type="number"
|
||||
name="httpTimeout"
|
||||
defaultValue={httpTimeout}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="isDisabled"
|
||||
checked={isDisabled}
|
||||
onChange={() => {
|
||||
updateApi({ isDisabled: !isDisabled });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("is_disabled")}
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TestButton translator={translator} api={api} />
|
||||
<Button
|
||||
@@ -283,6 +429,7 @@ function ApiFields({ translator }) {
|
||||
|
||||
function ApiAccordion({ translator }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { api, updateApi, resetApi } = useApi(translator);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
@@ -291,10 +438,19 @@ function ApiAccordion({ translator }) {
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{translator}</Typography>
|
||||
<Typography>
|
||||
{api.apiName ? `${translator} (${api.apiName})` : translator}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && <ApiFields translator={translator} />}
|
||||
{expanded && (
|
||||
<ApiFields
|
||||
translator={translator}
|
||||
api={api}
|
||||
updateApi={updateApi}
|
||||
resetApi={resetApi}
|
||||
/>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
@@ -305,11 +461,7 @@ export default function Apis() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">
|
||||
<Link href={URL_KISS_PROXY} target="_blank">
|
||||
{i18n("about_api_proxy")}
|
||||
</Link>
|
||||
</Alert>
|
||||
<Alert severity="info">{i18n("about_api")}</Alert>
|
||||
|
||||
<Box>
|
||||
{OPT_TRANS_ALL.map((translator) => (
|
||||
|
||||
@@ -96,9 +96,7 @@ export default function FavWords() {
|
||||
.join(" "),
|
||||
dictResult.content[0].mean
|
||||
.map(({ pre, cont }) => {
|
||||
return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join(
|
||||
"; "
|
||||
)}`;
|
||||
return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
|
||||
})
|
||||
.join("\n"),
|
||||
].join("\n\n")
|
||||
|
||||
@@ -27,6 +27,7 @@ import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
import { useRules } from "../../hooks/Rules";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Grid from "@mui/material/Grid";
|
||||
@@ -93,10 +94,14 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
transTiming = OPT_TIMING_PAGESCROLL,
|
||||
transTag = DEFAULT_TRANS_TAG,
|
||||
transTitle = "false",
|
||||
transSelected = "true",
|
||||
detectRemote = "false",
|
||||
skipLangs = [],
|
||||
fixerSelector = "",
|
||||
fixerFunc = "-",
|
||||
transStartHook = "",
|
||||
transEndHook = "",
|
||||
transRemoveHook = "",
|
||||
} = formValues;
|
||||
|
||||
const hasSamePattern = (str) => {
|
||||
@@ -177,6 +182,30 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const ShowMoreButton = showMore ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setShowMore(false);
|
||||
}}
|
||||
startIcon={<ExpandLessIcon />}
|
||||
>
|
||||
{i18n("less")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setShowMore(true);
|
||||
}}
|
||||
startIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
{i18n("more")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
@@ -339,95 +368,139 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOnly"
|
||||
value={transOnly}
|
||||
label={i18n("show_only_translations")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTiming"
|
||||
value={transTiming}
|
||||
label={i18n("trigger_mode")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_TIMING_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTag"
|
||||
value={transTag}
|
||||
label={i18n("translation_element_tag")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"span"}>{`<span>`}</MenuItem>
|
||||
<MenuItem value={"font"}>{`<font>`}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTitle"
|
||||
value={transTitle}
|
||||
label={i18n("translate_page_title")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transSelected"
|
||||
value={transSelected}
|
||||
label={i18n("translate_selected")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{showMore && (
|
||||
<>
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transOnly"
|
||||
value={transOnly}
|
||||
label={i18n("show_only_translations")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTiming"
|
||||
value={transTiming}
|
||||
label={i18n("trigger_mode")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{OPT_TIMING_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{i18n(item)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTag"
|
||||
value={transTag}
|
||||
label={i18n("translation_element_tag")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"span"}>{`<span>`}</MenuItem>
|
||||
<MenuItem value={"font"}>{`<font>`}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="transTitle"
|
||||
value={transTitle}
|
||||
label={i18n("translate_page_title")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3} lg={2}>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
fullWidth
|
||||
name="detectRemote"
|
||||
value={detectRemote}
|
||||
label={i18n("detect_lang_remote")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("fixer_selector")}
|
||||
name="fixerSelector"
|
||||
value={fixerSelector}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="fixerFunc"
|
||||
value={fixerFunc}
|
||||
label={i18n("fixer_function")}
|
||||
helperText={i18n("fixer_function_helper")}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{FIXER_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
@@ -458,34 +531,42 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("fixer_selector")}
|
||||
name="fixerSelector"
|
||||
value={fixerSelector}
|
||||
label={i18n("translate_start_hook")}
|
||||
helperText={i18n("translate_start_hook_helper")}
|
||||
name="transStartHook"
|
||||
value={transStartHook}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="fixerFunc"
|
||||
value={fixerFunc}
|
||||
label={i18n("fixer_function")}
|
||||
helperText={i18n("fixer_function_helper")}
|
||||
label={i18n("translate_end_hook")}
|
||||
helperText={i18n("translate_end_hook_helper")}
|
||||
name="transEndHook"
|
||||
value={transEndHook}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{GlobalItem}
|
||||
{FIXER_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("translate_remove_hook")}
|
||||
helperText={i18n("translate_remove_hook_helper")}
|
||||
name="transRemoveHook"
|
||||
value={transRemoveHook}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
@@ -564,18 +645,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
{i18n("delete")}
|
||||
</Button>
|
||||
)}
|
||||
{!showMore && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setShowMore(true);
|
||||
}}
|
||||
startIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
{i18n("more")}
|
||||
</Button>
|
||||
)}
|
||||
{ShowMoreButton}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -595,18 +665,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
{!showMore && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setShowMore(true);
|
||||
}}
|
||||
startIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
{i18n("more")}
|
||||
</Button>
|
||||
)}
|
||||
{ShowMoreButton}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -629,17 +688,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
||||
>
|
||||
{i18n("cancel")}
|
||||
</Button>
|
||||
{!showMore && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setShowMore(true);
|
||||
}}
|
||||
>
|
||||
{i18n("more")}
|
||||
</Button>
|
||||
)}
|
||||
{ShowMoreButton}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
UI_LANGS,
|
||||
TRANS_NEWLINE_LENGTH,
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_LANGDETECTOR_ALL,
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
@@ -25,12 +27,15 @@ import {
|
||||
DEFAULT_CSPLIST,
|
||||
MSG_CONTEXT_MENUS,
|
||||
MSG_UPDATE_CSP,
|
||||
DEFAULT_HTTP_TIMEOUT,
|
||||
} from "../../config";
|
||||
import { useShortcut } from "../../hooks/Shortcut";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
import { useFab } from "../../hooks/Fab";
|
||||
import { sendBgMsg } from "../../libs/msg";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import UploadButton from "./UploadButton";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
|
||||
function ShortcutItem({ action, label }) {
|
||||
const { shortcut, setShortcut } = useShortcut(action);
|
||||
@@ -67,6 +72,9 @@ export default function Settings() {
|
||||
case "newlineLength":
|
||||
value = limitNumber(value, 1, 1000);
|
||||
break;
|
||||
case "httpTimeout":
|
||||
value = limitNumber(value, 5000, 30000);
|
||||
break;
|
||||
case "touchTranslate":
|
||||
value = limitNumber(value, 0, 4);
|
||||
break;
|
||||
@@ -92,23 +100,48 @@ export default function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (data) => {
|
||||
try {
|
||||
await updateSetting(JSON.parse(data));
|
||||
} catch (err) {
|
||||
kissLog(err, "import setting");
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
uiLang,
|
||||
minLength,
|
||||
maxLength,
|
||||
clearCache,
|
||||
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||
contextMenuType = 1,
|
||||
touchTranslate = 2,
|
||||
blacklist = DEFAULT_BLACKLIST.join(",\n"),
|
||||
csplist = DEFAULT_CSPLIST.join(",\n"),
|
||||
transInterval = 500,
|
||||
langDetector = OPT_TRANS_MICROSOFT,
|
||||
} = setting;
|
||||
const { isHide = false } = fab || {};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
useFlexGap
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<UploadButton text={i18n("import")} handleImport={handleImport} />
|
||||
<DownloadButton
|
||||
handleData={() => JSON.stringify(setting, null, 2)}
|
||||
text={i18n("export")}
|
||||
fileName={`kiss-setting_${Date.now()}.json`}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
||||
<Select
|
||||
@@ -160,7 +193,14 @@ export default function Settings() {
|
||||
defaultValue={transInterval}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("http_timeout")}
|
||||
type="number"
|
||||
name="httpTimeout"
|
||||
defaultValue={httpTimeout}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
|
||||
<Select
|
||||
@@ -206,6 +246,22 @@ export default function Settings() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small">
|
||||
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
|
||||
<Select
|
||||
name="langDetector"
|
||||
value={langDetector}
|
||||
label={i18n("detect_lang_remote")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGDETECTOR_ALL.map((item) => (
|
||||
<MenuItem value={item} key={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{isExt ? (
|
||||
<>
|
||||
<FormControl size="small">
|
||||
|
||||
@@ -65,6 +65,7 @@ export default function SyncSetting() {
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="warning">{i18n("sync_warn")}</Alert>
|
||||
<Alert severity="warning">{i18n("sync_warn_2")}</Alert>
|
||||
|
||||
<TextField
|
||||
select
|
||||
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
OPT_LANGS_TO,
|
||||
OPT_TRANBOX_TRIGGER_CLICK,
|
||||
OPT_TRANBOX_TRIGGER_ALL,
|
||||
OPT_DICT_BAIDU,
|
||||
} from "../../config";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import { useCallback } from "react";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { useTranbox } from "../../hooks/Tranbox";
|
||||
@@ -47,7 +46,6 @@ export default function Tranbox() {
|
||||
);
|
||||
|
||||
const {
|
||||
transOpen,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
@@ -63,25 +61,12 @@ export default function Tranbox() {
|
||||
followSelection = false,
|
||||
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
|
||||
extStyles = "",
|
||||
enDict = OPT_DICT_BAIDU,
|
||||
} = tranboxSetting;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="transOpen"
|
||||
checked={transOpen}
|
||||
onChange={() => {
|
||||
updateTranbox({ transOpen: !transOpen });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("toggle_selection_translate")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
@@ -143,6 +128,18 @@ export default function Tranbox() {
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="enDict"
|
||||
value={enDict}
|
||||
label={i18n("english_dict")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
|
||||
<MenuItem value={OPT_DICT_BAIDU}>{OPT_DICT_BAIDU}</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("tranbtn_offset_x")}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_STYLE_ALL,
|
||||
DEFAULT_TRANS_APIS,
|
||||
} from "../../config";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
import { saveRule } from "../../libs/rules";
|
||||
@@ -32,6 +33,7 @@ import { kissLog } from "../../libs/log";
|
||||
export default function Popup({ setShowPopup, translator: tran }) {
|
||||
const i18n = useI18n();
|
||||
const [rule, setRule] = useState(tran?.rule);
|
||||
const [transApis, setTransApis] = useState(tran?.setting?.transApis || []);
|
||||
const [commands, setCommands] = useState({});
|
||||
|
||||
const handleOpenSetting = () => {
|
||||
@@ -106,7 +108,8 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
try {
|
||||
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
||||
if (!res.error) {
|
||||
setRule(res.data);
|
||||
setRule(res.rule);
|
||||
setTransApis(res.setting.transApis);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(err, "query rule");
|
||||
@@ -138,6 +141,20 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
})();
|
||||
}, [tran]);
|
||||
|
||||
const optApis = useMemo(
|
||||
() =>
|
||||
OPT_TRANS_ALL.map((key) => ({
|
||||
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
|
||||
apiKey: key,
|
||||
}))
|
||||
.filter((item) => !item.isDisabled)
|
||||
.map(({ apiKey, apiName }) => ({
|
||||
key: apiKey,
|
||||
name: apiName?.trim() || apiKey,
|
||||
})),
|
||||
[transApis]
|
||||
);
|
||||
|
||||
if (!rule) {
|
||||
return (
|
||||
<Box minWidth={300}>
|
||||
@@ -197,9 +214,9 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
{optApis.map(({ key, name }) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
@@ -93,9 +93,7 @@ export default function DictCont({ text }) {
|
||||
key={key}
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<Typography component="span">{`${
|
||||
PHONIC_MAP[key]?.[0] || key
|
||||
} ${val}`}</Typography>
|
||||
<Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
|
||||
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
|
||||
</Typography>
|
||||
))}
|
||||
|
||||
@@ -18,13 +18,19 @@ import LockIcon from "@mui/icons-material/Lock";
|
||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
|
||||
import { useState, useRef } from "react";
|
||||
import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
DEFAULT_TRANS_APIS,
|
||||
} from "../../config";
|
||||
import { useState, useRef, useMemo } from "react";
|
||||
import TranCont from "./TranCont";
|
||||
import DictCont from "./DictCont";
|
||||
import SugCont from "./SugCont";
|
||||
import CopyBtn from "./CopyBtn";
|
||||
import { isValidWord } from "../../libs/utils";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
|
||||
function Header({
|
||||
setShowPopup,
|
||||
@@ -34,7 +40,12 @@ function Header({
|
||||
setHideClickAway,
|
||||
followSelection,
|
||||
setFollowSelection,
|
||||
mouseHover,
|
||||
}) {
|
||||
if (!isMobile && simpleStyle && !mouseHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="KT-transbox-header"
|
||||
@@ -95,7 +106,15 @@ function Header({
|
||||
);
|
||||
}
|
||||
|
||||
function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
|
||||
function TranForm({
|
||||
text,
|
||||
setText,
|
||||
tranboxSetting,
|
||||
transApis,
|
||||
simpleStyle,
|
||||
langDetector,
|
||||
enDict,
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
@@ -105,6 +124,20 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
|
||||
const [toLang, setToLang] = useState(tranboxSetting.toLang);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const optApis = useMemo(
|
||||
() =>
|
||||
OPT_TRANS_ALL.map((key) => ({
|
||||
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
|
||||
apiKey: key,
|
||||
}))
|
||||
.filter((item) => !item.isDisabled)
|
||||
.map(({ apiKey, apiName }) => ({
|
||||
key: apiKey,
|
||||
name: apiName?.trim() || apiKey,
|
||||
})),
|
||||
[transApis]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="KT-transbox-container"
|
||||
@@ -168,9 +201,9 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
|
||||
setTranslator(e.target.value);
|
||||
}}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
{optApis.map(({ key, name }) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
@@ -227,7 +260,10 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{(!simpleStyle || !isValidWord(text) || !toLang.startsWith("zh")) && (
|
||||
{(!simpleStyle ||
|
||||
!isValidWord(text) ||
|
||||
!toLang.startsWith("zh") ||
|
||||
enDict === "-") && (
|
||||
<TranCont
|
||||
text={text}
|
||||
translator={translator}
|
||||
@@ -236,11 +272,16 @@ function TranForm({ text, setText, tranboxSetting, transApis, simpleStyle }) {
|
||||
toLang2={tranboxSetting.toLang2}
|
||||
transApis={transApis}
|
||||
simpleStyle={simpleStyle}
|
||||
langDetector={langDetector}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DictCont text={text} />
|
||||
<SugCont text={text} />
|
||||
{enDict !== "-" && (
|
||||
<>
|
||||
<DictCont text={text} />
|
||||
<SugCont text={text} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -262,7 +303,10 @@ export default function TranBox({
|
||||
followSelection,
|
||||
setFollowSelection,
|
||||
extStyles,
|
||||
langDetector,
|
||||
enDict,
|
||||
}) {
|
||||
const [mouseHover, setMouseHover] = useState(false);
|
||||
return (
|
||||
<SettingProvider>
|
||||
<ThemeProvider styles={extStyles}>
|
||||
@@ -280,9 +324,12 @@ export default function TranBox({
|
||||
setHideClickAway={setHideClickAway}
|
||||
followSelection={followSelection}
|
||||
setFollowSelection={setFollowSelection}
|
||||
mouseHover={mouseHover}
|
||||
/>
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseEnter={() => setMouseHover(true)}
|
||||
onMouseLeave={() => setMouseHover(false)}
|
||||
>
|
||||
<TranForm
|
||||
text={text}
|
||||
@@ -290,6 +337,8 @@ export default function TranBox({
|
||||
tranboxSetting={tranboxSetting}
|
||||
transApis={transApis}
|
||||
simpleStyle={simpleStyle}
|
||||
langDetector={langDetector}
|
||||
enDict={enDict}
|
||||
/>
|
||||
</DraggableResizable>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -5,10 +5,11 @@ import Stack from "@mui/material/Stack";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { DEFAULT_TRANS_APIS } from "../../config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiTranslate, apiBaiduLangdetect } from "../../apis";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import CopyBtn from "./CopyBtn";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { tryDetectLang } from "../../libs";
|
||||
|
||||
export default function TranCont({
|
||||
text,
|
||||
@@ -18,6 +19,7 @@ export default function TranCont({
|
||||
toLang2 = "en",
|
||||
transApis,
|
||||
simpleStyle,
|
||||
langDetector,
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [trText, setTrText] = useState("");
|
||||
@@ -32,8 +34,8 @@ export default function TranCont({
|
||||
setError("");
|
||||
|
||||
let to = toLang;
|
||||
if (toLang !== toLang2 && toLang2 !== "none") {
|
||||
const detectLang = await apiBaiduLangdetect(text);
|
||||
if (fromLang === "auto" && toLang !== toLang2 && toLang2 !== "none") {
|
||||
const detectLang = await tryDetectLang(text, true, langDetector);
|
||||
if (detectLang === toLang) {
|
||||
to = toLang2;
|
||||
}
|
||||
@@ -55,7 +57,7 @@ export default function TranCont({
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [text, translator, fromLang, toLang, toLang2, transApis]);
|
||||
}, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
|
||||
|
||||
if (simpleStyle) {
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
OPT_TRANBOX_TRIGGER_CLICK,
|
||||
OPT_TRANBOX_TRIGGER_HOVER,
|
||||
OPT_TRANBOX_TRIGGER_SELECT,
|
||||
OPT_DICT_BAIDU,
|
||||
} from "../../config";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
import { kissLog } from "../../libs/log";
|
||||
@@ -20,6 +21,7 @@ export default function Slection({
|
||||
tranboxSetting,
|
||||
transApis,
|
||||
uiLang,
|
||||
langDetector,
|
||||
}) {
|
||||
const {
|
||||
hideTranBtn = false,
|
||||
@@ -33,6 +35,7 @@ export default function Slection({
|
||||
btnOffsetY,
|
||||
boxOffsetX = 0,
|
||||
boxOffsetY = 10,
|
||||
enDict = OPT_DICT_BAIDU,
|
||||
} = tranboxSetting;
|
||||
|
||||
const boxWidth =
|
||||
@@ -234,6 +237,8 @@ export default function Slection({
|
||||
followSelection={followSelection}
|
||||
setFollowSelection={setFollowSelection}
|
||||
extStyles={extStyles}
|
||||
langDetector={langDetector}
|
||||
enDict={enDict}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user