chinese2english/src/client/index.ts

182 lines
4.8 KiB
TypeScript

import { DICTIONARY, SORTED_PAIRS, DYNAMIC_PATTERNS } from "./dictionary.js";
export const name = "dsh-plugin-english-client";
export const inject = [];
const CHINESE_REGEX = /[\u4e00-\u9fa5]/;
/**
* Translates a single text string using dictionary and pattern matching.
*/
export function translateText(text: string): string {
if (!text || !CHINESE_REGEX.test(text)) return text;
const trimmed = text.trim();
if (DICTIONARY[trimmed]) {
return text.replace(trimmed, DICTIONARY[trimmed]);
}
let result = text;
// Dynamic regex patterns (counts, numbers, time)
for (const [pattern, replacement] of DYNAMIC_PATTERNS) {
result = result.replace(pattern, replacement);
}
// Exact phrase search for substrings (longest phrases first)
for (const [cn, en] of SORTED_PAIRS) {
if (result.includes(cn)) {
result = result.replaceAll(cn, en);
if (!CHINESE_REGEX.test(result)) break;
}
}
return result;
}
let isTranslating = false;
/**
* Translates an element and all its children using native TreeWalker.
*/
export function translateElement(root: Node) {
if (!root) return;
if (root.nodeType === Node.TEXT_NODE) {
const val = root.nodeValue;
if (val && CHINESE_REGEX.test(val)) {
const translated = translateText(val);
if (translated !== val) {
root.nodeValue = translated;
}
}
return;
}
if (root.nodeType !== Node.ELEMENT_NODE) return;
const el = root as HTMLElement;
const tag = el.tagName;
if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return;
// Translate attributes
for (const attr of ["placeholder", "title", "aria-label"]) {
const val = el.getAttribute(attr);
if (val && CHINESE_REGEX.test(val)) {
el.setAttribute(attr, translateText(val));
}
}
// Fast native TreeWalker
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
let textNode = walker.nextNode();
while (textNode) {
const val = textNode.nodeValue;
if (val && CHINESE_REGEX.test(val)) {
const parentTag = textNode.parentElement?.tagName;
if (parentTag !== "SCRIPT" && parentTag !== "STYLE" && parentTag !== "PRE" && parentTag !== "CODE") {
const translated = translateText(val);
if (translated !== val) {
textNode.nodeValue = translated;
}
}
}
textNode = walker.nextNode();
}
}
function initTranslator() {
if (typeof document === "undefined" || (window as any).__DSH_TRANSLATOR_ACTIVE__) return;
(window as any).__DSH_TRANSLATOR_ACTIVE__ = true;
console.log("[dsh-plugin-english] Universal English translator activated.");
const queue = new Set<Node>();
let frameRequested = false;
const processQueue = () => {
frameRequested = false;
isTranslating = true;
try {
const nodes = Array.from(queue);
queue.clear();
for (const n of nodes) {
if (n.isConnected) {
translateElement(n);
}
}
} finally {
isTranslating = false;
}
};
const observer = new MutationObserver((mutations) => {
if (isTranslating) return;
for (const m of mutations) {
if (m.type === "childList") {
for (let i = 0; i < m.addedNodes.length; i++) {
const n = m.addedNodes[i];
if (n.nodeType === Node.TEXT_NODE || n.nodeType === Node.ELEMENT_NODE) {
queue.add(n);
}
}
} else if (m.type === "characterData") {
if (m.target.nodeValue && CHINESE_REGEX.test(m.target.nodeValue)) {
queue.add(m.target);
}
} else if (m.type === "attributes") {
queue.add(m.target);
}
}
if (!frameRequested && queue.size > 0) {
frameRequested = true;
requestAnimationFrame(processQueue);
}
});
// Observe root
const root = document.documentElement || document.body;
if (root) {
translateElement(root);
observer.observe(root, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ["placeholder", "title", "aria-label"],
});
} else {
document.addEventListener("DOMContentLoaded", () => {
translateElement(document.body);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ["placeholder", "title", "aria-label"],
});
});
}
// Periodic safety check every 1s for any dynamically inserted lazy elements
setInterval(() => {
if (document.body && !isTranslating) {
isTranslating = true;
try {
translateElement(document.body);
} finally {
isTranslating = false;
}
}
}, 1000);
}
// Start immediately when the bundle loads in browser
if (typeof window !== "undefined") {
initTranslator();
}
export function apply(ctx: any) {
initTranslator();
}