Enhanced Translation

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();
        }
    });
})();