您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Translate a webpage Translation with built-in AI into your preferred language, making browsing easier and faster.
// ==UserScript== // @name Enhanced Translation // @namespace your-namespace // @description Translate a webpage Translation with built-in AI into your preferred language, making browsing easier and faster. // @version 1.5 // @author UniverseDev // @license GPL-3.0-or-later // @match *://*/* // @grant none // ==/UserScript== (async function () { 'use strict'; const CONFIG = { targetLanguage: 'en', debugMode: true, translationAttribute: 'data-gm-translated', excludedElementsSelector: 'code, pre, .notranslate, img, svg, video, audio, kbd, samp, var, math, noscript, script, style', translationBatchSize: 250, // Reduced batch size for smoother updates dynamicContentDebounceDelay: 300, translationQueueDebounceDelay: 100, // Debounce for the translation queue processing textContainingElementsSelector: 'p, h1, h2, h3, h4, h5, h6, span, a, div, li, dt, dd, blockquote, th, td, summary, figcaption, label, button, textarea, select, option, sr-only', translatableAttributes: ['title', 'placeholder', 'alt', 'aria-label'], loadingIndicatorStyle: ` position: fixed; top: 10px; left: 10px; background-color: rgba(0, 0, 0, 0.7); color: white; padding: 8px 15px; border-radius: 5px; z-index: 10000; font-size: 14px; `, loadingIndicatorText: 'Translating Initial Visible Content...', // Updated initial text loadingIndicatorUpdatingText: (translatedCount, queueSize) => `Translating... (${translatedCount} translated, ${queueSize} in queue)`, // More informative useIntersectionObserver: true, intersectionObserverOptions: { rootMargin: '0px', threshold: 0.1 }, showErrorBanners: false, // Option to disable error banners useTurboMode: true, // NEW: Turbo mode for faster translation }; let dynamicContentTimer; let translationQueueTimer; let pageLanguage; let loadingIndicator; let translatedElementCount = 0; const translationQueue = new Set(); let isIdleCallbackRunning = false; let intersectionObserver; let langAttributeObserver; const targetLanguageCode = CONFIG.targetLanguage.toLowerCase(); // More descriptive variable name const isTrulyVisibleCache = new WeakMap(); const domManipulationQueue = []; // Track active translators and detectors to allow for destruction const activeTranslators = new Map(); const activeDetectors = new Map(); function logDebug(message, ...args) { if (CONFIG.debugMode) { console.log(`${new Date().toLocaleTimeString()} - DEBUG: ${message}`, ...args); } } function normalizeLang(lang) { return lang ? lang.toLowerCase().split('-')[0] : ''; } function showTranslationError(message) { if (CONFIG.showErrorBanners) { const errorBanner = document.createElement('div'); errorBanner.style.cssText = `position: fixed; bottom: 0; width: 100%; background-color: #f44336; color: #fff; text-align: center; padding: 10px; z-index: 9999;`; errorBanner.textContent = `Translation Error: ${message}`; document.body.appendChild(errorBanner); setTimeout(() => errorBanner.remove(), 5000); } console.error(`Translation Error: ${message}`); } const isTranslationSupported = (() => { try { return 'translation' in self && typeof self.translation.createTranslator === 'function'; } catch (error) { console.error("Error checking translation API:", error); return false; } })(); if (!isTranslationSupported) { logDebug("Translation features are unavailable."); return; } let globalLanguageDetector = null; async function getLanguageDetector(options = {}) { if (globalLanguageDetector) { return globalLanguageDetector; } try { const controller = new AbortController(); const detector = await self.translation.createDetector({ signal: options.signal }); activeDetectors.set(detector, controller); globalLanguageDetector = detector; return detector; } catch (error) { logDebug(`Error creating language detector: ${error.message}`); if (error.name !== 'AbortError') { showTranslationError(error.message); } return null; } } async function destroyLanguageDetector() { if (globalLanguageDetector) { const controller = activeDetectors.get(globalLanguageDetector); if (controller) { controller.abort(); activeDetectors.delete(globalLanguageDetector); } globalLanguageDetector.destroy(); globalLanguageDetector = null; logDebug('Language detector destroyed and resources released.'); } } async function detectLanguage(text) { // Centralized language detection const detector = await getLanguageDetector(); if (!detector) return 'unknown'; try { const detectionResult = (await detector.detect(text))[0]; if (!detectionResult || detectionResult.confidence < 0.5) { logDebug(`Detected language uncertain: ${detectionResult?.detectedLanguage}, confidence: ${detectionResult?.confidence}`); return 'unknown'; } logDebug(`Detected source language: ${detectionResult.detectedLanguage}, confidence: ${(detectionResult.confidence * 100).toFixed(1)}%`); return normalizeLang(detectionResult.detectedLanguage) || 'unknown'; } catch (error) { logDebug(`Language detection error: ${error}`); return 'unknown'; } } const translatorCache = new Map(); async function getTranslator(sourceLang, options = {}) { const key = `${sourceLang}-${targetLanguageCode}`; if (translatorCache.has(key)) { return translatorCache.get(key); } try { const controller = new AbortController(); const translator = await self.translation.createTranslator({ sourceLanguage: sourceLang, targetLanguage: targetLanguageCode, signal: options.signal }); activeTranslators.set(translator, controller); translatorCache.set(key, translator); return translator; } catch (error) { logDebug(`Error creating translator for ${key}: ${error.message}`); if (error.name !== 'AbortError') { showTranslationError(error.message); } return null; } } async function destroyTranslator(translator) { if (activeTranslators.has(translator)) { const controller = activeTranslators.get(translator); controller.abort(); // Abort any ongoing operations activeTranslators.delete(translator); for (const [key, cachedTranslator] of translatorCache.entries()) { if (cachedTranslator === translator) { translatorCache.delete(key); break; } } translator.destroy(); logDebug('Translator destroyed and resources released.'); } } async function destroyAllTranslators() { for (const translator of activeTranslators.keys()) { await destroyTranslator(translator); } } async function translateContent(text, sourceLang, options = {}) { if (!text || text.trim() === '') { return text; } const translator = await getTranslator(sourceLang, { signal: options.signal }); if (!translator || typeof translator.translate !== 'function') { logDebug("Translation API's `translate` method not found or invalid."); return text; } try { return await translator.translate(text, { signal: options.signal }); } catch (error) { logDebug(`Error during translation: ${error.message}`); if (error.name !== 'AbortError') { showTranslationError(error.message); } return text; } } function isTrulyVisible(element) { if (!element) return false; if (isTrulyVisibleCache.has(element)) { return isTrulyVisibleCache.get(element); } const style = window.getComputedStyle(element); const isVisible = style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0 && element.offsetParent !== null; isTrulyVisibleCache.set(element, isVisible); return isVisible; } function queueDOMManipulation(callback) { domManipulationQueue.push(callback); if (domManipulationQueue.length === 1) { requestAnimationFrame(processDOMManipulationQueue); } } function processDOMManipulationQueue() { while (domManipulationQueue.length > 0) { const callback = domManipulationQueue.shift(); callback(); } } async function translateTextNode(node, sourceLang, options = {}) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) { const originalText = node.textContent; const translatedText = await translateContent(originalText, sourceLang, options); if (translatedText && translatedText !== originalText) { queueDOMManipulation(() => { node.textContent = translatedText; logDebug(`Translated text node: "${originalText}" to "${translatedText}"`); }); } } } async function translateAttributes(element, sourceLang, options = {}) { let didTranslate = false; const translations = {}; for (const attribute of CONFIG.translatableAttributes) { if (element.hasAttribute(attribute)) { const originalValue = element.getAttribute(attribute); const translatedValue = await translateContent(originalValue, sourceLang, options); if (translatedValue && translatedValue !== originalValue) { translations[attribute] = translatedValue; didTranslate = true; } } } if (Object.keys(translations).length > 0) { queueDOMManipulation(() => { for (const attribute in translations) { element.setAttribute(attribute, translations[attribute]); logDebug(`Translated ${attribute}: "${element.getAttribute(attribute)}" to "${translations[attribute]}"`); } }); } return didTranslate; } async function translateElementContent(element, sourceLang, options = {}) { const textNodePromises = []; for (const node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { const originalText = node.textContent; const translatedTextPromise = translateContent(originalText, sourceLang, options); textNodePromises.push(translatedTextPromise); translatedTextPromise.then(translatedText => { if (translatedText && translatedText !== originalText) { queueDOMManipulation(() => { node.textContent = translatedText; logDebug(`Translated text node: "${originalText}" to "${translatedText}"`); }); } }); } } const results = await Promise.all(textNodePromises); return results.some(translatedText => translatedText !== undefined); } function shouldTranslateElement(element) { return isTrulyVisible(element) && !element.hasAttribute(CONFIG.translationAttribute); } async function translateElement(element) { if (!shouldTranslateElement(element)) { return; } let needsTranslation = false; let sourceLang = pageLanguage; const elementLang = normalizeLang(element.closest('[lang]')?.lang); if (elementLang && elementLang !== targetLanguageCode) { sourceLang = elementLang; needsTranslation = true; logDebug(`Using element-level language: ${sourceLang} for`, element); } else if (!sourceLang) { const textToDetect = element.textContent.substring(0, 200); if (/\w+/.test(textToDetect)) { const detectedLang = await detectLanguage(textToDetect); if (detectedLang !== 'unknown' && normalizeLang(detectedLang) !== targetLanguageCode) { sourceLang = detectedLang; needsTranslation = true; logDebug(`Detected element language: ${sourceLang} for`, element); } else if (detectedLang === targetLanguageCode) { queueDOMManipulation(() => element.setAttribute(CONFIG.translationAttribute, 'true')); return; } } else { queueDOMManipulation(() => element.setAttribute(CONFIG.translationAttribute, 'true')); return; } } else if (normalizeLang(sourceLang) === targetLanguageCode) { queueDOMManipulation(() => element.setAttribute(CONFIG.translationAttribute, 'true')); return; } else { needsTranslation = true; } let contentTranslated = false; let attributesTranslated = false; if (needsTranslation) { attributesTranslated = await translateAttributes(element, sourceLang); contentTranslated = await translateElementContent(element, sourceLang); } if (needsTranslation && (contentTranslated || attributesTranslated)) { queueDOMManipulation(() => { element.setAttribute(CONFIG.translationAttribute, 'true'); }); } translatedElementCount++; requestAnimationFrame(() => { if (loadingIndicator && typeof CONFIG.loadingIndicatorUpdatingText === 'function') { loadingIndicator.textContent = CONFIG.loadingIndicatorUpdatingText(translatedElementCount, translationQueue.size); } }); } async function processBatch(elements) { if (CONFIG.useTurboMode) { const translationPromises = elements.map(async (element) => { if (isTrulyVisible(element)) { await translateElement(element); } }); await Promise.all(translationPromises); } else { for (const element of elements) { if (isTrulyVisible(element)) { await translateElement(element); } } } } function showLoadingIndicator() { loadingIndicator = document.createElement('div'); loadingIndicator.style.cssText = CONFIG.loadingIndicatorStyle; loadingIndicator.textContent = CONFIG.loadingIndicatorText; document.body.appendChild(loadingIndicator); } function hideLoadingIndicator() { if (loadingIndicator) { loadingIndicator.remove(); loadingIndicator = null; translatedElementCount = 0; } } function queryShadowDOM(root, selector) { let elements = Array.from(root.querySelectorAll(selector)); const shadowHosts = root.querySelectorAll('*'); shadowHosts.forEach(host => { if (host.shadowRoot) { elements = elements.concat(Array.from(queryShadowDOM(host.shadowRoot, selector))); // Convert NodeList to Array } }); return elements; } async function translateVisibleContent(elements) { const visibleElementsToTranslate = elements.filter(el => isTrulyVisible(el)); logDebug(`Found ${visibleElementsToTranslate.length} initially visible elements to translate.`); for (let i = 0; i < visibleElementsToTranslate.length; i += CONFIG.translationBatchSize) { const batch = visibleElementsToTranslate.slice(i, i + CONFIG.translationBatchSize); await processBatch(batch); await new Promise(resolve => setTimeout(resolve, 0)); } logDebug('Initial visible page content translation completed.'); hideLoadingIndicator(); } async function translatePageContent() { showLoadingIndicator(); pageLanguage = normalizeLang(document.documentElement.lang) || (await detectLanguage(document.body.innerText.substring(0, 500))); logDebug(`Page language: ${pageLanguage}`); logDebug(`Preferred language: ${targetLanguageCode}`); if (pageLanguage === targetLanguageCode) { logDebug('Page is already in the preferred language.'); hideLoadingIndicator(); return; } const elementsToTranslate = queryShadowDOM(document.body, `${CONFIG.textContainingElementsSelector}:not(${CONFIG.excludedElementsSelector}):not([${CONFIG.translationAttribute}])`); logDebug(`Found ${elementsToTranslate.length} elements to potentially translate (initial).`); if (CONFIG.useIntersectionObserver) { initIntersectionObserver(); elementsToTranslate.forEach(element => { if (isTrulyVisible(element)) { intersectionObserver.observe(element); } }); logDebug('Observing initially visible elements with IntersectionObserver.'); } else { await translateVisibleContent(elementsToTranslate); } } function enqueueTranslatableElement(element) { if (element && !element.matches(CONFIG.excludedElementsSelector) && !element.hasAttribute(CONFIG.translationAttribute) && isTrulyVisible(element)) { translationQueue.add(element); if (!isIdleCallbackRunning) { isIdleCallbackRunning = true; translationQueueTimer = setTimeout(processTranslationQueue, CONFIG.translationQueueDebounceDelay); } } } async function processTranslationQueue() { isIdleCallbackRunning = false; const elementsToProcess = Array.from(translationQueue); translationQueue.clear(); if (CONFIG.useTurboMode) { await Promise.all(elementsToProcess.map(translateElement)); } else { for (const element of elementsToProcess) { await translateElement(element); } } } function initIntersectionObserver() { intersectionObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const element = entry.target; if (!element.hasAttribute(CONFIG.translationAttribute)) { // Ensure it hasn't been translated while waiting enqueueTranslatableElement(element); } observer.unobserve(element); // Disconnect after enqueuing for translation } }); }, CONFIG.intersectionObserverOptions); logDebug('IntersectionObserver initialized.'); } async function translateAddedNode(node) { if (node.nodeType === Node.ELEMENT_NODE) { if (CONFIG.useIntersectionObserver) { if (shouldTranslateElement(node)) { intersectionObserver.observe(node); } } else if (isTrulyVisible(node)) { enqueueTranslatableElement(node); } if (node.shadowRoot) { const shadowElements = queryShadowDOM(node.shadowRoot, `*:not(${CONFIG.excludedElementsSelector}):not([${CONFIG.translationAttribute}])`); shadowElements.forEach(el => { if (CONFIG.useIntersectionObserver && isTrulyVisible(el)) { intersectionObserver.observe(el); } else if (!CONFIG.useIntersectionObserver && isTrulyVisible(el)) { enqueueTranslatableElement(el); } }); } node.querySelectorAll(`*:not(${CONFIG.excludedElementsSelector}):not([${CONFIG.translationAttribute}])`).forEach(child => { if (CONFIG.useIntersectionObserver && isTrulyVisible(child)) { intersectionObserver.observe(child); } else if (!CONFIG.useIntersectionObserver && isTrulyVisible(child)) { enqueueTranslatableElement(child); } }); } else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() && node.parentElement) { if (CONFIG.useIntersectionObserver && isTrulyVisible(node.parentElement)) { intersectionObserver.observe(node.parentElement); } else if (!CONFIG.useIntersectionObserver && isTrulyVisible(node.parentElement)) { enqueueTranslatableElement(node.parentElement); } } } async function handleChildListMutation(mutation) { for (const addedNode of mutation.addedNodes) { await translateAddedNode(addedNode); } } async function handleCharacterDataMutation(mutation) { if (mutation.target.parentNode && isTrulyVisible(mutation.target.parentNode)) { enqueueTranslatableElement(mutation.target.parentNode); } } async function handleAttributeMutation(mutation) { if (mutation.target instanceof Element && CONFIG.translatableAttributes.includes(mutation.attributeName)) { if (mutation.attributeName === 'style' || mutation.attributeName === 'class') { isTrulyVisibleCache.delete(mutation.target); } if (isTrulyVisible(mutation.target)) { enqueueTranslatableElement(mutation.target); } } } async function translateDynamicContent(mutationsList) { for (const mutation of mutationsList) { switch (mutation.type) { case 'childList': await handleChildListMutation(mutation); break; case 'characterData': await handleCharacterDataMutation(mutation); break; case 'attributes': await handleAttributeMutation(mutation); break; } } } function observeDynamicContent() { const observer = new MutationObserver(async (mutationsList) => { if (mutationsList.length > 0) { clearTimeout(dynamicContentTimer); dynamicContentTimer = setTimeout(() => { translateDynamicContent(mutationsList); }, CONFIG.dynamicContentDebounceDelay); } }); observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributeFilter: CONFIG.translatableAttributes.concat(['style', 'class']), attributes: true, }); logDebug('MutationObserver initialized for dynamic content.'); } function initLangAttributeObserver() { langAttributeObserver = new MutationObserver(mutationsList => { mutationsList.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'lang') { const element = mutation.target; logDebug(`'lang' attribute changed on:`, element); element.removeAttribute(CONFIG.translationAttribute); if (isTrulyVisible(element)) { enqueueTranslatableElement(element); } } }); }); langAttributeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'], subtree: true }); logDebug('Lang attribute observer initialized.'); } // Global cleanup function to destroy resources async function cleanup() { logDebug('Cleaning up translation resources...'); await destroyAllTranslators(); await destroyLanguageDetector(); if (intersectionObserver) { intersectionObserver.disconnect(); } if (langAttributeObserver) { langAttributeObserver.disconnect(); } // Optionally disconnect the dynamic content observer if you keep a reference to it. logDebug('Translation resources cleaned up.'); } window.addEventListener('beforeunload', cleanup); window.addEventListener('unload', cleanup); window.addEventListener('load', async () => { if (isTranslationSupported) { await translatePageContent(); observeDynamicContent(); initLangAttributeObserver(); } }); })();