From d5fc69e210998718a0d9613ca910196da344b09e Mon Sep 17 00:00:00 2001 From: Gabe Yuan Date: Fri, 19 Jan 2024 16:02:53 +0800 Subject: [PATCH] feat: support custom terms --- README.en.md | 1 + README.md | 1 + src/apis/index.js | 12 +++++--- src/config/i18n.js | 16 ++++++++--- src/config/index.js | 1 + src/config/rules.js | 4 ++- src/hooks/Translate.js | 6 ++++ src/libs/blacklist.js | 2 +- src/libs/req.js | 2 +- src/libs/rules.js | 3 ++ src/libs/translator.js | 56 ++++++++++++++++++++++++-------------- src/views/Content/index.js | 2 +- src/views/Options/Rules.js | 11 ++++++++ 13 files changed, 85 insertions(+), 32 deletions(-) diff --git a/README.en.md b/README.en.md index eb18b27..fc7d06e 100644 --- a/README.en.md +++ b/README.en.md @@ -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 diff --git a/README.md b/README.md index 39eff3e..af80d33 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ - [x] WebDAV - [x] 自定义翻译规则 - [x] 规则订阅/规则分享 + - [x] 自定义专业术语 - [x] 自定义译文样式 - [x] 自定义快捷键 - `Alt+Q` 开启翻译 diff --git a/src/apis/index.js b/src/apis/index.js index 2e18c74..ee11787 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -158,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: @@ -180,7 +182,7 @@ export const apiTranslate = async ({ 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[0].dst; + trText = res.data.map((item) => item.dst).join(" "); isSame = to === res.from; } break; @@ -189,11 +191,13 @@ export const apiTranslate = async ({ 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: diff --git a/src/config/i18n.js b/src/config/i18n.js index 28ef30b..00f5975 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -364,8 +364,8 @@ 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”选择器和内部选择器用“>>>”隔开。`, @@ -395,6 +395,14 @@ export const I18N = { 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: `1、多条术语用换行或分号“;”隔开。2、术语和译文用英文逗号“,”隔开。3、没有译文视为不翻译术语。4、留空表示采用全局设置。`, + en: `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: `根选择器`, en: `Root Selector`, @@ -716,7 +724,7 @@ export const I18N = { en: `Add 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 ",".`, }, }; diff --git a/src/config/index.js b/src/config/index.js index 96564be..dec4155 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -324,6 +324,7 @@ export const GLOBLA_RULE = { pattern: "*", selector: DEFAULT_SELECTOR, keepSelector: DEFAULT_KEEP_SELECTOR, + terms: "", translator: OPT_TRANS_MICROSOFT, fromLang: "auto", toLang: "zh-CN", diff --git a/src/config/rules.js b/src/config/rules.js index fc9fe82..31c1ba3 100644 --- a/src/config/rules.js +++ b/src/config/rules.js @@ -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, })); diff --git a/src/hooks/Translate.js b/src/hooks/Translate.js index 80ad96f..5ef8c9f 100644 --- a/src/hooks/Translate.js +++ b/src/hooks/Translate.js @@ -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 ( diff --git a/src/libs/blacklist.js b/src/libs/blacklist.js index 0ff4bfd..ae69da1 100644 --- a/src/libs/blacklist.js +++ b/src/libs/blacklist.js @@ -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())); diff --git a/src/libs/req.js b/src/libs/req.js index 607862a..a2e287c 100644 --- a/src/libs/req.js +++ b/src/libs/req.js @@ -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); diff --git a/src/libs/rules.js b/src/libs/rules.js index 9d8f28c..7fd82e8 100644 --- a/src/libs/rules.js +++ b/src/libs/rules.js @@ -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), diff --git a/src/libs/translator.js b/src/libs/translator.js index a723cc8..7270bde 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -42,6 +42,8 @@ export class Translator { ]; _eventName = genEventName(); _mouseoverNode = null; + _keepSelector = [null, null]; + _terms = new Map(); // 显示 _interseObserver = new IntersectionObserver( @@ -102,6 +104,17 @@ export class Translator { this._rule = rule; this._fixerSetting = fixerSetting; + this._keepSelector = (rule.keepSelector || "") + .split(SHADOW_KEY) + .map((item) => item.trim()); + const terms = (rule.terms || "") + .split(/\n|;/) + .map((item) => item.split(",").map((item) => item.trim())) + .filter(([term]) => Boolean(term)); + if (terms.length > 0) { + this._terms = new Map(terms); + } + if (rule.transOpen === "true") { this._register(); } @@ -386,47 +399,48 @@ 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 = []; - const [matchSelector, subSelector] = keepSelector.split(SHADOW_KEY); - if (matchSelector.trim() || subSelector?.trim()) { + + // 保留元素 + const [matchSelector, subSelector] = this._keepSelector; + if (matchSelector || subSelector) { let text = ""; el.childNodes.forEach((child) => { if ( child.nodeType === 1 && - ((matchSelector.trim() && child.matches(matchSelector)) || - (subSelector?.trim() && child.querySelector(subSelector))) + ((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.size > 0) { + const re = new RegExp([...this._terms.keys()].join("|"), "g"); + q = q.replace(re, (term) => { + const text = `[${keeps.length}]`; + keeps.push(this._terms.get(term) || term); + return text; + }); + } traEl = document.createElement(APP_LCNAME); traEl.style.visibility = "visible"; @@ -438,6 +452,8 @@ export class Translator { "-webkit-line-clamp: unset; max-height: none; height: auto;"; } + // console.log({ q, keeps }); + const root = createRoot(traEl); root.render(); }; diff --git a/src/views/Content/index.js b/src/views/Content/index.js index 2567fc0..e776876 100644 --- a/src/views/Content/index.js +++ b/src/views/Content/index.js @@ -160,7 +160,7 @@ export default function Content({ q, keeps, translator }) { {keeps.length > 0 ? ( keeps[parseInt(p)]), + __html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]), }} /> ) : ( diff --git a/src/views/Options/Rules.js b/src/views/Options/Rules.js index 0183d24..4354e24 100644 --- a/src/views/Options/Rules.js +++ b/src/views/Options/Rules.js @@ -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 /> +