feat: support custom terms
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- [x] WebDAV
|
||||
- [x] 自定义翻译规则
|
||||
- [x] 规则订阅/规则分享
|
||||
- [x] 自定义专业术语
|
||||
- [x] 自定义译文样式
|
||||
- [x] 自定义快捷键
|
||||
- `Alt+Q` 开启翻译
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ",".`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(<Content q={q} keeps={keeps} translator={this} />);
|
||||
};
|
||||
|
||||
@@ -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)]),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user