const BLOCKED_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"]); const SCROLL_DEBOUNCE_MS = 250; const MAX_NODES_PER_PASS = 30; const originalTextMap = new Map(); let autoTranslateEnabled = false; let scrollTimer = null; let translating = false; let needRerun = false; function isVisible(element) { let el = element; while (el) { const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden") { return false; } el = el.parentElement; } return true; } function isInViewport(element) { const rect = element.getBoundingClientRect(); return rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth; } function collectTextNodes(onlyViewport) { const root = document.body || document.documentElement; if (!root) { return []; } const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { const text = node.nodeValue?.trim(); const parent = node.parentElement; if (!text || !parent) { return NodeFilter.FILTER_REJECT; } if (BLOCKED_TAGS.has(parent.tagName) || parent.isContentEditable) { return NodeFilter.FILTER_REJECT; } if (originalTextMap.has(node)) { return NodeFilter.FILTER_REJECT; } if (!isVisible(parent)) { return NodeFilter.FILTER_REJECT; } if (onlyViewport && !isInViewport(parent)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }); const nodes = []; while (walker.nextNode()) { nodes.push(walker.currentNode); } return nodes; } function requestTranslation(text) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: "translate", text }, (response) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; } if (!response?.ok) { reject(new Error(response?.error || "翻译失败")); return; } resolve(response.text); }); }); } function stopAutoTranslate() { autoTranslateEnabled = false; if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null; } window.removeEventListener("scroll", handleScroll); window.removeEventListener("resize", handleScroll); } async function translateVisibleOnce() { const nodes = collectTextNodes(true); if (!nodes.length) { return 0; } const batch = nodes.slice(0, MAX_NODES_PER_PASS); for (const node of batch) { if (!autoTranslateEnabled) { break; } const original = node.nodeValue; const translated = await requestTranslation(original); originalTextMap.set(node, original); node.nodeValue = translated; } if (nodes.length > batch.length) { needRerun = true; } return batch.length; } async function runTranslateCycle() { if (!autoTranslateEnabled) { return; } if (translating) { needRerun = true; return; } translating = true; try { await translateVisibleOnce(); } catch (err) { stopAutoTranslate(); alert(`Transly: ${err.message || "翻译失败"}`); } finally { translating = false; if (autoTranslateEnabled && needRerun) { needRerun = false; setTimeout(() => { runTranslateCycle().catch(() => {}); }, 0); } } } function handleScroll() { if (!autoTranslateEnabled) { return; } if (scrollTimer) { clearTimeout(scrollTimer); } scrollTimer = setTimeout(() => { runTranslateCycle().catch(() => {}); }, SCROLL_DEBOUNCE_MS); } async function translatePage() { const hasCurrentNodes = collectTextNodes(true).length > 0; if (!autoTranslateEnabled) { autoTranslateEnabled = true; window.addEventListener("scroll", handleScroll, { passive: true }); window.addEventListener("resize", handleScroll); } await runTranslateCycle(); if (!hasCurrentNodes && !originalTextMap.size) { alert("Transly: 当前可视区域没有可翻译文本"); } } function restorePage() { stopAutoTranslate(); if (!originalTextMap.size) { alert("Transly: 没有可恢复内容"); return; } for (const [node, text] of originalTextMap.entries()) { if (node?.isConnected) { node.nodeValue = text; } } originalTextMap.clear(); } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.action === "translatePage") { translatePage() .then(() => sendResponse({ ok: true })) .catch((err) => sendResponse({ ok: false, error: err.message || "翻译失败" })); return true; } if (message?.action === "restorePage") { restorePage(); sendResponse({ ok: true }); return; } if (message?.action === "showTranslation") { alert(message.text || ""); sendResponse({ ok: true }); return; } if (message?.action === "showError") { alert(`Transly: ${message.error || "翻译失败"}`); sendResponse({ ok: true }); } });