Compare commits

..

13 Commits

Author SHA1 Message Date
Gabe Yuan
72ccfc8aec v1.8.11 2024-05-23 20:06:48 +08:00
Gabe Yuan
d117c5dc10 feat: baidu dict can be disabled 2024-05-23 00:08:10 +08:00
Gabe Yuan
9312783f44 feat: lang detector can be selected 2024-05-22 23:33:30 +08:00
Gabe Yuan
e5b16ebfd3 Merge remote-tracking branch 'origin/master' into dev 2024-05-22 10:19:04 +08:00
Gabe Yuan
5d1d65c2d3 feat: the temperature and maxTokens of the openai can be configured 2024-05-21 23:15:46 +08:00
Gabe
9ca1309cec Merge pull request #126 from kebyn/master
fix: option translation
2024-05-21 20:02:53 +08:00
kebyn
a03afc05f5 fix: option translation 2024-05-21 08:56:09 +00:00
Gabe Yuan
0198963584 fix: deeplx: replace auto to blank string 2024-05-21 11:55:17 +08:00
Gabe Yuan
58e745d967 v1.8.10 2024-05-17 10:38:11 +08:00
Gabe Yuan
377e347d68 feat: support translate hooks 2024-05-15 11:07:13 +08:00
Gabe Yuan
bac0704d3d feat: download and upload settings 2024-05-12 20:24:40 +08:00
Gabe Yuan
d2ff46edf6 fix: show full gemini url 2024-05-12 16:25:20 +08:00
Gabe Yuan
f908372b4e feat: support hook for custom api 2024-05-12 16:10:11 +08:00
25 changed files with 525 additions and 112 deletions

2
.env
View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.8.9",
"version": "1.8.11",
"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"
},
@@ -31,7 +32,7 @@
"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 .",
"zip": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},

11
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ dependencies:
react-scripts:
specifier: 5.0.1
version: 5.0.1(@babel/plugin-syntax-flow@7.24.1)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(react@18.2.0)(typescript@5.4.5)
sval:
specifier: ^0.5.2
version: 0.5.2
webdav:
specifier: ^5.3.0
version: 5.3.0
@@ -6504,7 +6507,7 @@ packages:
optional: true
dependencies:
abab: 2.0.6
acorn: 8.10.0
acorn: 8.11.3
acorn-globals: 6.0.0
cssom: 0.4.4
cssstyle: 2.3.0
@@ -9348,6 +9351,12 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
/sval@0.5.2:
resolution: {integrity: sha512-cMN4SWqQ8K2DypYVZ1DVsicvXsr4gQmAYR2faKwHttJFJAqjfc4+taG9esMIP0hMP5+4Caun99n6y+4T6nCPEA==}
dependencies:
acorn: 8.11.3
dev: false
/svg-parser@2.0.4:
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}

View File

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

View File

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

View File

@@ -24,6 +24,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,
@@ -31,8 +33,11 @@ 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";
/**
* 同步数据
@@ -58,6 +63,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
@@ -275,27 +326,14 @@ export const apiTranslate = async ({
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:
}

View File

@@ -28,10 +28,12 @@ import {
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();
@@ -183,7 +185,17 @@ const genTencent = ({ text, from, to }) => {
return [URL_TENCENT_TRANSMART, init];
};
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
const genOpenAI = ({
text,
from,
to,
url,
key,
prompt,
model,
temperature,
maxTokens,
}) => {
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to);
@@ -200,8 +212,8 @@ const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
content: text,
},
],
temperature: 0,
max_tokens: 256,
temperature,
max_tokens: maxTokens,
};
const init = {
@@ -218,6 +230,9 @@ const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
};
const genGemini = ({ text, from, to, url, key, prompt, model }) => {
url = url
.replaceAll(INPUT_PLACE_MODEL, model)
.replaceAll(INPUT_PLACE_KEY, key);
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
@@ -236,7 +251,6 @@ const genGemini = ({ text, from, to, url, key, prompt, model }) => {
],
};
const input = `${url}/${model}:generateContent?key=${key}`;
const init = {
headers: {
"Content-type": "application/json",
@@ -245,7 +259,7 @@ const genGemini = ({ text, from, to, url, key, prompt, model }) => {
body: JSON.stringify(data),
};
return [input, init];
return [url, init];
};
const genOllama = ({ text, from, to, url, key, prompt, model }) => {
@@ -293,20 +307,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",
},
@@ -316,23 +337,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];
};

View File

@@ -141,6 +141,7 @@ function showTransbox({
transApis,
darkMode,
uiLang,
langDetector,
}) {
if (!tranboxSetting?.transOpen) {
return;
@@ -172,6 +173,7 @@ function showTransbox({
tranboxSetting={tranboxSetting}
transApis={transApis}
uiLang={uiLang}
langDetector={langDetector}
/>
</CacheProvider>
</React.StrictMode>

View File

@@ -42,7 +42,23 @@ const customApiLangs = `["en", "English - English"],
["vi", "Vietnamese - Tiếng Việt"],
`;
const customDefaultOption = `{
const hookExample = `// URL
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
},
method: "GET",
body: null,
}]
// Response Hook
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`;
const customApiHelpZH = `// 请求数据默认格式
{
"url": "{{url}}",
"method": "POST",
"headers": {
@@ -50,18 +66,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 +80,43 @@ ${customDefaultOption}
to: "", // 目标语言(可选)
}
// Hook 范例
${hookExample}
// 支持的语言代码如下
${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
${hookExample}
// The supported language codes are as follows
${customApiLangs}
`;
@@ -623,7 +656,7 @@ export const I18N = {
},
use_simple_style: {
zh: `使用简洁界面`,
en: `Click outside to close the pop-up window`,
en: `Use a simple interface`,
},
show: {
zh: `显示`,
@@ -873,4 +906,32 @@ 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`,
},
};

View File

@@ -78,9 +78,16 @@ 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_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";
@@ -95,6 +102,8 @@ export const URL_NIUTRANS_REG =
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_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
@@ -140,6 +149,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 - 简体中文"],
@@ -202,7 +218,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"],
]),
@@ -318,6 +334,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,
@@ -384,6 +406,7 @@ 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"; // 默认高亮背景色/线条颜色
@@ -416,6 +439,9 @@ export const GLOBLA_RULE = {
skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器
fixerFunc: "-", // 修复函数
transStartHook: "", // 钩子函数
transEndHook: "", // 钩子函数
transRemoveHook: "", // 钩子函数
};
// 输入框翻译
@@ -463,6 +489,7 @@ export const DEFAULT_TRANBOX_SETTING = {
followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
};
// 订阅列表
@@ -485,7 +512,9 @@ export const DEFAULT_SUBRULES_LIST = [
const defaultCustomApi = {
url: "",
key: "",
customOption: "",
customOption: "", // (作废)
reqHook: "", // request 钩子函数
resHook: "", // response 钩子函数
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
};
@@ -494,6 +523,8 @@ const defaultOpenaiApi = {
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}.`,
temperature: 0,
maxTokens: 256,
fetchLimit: 1,
fetchInterval: 500,
};
@@ -507,7 +538,7 @@ const defaultOllamaApi = {
};
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, // 任务间隔时间
@@ -552,7 +583,7 @@ export const DEFAULT_TRANS_APIS = {
[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}`,
@@ -560,7 +591,7 @@ export const DEFAULT_TRANS_APIS = {
fetchInterval: 500,
},
[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,
@@ -628,6 +659,7 @@ export const DEFAULT_SETTING = {
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
// disableLangs: [], // 不翻译的语言(移至rule作废)
transInterval: 500, // 翻译间隔时间
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
};
export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -30,6 +30,9 @@ export const DEFAULT_RULE = {
skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器
fixerFunc: GLOBAL_KEY, // 修复函数
transStartHook: "", // 钩子函数
transEndHook: "", // 钩子函数
transRemoveHook: "", // 钩子函数
};
const DEFAULT_DIY_STYLE = `color: #666;

View File

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

View File

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

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

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

View File

@@ -74,6 +74,9 @@ export const matchRule = async (
"injectJs",
"injectCss",
"fixerSelector",
"transStartHook",
"transEndHook",
"transRemoveHook",
].forEach((key) => {
if (!rule[key]?.trim()) {
rule[key] = globalRule[key];
@@ -162,6 +165,9 @@ export const checkRules = (rules) => {
skipLangs,
fixerSelector,
fixerFunc,
transStartHook,
transEndHook,
transRemoveHook,
}) => ({
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
@@ -185,6 +191,10 @@ export const checkRules = (rules) => {
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
skipLangs: type(skipLangs) === "array" ? skipLangs : [],
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
transRemoveHook:
type(transRemoveHook) === "string" ? transRemoveHook : "",
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
})
);

View File

@@ -25,6 +25,7 @@ import { sendBgMsg } from "./msg";
import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log";
import interpreter from "./interpreter";
/**
* 翻译类
@@ -405,6 +406,7 @@ export class Translator {
// 移除键盘监听
window.removeEventListener("keydown", this._handleKeydown);
const { transRemoveHook } = this._rule;
this._tranNodes.forEach((innerHTML, node) => {
if (
!this._rule.transTiming ||
@@ -420,10 +422,17 @@ export class Translator {
}
// 移除/恢复元素
if (innerHTML && this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} else {
node.querySelector(APP_LCNAME)?.remove();
if (innerHTML) {
if (this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} else {
node.querySelector(APP_LCNAME)?.remove();
}
// 钩子函数
if (transRemoveHook?.trim()) {
interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`);
interpreter.exports.transRemoveHook(node);
}
}
});
@@ -490,6 +499,13 @@ export class Translator {
}
const keeps = [];
// 翻译开始钩子函数
const { transStartHook } = this._rule;
if (transStartHook?.trim()) {
interpreter.run(`exports.transStartHook = ${transStartHook}`);
interpreter.exports.transStartHook(el, q);
}
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
@@ -538,18 +554,22 @@ export class Translator {
}
}
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
// if (this._rule.transOnly === "true") {
// el.innerHTML = "";
// }
// 附加样式
const { selectStyle, parentStyle } = this._rule;
el.appendChild(traEl);
el.style.cssText += selectStyle;
if (el.parentElement) {
el.parentElement.style.cssText += parentStyle;
}
// 插入译文节点
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
// if (this._rule.transOnly === "true") {
// el.innerHTML = "";
// }
el.appendChild(traEl);
// 渲染译文节点
const root = createRoot(traEl);
root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
};

View File

@@ -15,6 +15,16 @@ export const limitNumber = (num, min = 0, max = 100) => {
return number;
};
export const limitFloat = (num, min = 0, max = 100) => {
const number = parseFloat(num);
if (Number.isNaN(number) || number < min) {
return min;
} else if (number > max) {
return max;
}
return number;
};
/**
* 匹配是否为数组中的值
* @param {*} arr

View File

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

View File

@@ -38,7 +38,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();
@@ -119,7 +119,10 @@ function ApiFields({ translator }) {
fetchInterval = DEFAULT_FETCH_INTERVAL,
dictNo = "",
memoryNo = "",
customOption = "",
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
} = api;
const handleChange = (e) => {
@@ -131,6 +134,12 @@ function ApiFields({ translator }) {
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "temperature":
value = limitFloat(value, 0, 2);
break;
case "maxTokens":
value = limitNumber(value, 0, 2 ** 15);
break;
default:
}
updateApi({
@@ -224,6 +233,27 @@ function ApiFields({ translator }) {
</>
)}
{translator.startsWith(OPT_TRANS_OPENAI) && (
<>
<TextField
size="small"
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
<TextField
size="small"
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</>
)}
{translator === OPT_TRANS_NIUTRANS && (
<>
<TextField
@@ -244,15 +274,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

View File

@@ -97,6 +97,9 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
skipLangs = [],
fixerSelector = "",
fixerFunc = "-",
transStartHook = "",
transEndHook = "",
transRemoveHook = "",
} = formValues;
const hasSamePattern = (str) => {
@@ -458,6 +461,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
@@ -468,6 +472,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
select
@@ -487,6 +492,40 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))}
</TextField>
<TextField
size="small"
label={i18n("translate_start_hook")}
helperText={i18n("translate_start_hook_helper")}
name="transStartHook"
value={transStartHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("translate_end_hook")}
helperText={i18n("translate_end_hook_helper")}
name="transEndHook"
value={transEndHook}
disabled={disabled}
onChange={handleChange}
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"
label={i18n("selector_style")}

View File

@@ -17,6 +17,8 @@ import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_LANGDETECTOR_ALL,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
@@ -31,6 +33,8 @@ 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);
@@ -92,6 +96,14 @@ export default function Settings() {
}
};
const handleImport = async (data) => {
try {
await updateSetting(JSON.parse(data));
} catch (err) {
kissLog(err, "import setting");
}
};
const {
uiLang,
minLength,
@@ -103,12 +115,28 @@ export default function Settings() {
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
@@ -206,6 +234,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">

View File

@@ -9,6 +9,7 @@ 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";
@@ -63,6 +64,7 @@ export default function Tranbox() {
followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles = "",
enDict = OPT_DICT_BAIDU,
} = tranboxSetting;
return (
@@ -143,6 +145,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")}

View File

@@ -101,7 +101,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);
@@ -233,7 +241,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}
@@ -242,11 +253,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>
);
}
@@ -268,6 +284,8 @@ export default function TranBox({
followSelection,
setFollowSelection,
extStyles,
langDetector,
enDict,
}) {
const [mouseHover, setMouseHover] = useState(false);
return (
@@ -300,6 +318,8 @@ export default function TranBox({
tranboxSetting={tranboxSetting}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
/>
</DraggableResizable>
</ThemeProvider>

View File

@@ -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("");
@@ -33,7 +35,7 @@ export default function TranCont({
let to = toLang;
if (toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await apiBaiduLangdetect(text);
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 (

View File

@@ -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}
/>
)}