import { createRoot } from "react-dom/client";
import {
APP_LCNAME,
TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH,
MSG_TRANS_CURRULE,
MSG_INJECT_JS,
MSG_INJECT_CSS,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
OPT_TIMING_PAGESCROLL,
OPT_TIMING_PAGEOPEN,
OPT_TIMING_MOUSEOVER,
DEFAULT_TRANS_APIS,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
} 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";
import { sendBgMsg } from "./msg";
import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log";
/**
* 翻译类
*/
export class Translator {
_rule = {};
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
_skipNodeNames = [
APP_LCNAME,
"style",
"svg",
"img",
"audio",
"video",
"textarea",
"input",
"button",
"select",
"option",
"head",
"script",
"iframe",
];
_eventName = genEventName();
_mouseoverNode = null;
_keepSelector = [null, null];
_terms = [];
_docTitle = "";
// 显示
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
if (intersection.isIntersecting) {
this._render(intersection.target);
this._interseObserver.unobserve(intersection.target);
}
});
},
{
threshold: 0.1,
}
);
// 变化
_mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
!this._skipNodeNames.includes(mutation.target.localName) &&
mutation.addedNodes.length > 0
) {
const nodes = Array.from(mutation.addedNodes).filter((node) => {
if (
this._skipNodeNames.includes(node.localName) ||
node.id === APP_LCNAME
) {
return false;
}
return true;
});
if (nodes.length > 0) {
// const rootNode = mutation.target.getRootNode();
// todo
this._reTranslate();
}
}
});
});
// 插入 shadowroot
_overrideAttachShadow = () => {
const _this = this;
const _attachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function () {
_this._reTranslate();
return _attachShadow.apply(this, arguments);
};
};
_updatePool(translator) {
if (!translator) {
return;
}
const {
fetchInterval = DEFAULT_FETCH_INTERVAL,
fetchLimit = DEFAULT_FETCH_LIMIT,
} = this._setting.transApis[translator] || {};
updateFetchPool(fetchInterval, fetchLimit);
}
constructor(rule, setting) {
this._overrideAttachShadow();
this._setting = setting;
this._rule = rule;
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));
this._updatePool(rule.translator);
if (rule.transOpen === "true") {
this._register();
}
}
get setting() {
return this._setting;
}
get eventName() {
return this._eventName;
}
get rule() {
// console.log("get rule", this._rule);
return this._rule;
}
set rule(rule) {
// console.log("set rule", rule);
this._rule = rule;
// 广播消息
const eventName = this._eventName;
window.dispatchEvent(
new CustomEvent(eventName, {
detail: {
action: MSG_TRANS_CURRULE,
args: rule,
},
})
);
}
updateRule = (obj) => {
this.rule = { ...this.rule, ...obj };
this._updatePool(obj.translator);
};
toggle = () => {
if (this.rule.transOpen === "true") {
this.rule = { ...this.rule, transOpen: "false" };
this._unRegister();
} else {
this.rule = { ...this.rule, transOpen: "true" };
this._register();
}
};
toggleStyle = () => {
const textStyle =
this.rule.textStyle === OPT_STYLE_FUZZY
? OPT_STYLE_DASHLINE
: OPT_STYLE_FUZZY;
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));
} catch (err) {
kissLog(selector, "querySelectorAll err");
}
return [];
};
_queryFilter = (selector, rootNode) => {
return this._querySelectorAll(selector, rootNode).filter(
(node) => this._queryFilter(selector, node).length === 0
);
};
_queryShadowNodes = (selector, rootNode) => {
this._rootNodes.add(rootNode);
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
Array.from(rootNode.querySelectorAll("*"))
.map((item) => item.shadowRoot)
.filter(Boolean)
.forEach((item) => {
this._queryShadowNodes(selector, item);
});
};
_queryNodes = (rootNode = document) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
// .filter(Boolean);
// const childNodes = childRoots.map((item) => this._queryNodes(item));
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
// return nodes.concat(childNodes).flat();
this._rootNodes.add(rootNode);
this._rule.selector
.split(";")
.map((item) => item.trim())
.filter(Boolean)
.forEach((selector) => {
if (selector.includes(SHADOW_KEY)) {
const [outSelector, inSelector] = selector
.split(SHADOW_KEY)
.map((item) => item.trim());
if (outSelector && inSelector) {
const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
// this._rootNodes.add(outNode.shadowRoot);
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
// (item) => {
// if (!this._tranNodes.has(item)) {
// this._tranNodes.set(item, "");
// }
// }
// );
this._queryShadowNodes(inSelector, outNode.shadowRoot);
}
});
}
} else {
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
}
});
};
_register = () => {
const { fromLang, toLang, injectJs, injectCss, fixerSelector, fixerFunc } =
this._rule;
if (fromLang === toLang) {
return;
}
// webfix
if (fixerSelector && fixerFunc !== "-") {
runFixer(fixerSelector, fixerFunc);
}
// 注入用户JS/CSS
if (isExt) {
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
} else {
injectJs && injectInlineJs(injectJs);
injectCss && injectInternalCss(injectCss);
}
// 搜索节点
this._queryNodes();
this._rootNodes.forEach((node) => {
// 监听节点变化;
this._mutaObserver.observe(node, {
childList: true,
subtree: true,
// characterData: true,
});
});
if (
!this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 监听节点显示
this._tranNodes.forEach((_, node) => {
this._interseObserver.observe(node);
});
} else if (this._rule.transTiming === OPT_TIMING_PAGEOPEN) {
// 全文直接翻译
this._tranNodes.forEach((_, node) => {
this._render(node);
});
} else {
// 监听鼠标悬停
window.addEventListener("keydown", this._handleKeydown);
this._tranNodes.forEach((_, node) => {
node.addEventListener("mouseenter", this._handleMouseover);
node.addEventListener("mouseleave", this._handleMouseout);
});
}
// 翻译页面标题
if (this._rule.transTitle === "true" && !this._docTitle) {
const title = document.title;
this._docTitle = title;
this.translateText(title).then((trText) => {
document.title = `${trText} | ${title}`;
});
}
};
_handleMouseover = (e) => {
// console.log("mouseenter", e);
if (!this._tranNodes.has(e.target)) {
return;
}
const key = this._rule.transTiming.slice(3);
if (this._rule.transTiming === OPT_TIMING_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseenter", this._handleMouseover);
e.target.removeEventListener("mouseleave", this._handleMouseout);
this._render(e.target);
} else {
this._mouseoverNode = e.target;
}
};
_handleMouseout = (e) => {
// console.log("mouseleave", e);
if (!this._tranNodes.has(e.target)) {
return;
}
this._mouseoverNode = null;
};
_handleKeydown = (e) => {
// console.log("keydown", e);
const key = this._rule.transTiming.slice(3);
if (e[key] && this._mouseoverNode) {
this._mouseoverNode.removeEventListener(
"mouseenter",
this._handleMouseover
);
this._mouseoverNode.removeEventListener(
"mouseleave",
this._handleMouseout
);
const node = this._mouseoverNode;
this._render(node);
this._mouseoverNode = null;
}
};
_unRegister = () => {
// 恢复页面标题
if (this._docTitle) {
document.title = this._docTitle;
this._docTitle = "";
}
// 解除节点变化监听
this._mutaObserver.disconnect();
// 解除节点显示监听
// this._interseObserver.disconnect();
// 移除键盘监听
window.removeEventListener("keydown", this._handleKeydown);
this._tranNodes.forEach((innerHTML, node) => {
if (
!this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 解除节点显示监听
this._interseObserver.unobserve(node);
} else if (this._rule.transTiming !== OPT_TIMING_PAGEOPEN) {
// 移除鼠标悬停监听
// node.style.pointerEvents = "none";
node.removeEventListener("mouseenter", this._handleMouseover);
node.removeEventListener("mouseleave", this._handleMouseout);
}
// 移除/恢复元素
if (innerHTML && this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} else {
node.querySelector(APP_LCNAME)?.remove();
}
});
// 移除用户JS/CSS
this._removeInjector();
// 清空节点集合
this._rootNodes.clear();
this._tranNodes.clear();
// 清空任务池
clearFetchPool();
};
_removeInjector = () => {
document
.querySelectorAll(`[data-source^="KISS-Calendar"]`)
?.forEach((el) => el.remove());
};
_reTranslate = debounce(() => {
if (this._rule.transOpen === "true") {
window.removeEventListener("keydown", this._handleKeydown);
this._mutaObserver.disconnect();
this._interseObserver.disconnect();
this._removeInjector();
this._register();
}
}, this._setting.transInterval);
_invalidLength = (q) =>
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH);
_render = (el) => {
let traEl = el.querySelector(APP_LCNAME);
// 已翻译
if (traEl) {
if (this._rule.transOnly === "true") {
return;
}
const preText = this._tranNodes.get(el);
const curText = el.innerText.trim();
// const traText = traEl.innerText.trim();
// todo
// 1. traText when loading
// 2. replace startsWith
if (curText.startsWith(preText)) {
return;
}
traEl.remove();
}
let q = el.innerText.trim();
if (this._rule.transOnly === "true") {
this._tranNodes.set(el, el.innerHTML);
} else {
this._tranNodes.set(el, q);
}
const keeps = [];
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
let text = "";
el.childNodes.forEach((child) => {
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}]`;
keeps.push(child.outerHTML);
} else {
text += child.textContent;
}
});
if (keeps.length > 0) {
// textContent会保留些无用的换行符,严重影响翻译质量
if (q.includes("\n")) {
q = text;
} else {
q = text.replaceAll("\n", " ");
}
}
}
// 太长或太短
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";
// 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;
}
const root = createRoot(traEl);
root.render();
};
}