Compare commits

...

22 Commits

Author SHA1 Message Date
Gabe Yuan
71bbd2e54a v1.8.1 2024-02-06 10:09:33 +08:00
Gabe Yuan
3083d8e147 feat: context menu type 2024-02-05 11:28:34 +08:00
Gabe Yuan
e74883e9c2 fix: contextMenus: duplicate id err 2024-02-05 10:51:42 +08:00
Gabe Yuan
0816a9d167 fix: add BLOCKQUOTE to webfix 2024-02-05 10:02:30 +08:00
Gabe Yuan
4b3e91fa84 v1.8.1 2024-02-02 15:45:33 +08:00
Gabe Yuan
0973a0b60e fix: some js syntax 2024-02-02 15:44:44 +08:00
Gabe Yuan
de5f61126d fix: terms hepler text 2024-02-02 12:35:56 +08:00
Gabe Yuan
0c20ca761f fix: update toggle_translate CN text 2024-02-02 12:22:08 +08:00
Gabe Yuan
4bce56207e fix: optimize terms function 2024-02-02 12:10:27 +08:00
Gabe Yuan
dca54e0033 feat: setting: translate page title 2024-02-02 11:20:39 +08:00
Gabe Yuan
309646bf1d feat: setting: translate page title 2024-02-02 11:13:41 +08:00
Gabe Yuan
18b9961b39 fix: try add context menux on startup 2024-02-02 10:49:15 +08:00
Gabe Yuan
1e51ff17f2 v1.8.0 2024-01-22 13:19:37 +08:00
Gabe Yuan
63b5f707e2 fix: update ui when shortcut changed 2024-01-22 13:11:02 +08:00
Gabe Yuan
30efb6ee7a fix: title translate 2024-01-19 21:03:51 +08:00
Gabe Yuan
61b017618a feat: supported translation all when page opened 2024-01-19 17:55:18 +08:00
Gabe Yuan
1e0397adc9 feat: translate page title 2024-01-19 17:18:05 +08:00
Gabe Yuan
48b34bf95f fix: save new rule with hostname 2024-01-19 16:13:46 +08:00
Gabe Yuan
d5fc69e210 feat: support custom terms 2024-01-19 16:02:53 +08:00
Gabe Yuan
59f9dd697f fix: update baidu translate api 2024-01-18 15:26:37 +08:00
Gabe Yuan
c9d72323f1 keep selector support for sub-element 2024-01-12 16:04:34 +08:00
Gabe Yuan
e87f7f3abe fix: help text 2024-01-12 09:42:49 +08:00
32 changed files with 474 additions and 190 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.7.16
REACT_APP_VERSION=1.8.2
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -26,6 +26,7 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] WebDAV
- [x] Custom translation rules
- [x] Rule subscription/rule sharing
- [x] Customized terminology
- [x] Custom translation style
- [x] Custom shortcut keys
- `Alt+Q` Toggle Translation

View File

@@ -26,9 +26,10 @@
- [x] WebDAV
- [x] 自定义翻译规则
- [x] 规则订阅/规则分享
- [x] 自定义专业术语
- [x] 自定义译文样式
- [x] 自定义快捷键
- `Alt+Q` 启翻译
- `Alt+Q`翻译
- `Alt+C` 切换样式
- `Alt+K` 打开设置弹窗
- `Alt+S` 打开翻译弹窗/翻译选中文字

View File

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

View File

@@ -6,15 +6,15 @@
"message": "A simple bilingual translation extension & Greasemonkey script"
},
"toggle_translate": {
"message": "Toggle Translate (Alt+Q)"
"message": "Toggle Translate"
},
"toggle_style": {
"message": "Toggle Style (Alt+C)"
"message": "Toggle Style"
},
"open_options": {
"message": "Open Options (Alt+O)"
"message": "Open Options"
},
"open_tranbox": {
"message": "Translate Popup/Selected (Alt+S)"
"message": "Translate Popup/Selected"
}
}

View File

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

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.7.16",
"version": "1.8.2",
"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.7.16",
"version": "1.8.2",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import {
MSG_TRANS_TOGGLE_STYLE,
MSG_OPEN_TRANBOX,
MSG_CONTEXT_MENUS,
MSG_COMMAND_SHORTCUTS,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
@@ -27,39 +28,51 @@ globalThis.ContextType = "BACKGROUND";
/**
* 添加右键菜单
*/
function addContextMenus() {
browser.contextMenus.create({
id: CMD_TOGGLE_TRANSLATE,
title: browser.i18n.getMessage("toggle_translate"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_TOGGLE_STYLE,
title: browser.i18n.getMessage("toggle_style"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_TRANBOX,
title: browser.i18n.getMessage("open_tranbox"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: "options_separator",
type: "separator",
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_OPTIONS,
title: browser.i18n.getMessage("open_options"),
contexts: ["page", "selection"],
});
}
async function addContextMenus(contextMenuType = 1) {
// 添加前先删除,避免重复ID的错误
try {
await browser.contextMenus.removeAll();
} catch (err) {
//
}
/**
* 清除右键菜单
*/
function removeContextMenus() {
browser.contextMenus.removeAll();
switch (contextMenuType) {
case 1:
browser.contextMenus.create({
id: CMD_TOGGLE_TRANSLATE,
title: browser.i18n.getMessage("toggle_translate"),
contexts: ["page", "selection"],
});
break;
case 2:
browser.contextMenus.create({
id: CMD_TOGGLE_TRANSLATE,
title: browser.i18n.getMessage("toggle_translate"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_TOGGLE_STYLE,
title: browser.i18n.getMessage("toggle_style"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_TRANBOX,
title: browser.i18n.getMessage("open_tranbox"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: "options_separator",
type: "separator",
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_OPTIONS,
title: browser.i18n.getMessage("open_options"),
contexts: ["page", "selection"],
});
break;
default:
}
}
/**
@@ -79,11 +92,8 @@ browser.runtime.onStartup.addListener(async () => {
// 同步数据
await trySyncSettingAndRules();
const {
clearCache,
contextMenus = true,
subrulesList,
} = await getSettingWithDefault();
const { clearCache, contextMenuType, subrulesList } =
await getSettingWithDefault();
// 清除缓存
if (clearCache) {
@@ -91,9 +101,8 @@ browser.runtime.onStartup.addListener(async () => {
}
// 右键菜单
if (!contextMenus) {
removeContextMenus();
}
// firefox重启后菜单会消失,故重复添加
addContextMenus(contextMenuType);
// 同步订阅规则
trySyncAllSubRules({ subrulesList });
@@ -131,12 +140,18 @@ browser.runtime.onMessage.addListener(
saveRule(args);
break;
case MSG_CONTEXT_MENUS:
const { contextMenus } = args;
if (contextMenus) {
addContextMenus();
} else {
removeContextMenus();
}
const { contextMenuType } = args;
addContextMenus(contextMenuType);
break;
case MSG_COMMAND_SHORTCUTS:
browser.commands
.getAll()
.then((commands) => {
sendResponse({ data: commands });
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
default:
sendResponse({ error: `message action is unavailable: ${action}` });

View File

@@ -140,7 +140,7 @@ async function showFab(translator) {
* @returns
*/
function showTransbox({
contextMenus = true,
contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
}) {
@@ -165,7 +165,7 @@ function showTransbox({
<React.StrictMode>
<CacheProvider value={cache}>
<Slection
contextMenus={contextMenus}
contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting}
transApis={transApis}
/>

View File

@@ -112,8 +112,8 @@ export const I18N = {
en: customApiHelpEN,
},
translate_alt: {
zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`,
zh: `翻译`,
en: `Translate`,
},
basic_setting: {
zh: `基本设置`,
@@ -183,13 +183,17 @@ export const I18N = {
zh: `翻译服务`,
en: `Translate Service`,
},
mouseover_translation: {
zh: `鼠标悬停翻译`,
en: `Mouseover translation`,
translate_timing: {
zh: `翻译时机`,
en: `Translate Timing`,
},
mk_disable: {
zh: `禁用`,
en: `Disable`,
zh: `滚动加载(建议)`,
en: `Rolling Loading (Suggested)`,
},
mk_pageopen: {
zh: `页面打开`,
en: `Page Open`,
},
mk_mouseover: {
zh: `鼠标悬停`,
@@ -228,8 +232,8 @@ export const I18N = {
en: `Text Style`,
},
text_style_alt: {
zh: `文字样式 (Alt+C)`,
en: `Text Style (Alt+C)`,
zh: `文字样式`,
en: `Text Style`,
},
bg_color: {
zh: `样式颜色`,
@@ -364,15 +368,15 @@ export const I18N = {
en: `URL pattern`,
},
pattern_helper: {
zh: `1、支持星号(*)通配符。2、多个URL用英文逗号“,”分隔。`,
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs separated by English commas ",".`,
zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔。`,
en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
},
selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
},
translate_switch: {
zh: `启翻译`,
zh: `翻译`,
en: `Translate Switch`,
},
default_enabled: {
@@ -392,8 +396,16 @@ export const I18N = {
en: `Keep unchanged selector`,
},
keep_selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting.`,
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、子元素选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3.Sub-element selectors are separated by ">>>".`,
},
terms: {
zh: `专业术语`,
en: `Terms`,
},
terms_helper: {
zh: `0、支持正则表达式匹配。1、多条术语用换行或分号“;”隔开。2、术语和译文用英文逗号“,”隔开。3、没有译文视为不翻译术语。4、留空表示采用全局设置。`,
en: `0. Supports regular expression matching. 1. Separate multiple terms with newlines or semicolons ";". 2. Terms and translations are separated by English commas ",". 3. If there is no translation, the term will be deemed not to be translated. 4. Leave blank to adopt the global setting.`,
},
root_selector: {
zh: `根选择器`,
@@ -548,7 +560,7 @@ export const I18N = {
en: `Shortcuts Setting`,
},
toggle_translate_shortcut: {
zh: `"启翻译"快捷键`,
zh: `"启翻译"快捷键`,
en: `"Toggle Translate" Shortcut`,
},
toggle_style_shortcut: {
@@ -711,12 +723,28 @@ export const I18N = {
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
},
add_context_menus: {
zh: `添加右键菜单`,
en: `Add Context Menus`,
context_menus: {
zh: `右键菜单`,
en: `Context Menus`,
},
hide_context_menus: {
zh: `隐藏右键菜单`,
en: `Hide Context Menus`,
},
simple_context_menus: {
zh: `简单右键菜单`,
en: `Simple_context_menus Context Menus`,
},
secondary_context_menus: {
zh: `二级右键菜单`,
en: `Secondary Context Menus`,
},
mulkeys_help: {
zh: `支持英文逗号隔开多个KEY轮调用。`,
en: `Supports multiple KEY round calling calls separated by English commas.`,
zh: `支持用换行或英文逗号“,”分隔多个KEY轮调用。`,
en: `Supports multiple KEY polling calls separated by newlines or English commas ",".`,
},
translate_page_title: {
zh: `是否同时翻译页面标题`,
en: `Translate Page Title`,
},
};

View File

@@ -65,6 +65,7 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_CONTEXT_MENUS = "context_menus";
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const THEME_LIGHT = "light";
export const THEME_DARK = "dark";
@@ -83,7 +84,8 @@ export const URL_MICROSOFT_TRAN =
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
export const URL_BAIDU_TRAN = "https://fanyi.baidu.com/v2transapi";
export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
@@ -101,11 +103,11 @@ export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
@@ -296,13 +298,15 @@ export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_BLOCKQUOTE,
];
export const OPT_MOUSEKEY_DISABLE = "mk_disable";
export const OPT_MOUSEKEY_DISABLE = "mk_disable"; // 滚动加载翻译
export const OPT_MOUSEKEY_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
export const OPT_MOUSEKEY_ALT = "mk_altKey";
export const OPT_MOUSEKEY_ALL = [
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_PAGEOPEN,
OPT_MOUSEKEY_MOUSEOVER,
OPT_MOUSEKEY_CONTROL,
OPT_MOUSEKEY_SHIFT,
@@ -323,6 +327,7 @@ export const GLOBLA_RULE = {
pattern: "*",
selector: DEFAULT_SELECTOR,
keepSelector: DEFAULT_KEEP_SELECTOR,
terms: "",
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",
@@ -447,11 +452,13 @@ export const DEFAULT_SETTING = {
injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
detectRemote: false, // 是否使用远程语言检测
contextMenus: true, // 是否添加右键菜单
contextMenus: true, // 是否添加右键菜单(作废)
contextMenuType: 1, // 右键菜单类型(0不显示1简单菜单2多级菜单)
transTitle: false, // 是否同时翻译页面标题
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
transApis: DEFAULT_TRANS_APIS, // 翻译接口
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
mouseKey: OPT_MOUSEKEY_DISABLE, // 翻译时机/鼠标悬停翻译
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置

View File

@@ -10,6 +10,7 @@ export const DEFAULT_RULE = {
pattern: "",
selector: "",
keepSelector: "",
terms: "",
translator: GLOBAL_KEY,
fromLang: GLOBAL_KEY,
toLang: GLOBAL_KEY,
@@ -188,9 +189,10 @@ const RULES_MAP = {
export const BUILTIN_RULES = Object.entries(RULES_MAP)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([pattern, [selector, keepSelector = ""]]) => ({
.map(([pattern, [selector, keepSelector = "", terms = ""]]) => ({
...DEFAULT_RULE,
pattern,
selector,
keepSelector,
terms,
}));

View File

@@ -23,6 +23,12 @@ export function useTranslate(q, rule, setting) {
try {
setLoading(true);
if (!q.replace(/\[(\d+)\]/g, "").trim()) {
setText(q);
setSamelang(false);
return;
}
const deLang = await tryDetectLang(q, setting.detectRemote);
const disableLangs = setting.disableLangs || [];
if (

View File

@@ -10,4 +10,4 @@ import { DEFAULT_BLACKLIST } from "../config";
export const isInBlacklist = (
href,
{ blacklist = DEFAULT_BLACKLIST.join(",\n") }
) => blacklist.split(",").some((url) => isMatch(href, url.trim()));
) => blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));

View File

@@ -26,7 +26,7 @@ const keyMap = new Map();
// 轮询key
const keyPick = (translator, key = "") => {
const keys = key
.split(",")
.split(/\n|,/)
.map((item) => item.trim())
.filter(Boolean);

View File

@@ -67,6 +67,7 @@ export const matchRule = async (
rule.selector = rule.selector?.trim() || globalRule.selector;
rule.keepSelector = rule.keepSelector?.trim() || globalRule.keepSelector;
rule.terms = rule.terms?.trim() || globalRule.terms;
if (rule.textStyle === GLOBAL_KEY) {
rule.textStyle = globalRule.textStyle;
rule.bgColor = globalRule.bgColor;
@@ -114,6 +115,7 @@ export const checkRules = (rules) => {
pattern,
selector,
keepSelector,
terms,
translator,
fromLang,
toLang,
@@ -125,6 +127,7 @@ export const checkRules = (rules) => {
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
keepSelector: type(keepSelector) === "string" ? keepSelector : "",
terms: type(terms) === "string" ? terms : "",
bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),

View File

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

View File

@@ -8,12 +8,15 @@ import {
OPT_STYLE_FUZZY,
SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_PAGEOPEN,
OPT_MOUSEKEY_MOUSEOVER,
DEFAULT_TRANS_APIS,
} from "../config";
import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils";
import { runFixer } from "./webfix";
import { apiTranslate } from "../apis";
/**
* 翻译类
@@ -42,6 +45,9 @@ export class Translator {
];
_eventName = genEventName();
_mouseoverNode = null;
_keepSelector = [null, null];
_terms = [];
_docTitle = "";
// 显示
_interseObserver = new IntersectionObserver(
@@ -102,6 +108,14 @@ export class Translator {
this._rule = rule;
this._fixerSetting = fixerSetting;
this._keepSelector = (rule.keepSelector || "")
.split(SHADOW_KEY)
.map((item) => item.trim());
this._terms = (rule.terms || "")
.split(/\n|;/)
.map((item) => item.split(",").map((item) => item.trim()))
.filter(([term]) => Boolean(term));
if (rule.transOpen === "true") {
this._register();
}
@@ -158,6 +172,20 @@ export class Translator {
this.rule = { ...this.rule, textStyle };
};
translateText = async (text) => {
const { translator, fromLang, toLang } = this._rule;
const apiSetting =
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
const [trText] = await apiTranslate({
text,
translator,
fromLang,
toLang,
apiSetting,
});
return trText;
};
_querySelectorAll = (selector, node) => {
try {
return Array.from(node.querySelectorAll(selector));
@@ -263,6 +291,11 @@ export class Translator {
this._tranNodes.forEach((_, node) => {
this._interseObserver.observe(node);
});
} else if (this._setting.mouseKey === OPT_MOUSEKEY_PAGEOPEN) {
// 全文直接翻译
this._tranNodes.forEach((_, node) => {
this._render(node);
});
} else {
// 监听鼠标悬停
window.addEventListener("keydown", this._handleKeydown);
@@ -271,6 +304,15 @@ export class Translator {
node.addEventListener("mouseleave", this._handleMouseout);
});
}
// 翻译页面标题
if (this._setting.transTitle && !this._docTitle) {
const title = document.title;
this._docTitle = title;
this.translateText(title).then((trText) => {
document.title = `${trText} | ${title}`;
});
}
};
_handleMouseover = (e) => {
@@ -318,6 +360,12 @@ export class Translator {
};
_unRegister = () => {
// 恢复页面标题
if (this._docTitle) {
document.title = this._docTitle;
this._docTitle = "";
}
// 解除节点变化监听
this._mutaObserver.disconnect();
@@ -334,6 +382,10 @@ export class Translator {
// 移除已插入元素
node.querySelector(APP_LCNAME)?.remove();
});
} else if (this._setting.mouseKey === OPT_MOUSEKEY_PAGEOPEN) {
this._tranNodes.forEach((_, node) => {
node.querySelector(APP_LCNAME)?.remove();
});
} else {
// 移除鼠标悬停监听
window.removeEventListener("keydown", this._handleKeydown);
@@ -386,42 +438,50 @@ export class Translator {
let q = el.innerText.trim();
this._tranNodes.set(el, q);
// 太长或太短
if (this._invalidLength(q)) {
return;
}
// console.log("---> ", q);
const keepSelector = this._rule.keepSelector || "";
const keeps = [];
if (keepSelector.trim()) {
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
let text = "";
el.childNodes.forEach((child) => {
if (child.nodeType === 1 && child.matches(keepSelector)) {
if (
child.nodeType === 1 &&
((matchSelector && child.matches(matchSelector)) ||
(subSelector && child.querySelector(subSelector)))
) {
if (child.nodeName === "IMG") {
child.style.cssText += `width: ${child.width}px;`;
child.style.cssText += `height: ${child.height}px;`;
}
text += `#${keeps.length}#`;
text += `[${keeps.length}]`;
keeps.push(child.outerHTML);
} else {
text += child.textContent;
}
});
// 太长或太短
if (this._invalidLength(text.replace(/#(\d+)#/g, "").trim())) {
return;
}
if (keeps.length > 0) {
q = text;
}
}
// console.log("---> ", q);
// 太长或太短
if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
return;
}
// 专业术语
if (this._terms.length > 0) {
for (const term of this._terms) {
const re = new RegExp(term[0], "g");
q = q.replace(re, (t) => {
const text = `[${keeps.length}]`;
keeps.push(term[1] || t);
return text;
});
}
}
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
@@ -433,6 +493,8 @@ export class Translator {
"-webkit-line-clamp: unset; max-height: none; height: auto;";
}
// console.log({ q, keeps });
const root = createRoot(traEl);
root.render(<Content q={q} keeps={keeps} translator={this} />);
};

View File

@@ -69,8 +69,8 @@ function brFixer(node, tag = "p") {
}
node.setAttribute(fixedSign, "true");
var gapTags = ["BR", "WBR"];
var newlineTags = [
const gapTags = ["BR", "WBR"];
const newlineTags = [
"DIV",
"UL",
"OL",
@@ -85,9 +85,10 @@ function brFixer(node, tag = "p") {
"HR",
"PRE",
"TABLE",
"BLOCKQUOTE",
];
var html = "";
let html = "";
node.childNodes.forEach(function (child, index) {
if (index === 0) {
html += `<${tag} class="kiss-p">`;
@@ -99,8 +100,8 @@ function brFixer(node, tag = "p") {
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
} else if (child.outerHTML) {
html += child.outerHTML;
} else if (child.nodeValue) {
html += child.nodeValue;
} else if (child.textContent) {
html += child.textContent;
}
if (index === node.childNodes.length - 1) {
@@ -160,7 +161,7 @@ const fixerMap = {
* @param {*} rootSelector
*/
function run(selector, fixer, rootSelector) {
var mutaObserver = new MutationObserver(function (mutations) {
const mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
if (addNode && addNode.querySelectorAll) {
@@ -172,7 +173,7 @@ function run(selector, fixer, rootSelector) {
});
});
var rootNodes = [document];
let rootNodes = [document];
if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector);
}
@@ -241,8 +242,8 @@ export async function matchFixer(href, { injectWebfix }) {
const userSites = await getWebfixRulesWithDefault();
const subSites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
const sites = [...userSites, ...subSites];
for (var i = 0; i < sites.length; i++) {
var site = sites[i];
for (let i = 0; i < sites.length; i++) {
const site = sites[i];
if (isMatch(href, site.pattern) && fixerMap[site.fixer]) {
return site;
}

View File

@@ -95,11 +95,11 @@ export default function Action({ translator, fab }) {
// 注册菜单
try {
const menuCommandIds = [];
const { contextMenus = true } = translator.setting;
contextMenus &&
const { contextMenuType } = translator.setting;
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+Q)",
"Toggle Translate",
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
@@ -108,7 +108,7 @@ export default function Action({ translator, fab }) {
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+C)",
"Toggle Style",
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
@@ -117,7 +117,7 @@ export default function Action({ translator, fab }) {
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+K)",
"Open Menu",
(event) => {
setShowPopup((pre) => !pre);
},

View File

@@ -160,7 +160,7 @@ export default function Content({ q, keeps, translator }) {
{keeps.length > 0 ? (
<span
dangerouslySetInnerHTML={{
__html: text.replace(/#(\d+)#/g, (_, p) => keeps[parseInt(p)]),
__html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
}}
/>
) : (

View File

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

View File

@@ -66,6 +66,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
pattern,
selector,
keepSelector = "",
terms = "",
translator,
fromLang,
toLang,
@@ -190,6 +191,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
onChange={handleChange}
multiline
/>
<TextField
size="small"
label={i18n("terms")}
helperText={i18n("terms_helper")}
name="terms"
value={terms}
disabled={disabled}
onChange={handleChange}
multiline
/>
<Box>
<Grid container spacing={2} columns={12}>

View File

@@ -67,8 +67,8 @@ export default function Settings() {
case "touchTranslate":
value = limitNumber(value, 0, 4);
break;
case "contextMenus":
isExt && sendBgMsg(MSG_CONTEXT_MENUS, { contextMenus: value });
case "contextMenuType":
isExt && sendBgMsg(MSG_CONTEXT_MENUS, { contextMenuType: value });
break;
default:
}
@@ -96,7 +96,8 @@ export default function Settings() {
newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
detectRemote = false,
contextMenus = true,
contextMenuType = 1,
transTitle = false,
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
disableLangs = [],
@@ -168,11 +169,11 @@ export default function Settings() {
/>
<FormControl size="small">
<InputLabel>{i18n("mouseover_translation")}</InputLabel>
<InputLabel>{i18n("translate_timing")}</InputLabel>
<Select
name="mouseKey"
value={mouseKey}
label={i18n("mouseover_translation")}
label={i18n("translate_timing")}
onChange={handleChange}
>
{OPT_MOUSEKEY_ALL.map((item) => (
@@ -183,6 +184,19 @@ export default function Settings() {
</Select>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("translate_page_title")}</InputLabel>
<Select
name="transTitle"
value={transTitle}
label={i18n("translate_page_title")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</Select>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
<Select
@@ -215,15 +229,16 @@ export default function Settings() {
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("add_context_menus")}</InputLabel>
<InputLabel>{i18n("context_menus")}</InputLabel>
<Select
name="contextMenus"
value={contextMenus}
label={i18n("add_context_menus")}
name="contextMenuType"
value={contextMenuType}
label={i18n("context_menus")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
<MenuItem value={0}>{i18n("hide_context_menus")}</MenuItem>
<MenuItem value={1}>{i18n("simple_context_menus")}</MenuItem>
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</Select>
</FormControl>

View File

@@ -18,6 +18,7 @@ import {
MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
@@ -30,6 +31,7 @@ import { tryClearCaches } from "../../libs";
export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => {
if (!tran) {
@@ -84,7 +86,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
const tab = await getTabInfo();
href = tab.url;
}
const newRule = { ...rule, pattern: href };
const newRule = { ...rule, pattern: href.split("/")[2] };
if (isExt && tran) {
sendBgMsg(MSG_SAVE_RULE, newRule);
} else {
@@ -111,6 +113,32 @@ export default function Popup({ setShowPopup, translator: tran }) {
})();
}, [tran]);
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
if (!res.error) {
res.data.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
}
} else {
const shortcuts = tran.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
console.log("[query cmds]", err);
}
})();
}, [tran]);
if (!rule) {
return (
<Box minWidth={300}>
@@ -153,7 +181,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/>
</Stack>
@@ -211,7 +243,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small"
value={textStyle}
name="textStyle"
label={i18n("text_style_alt")}
label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange}
>
{OPT_STYLE_ALL.map((item) => (

View File

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

View File

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

View File

@@ -59,7 +59,8 @@ export default function TranCont({
// 词典
if (isValidWord(text) && toLang.startsWith("zh")) {
if (fromLang === "en" && translator === OPT_TRANS_BAIDU) {
setDictResult(tranRes[2].dict_result);
tranRes[2].type === 1 &&
setDictResult(JSON.parse(tranRes[2].result));
} else {
const dictRes = await apiTranslate({
text,
@@ -67,7 +68,8 @@ export default function TranCont({
fromLang: "en",
toLang: "zh-CN",
});
setDictResult(dictRes[2].dict_result);
dictRes[2].type === 1 &&
setDictResult(JSON.parse(dictRes[2].result));
}
}
} catch (err) {

View File

@@ -7,7 +7,11 @@ import { isGm, isExt } from "../../libs/client";
import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config";
import { isMobile } from "../../libs/mobile";
export default function Slection({ contextMenus, tranboxSetting, transApis }) {
export default function Slection({
contextMenuType,
tranboxSetting,
transApis,
}) {
const boxWidth = limitNumber(window.innerWidth, 300, 600);
const boxHeight = limitNumber(window.innerHeight, 200, 400);
@@ -106,10 +110,10 @@ export default function Slection({ contextMenus, tranboxSetting, transApis }) {
// 注册菜单
try {
const menuCommandIds = [];
contextMenus &&
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
"Translate Selected Text (Alt+S)",
"Translate Selected Text",
(event) => {
handleTranbox();
},
@@ -125,7 +129,7 @@ export default function Slection({ contextMenus, tranboxSetting, transApis }) {
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}, [handleTranbox, contextMenus]);
}, [handleTranbox, contextMenuType]);
return (
<>