Background Translator

test

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Background Translator
// @match        https://wtr-lab.com/*/chapter-*?service=web*
// @match        https://translate.google.com/?sl=auto&tl=en&op=translate
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @description  test
// @version 0.0.1.20260510071109
// @namespace https://greasyfork.org/users/1527990
// ==/UserScript==

(function() {
    'use strict';

    let lastUrl = location.href;
    const TARGET = '.chapter-body';

    const isChineseChapter = (t) => /第\s*\d+\s*章/.test(t) || /[\u4e00-\u9fa5]/.test(t);

// --- GOOGLE TRANSLATE SIDE ---
    if (location.href.includes('translate.google.com')) {
        GM_addValueChangeListener("request_translate", function(name, old_val, new_val) {
            if (!new_val) return;

            // 1. Extract the specific number from the Chinese source (e.g., 86)
            const sourceMatch = new_val.match(/第\s*(\d+)\s*章/);
            const targetNum = sourceMatch ? sourceMatch[1] : null;

            if (!targetNum) {
                console.error("Could not find Chapter Number in source text.");
                return;
            }

            let inputField = document.querySelector('textarea[aria-label="Source text"]');
            if (!inputField) return;

            inputField.focus();
            inputField.value = new_val;
            inputField.dispatchEvent(new Event('input', { bubbles: true }));

            const startTime = Date.now();
            let stabilityCounter = 0;
            let lastFoundText = "";

            let checkResult = setInterval(() => {
                // Get all translation parts
                let parts = document.querySelectorAll('.ryNqvb, span[jsname="W297wb"]');
                if (parts.length === 0) return;

                // The FIRST part should be our Chapter header
                let firstLine = parts[0].innerText.trim();

                // 2. THE FORMAT CHECK: Must be "Chapter [Number]"
                // We use a Regex to ensure "Chapter" is there and the number matches exactly
                const chapterPattern = new RegExp(`^Chapter\\s*${targetNum}`, 'i');
                const isCorrectChapter = chapterPattern.test(firstLine);

                let currentFullText = Array.from(parts).map(p => p.innerText.trim()).filter(t => t.length > 0).join('\n\n');

                // DEBUG LOGGING
                console.log(`Waiting for: Chapter ${targetNum} | Currently seeing: "${firstLine}"`);

                // 3. VALIDATION GATES
                if (isCorrectChapter && currentFullText.length > (new_val.length * 0.3) && currentFullText === lastFoundText) {
                    stabilityCounter++;
                } else {
                    stabilityCounter = 0; // Reset if the number disappears or text flickers
                }

                lastFoundText = currentFullText;

                // Return only after passing all checks for 2 cycles (10 seconds total)
                if (stabilityCounter >= 2) {
                    console.log("MATCH CONFIRMED. Returning Chapter " + targetNum);
                    GM_setValue("receive_translate", currentFullText);
                    GM_setValue("request_translate", "");
                    clearInterval(checkResult);
                } else if (Date.now() - startTime > 60000) {
                    GM_setValue("receive_translate", "Error: Timeout waiting for Chapter " + targetNum);
                    GM_setValue("request_translate", "");
                    clearInterval(checkResult);
                }
            }, 5000);
        });
        return;
    }

// --- HELPER: Get Chapter from URL ---
    const getUrlChapter = () => {
        const match = location.href.match(/chapter-(\d+)/);
        return match ? match[1] : null;
    };

    // --- NOVEL SITE SIDE ---

    function runTranslation() {
        const urlNum = getUrlChapter();
        if (!urlNum) return;

        // 1. Target the specific container for the current chapter
        const container = document.getElementById(`chapter-${urlNum}`) || document.querySelector(`.chapter-container#chapter-${urlNum}`);
        const div = container ? container.querySelector(TARGET) : document.querySelector(TARGET);

        if (!div || div.dataset.status === "processing" || div.dataset.status === "done" || !isChineseChapter(div.innerText)) return;

        div.dataset.status = "processing";

        setTimeout(() => {
            // Verify we haven't clicked 'next' again during the 3s wait
            if (getUrlChapter() !== urlNum) return;

            const lines = div.querySelectorAll('.wtr-line');
            let textArray = [];
            lines.forEach(line => {
                let lineClone = line.cloneNode(true);
                lineClone.querySelectorAll('.para-number-overlay').forEach(e => e.remove());
                let t = lineClone.innerText.trim();
                if (t) textArray.push(t);
            });

            const textToSend = textArray.join('\n\n');
            if (textToSend.length < 10) {
                div.dataset.status = "";
                return;
            }

            div.style.opacity = "0.5";
            GM_setValue("receive_translate", "");
            GM_setValue("request_translate", textToSend);
        }, 3000);
    }

    // --- SPA ENGINE (PRE-LOAD AWARE) ---
    setInterval(() => {
        const currentUrl = location.href;
        const urlNum = getUrlChapter();

        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            console.log("SPA Navigated to chapter:", urlNum);

            // Cleanup: The SPA keeps old chapters, so we remove the OLD overlay
            // but keep the current one if it exists.
            document.querySelectorAll('#ai-overlay').forEach(el => {
                // If the overlay text doesn't match the new chapter number, kill it
                if (!el.innerText.includes(urlNum)) el.remove();
            });
        }

        // Trigger translation
        runTranslation();
    }, 1500);

    // --- HANDLE TRIP BACK ---
    GM_addValueChangeListener("receive_translate", function(n, o, val) {
        if (!val) return;

        // Find the div matching the current URL
        const urlNum = getUrlChapter();
        const container = document.getElementById(`chapter-${urlNum}`);
        const div = container ? container.querySelector(TARGET) : document.querySelector(TARGET);

        if (!div) return;

        // Check if overlay already exists for this specific container
        let overlay = div.parentNode.querySelector('#ai-overlay');
        if (!overlay) {
            overlay = document.createElement('div');
            overlay.id = 'ai-overlay';
            overlay.className = div.className;
            overlay.style.cssText = `
                display: block !important;
                opacity: 1 !important;
                line-height: 1.4 !important;
                word-break: break-word !important;
            `;
            div.parentNode.insertBefore(overlay, div);
        }

        overlay.innerHTML = '';
        const rawLines = val.replace(/waerdygjfkstlhujhgfd/g, 'awdrsgyjekfthuhgf').split('\n');
        let lastLineWasEmpty = false;

        rawLines.forEach(lineText => {
            const trimmed = lineText.trim();
            if (trimmed === '' && lastLineWasEmpty) return;

            const p = document.createElement('div');
            if (trimmed === '') {
                p.style.height = "0.4em";
                lastLineWasEmpty = true;
            } else {
                p.innerText = trimmed;
                p.style.marginBottom = "0.4em";
                lastLineWasEmpty = false;
            }
            overlay.appendChild(p);
        });

        div.style.display = "none";
        div.setAttribute('data-status', 'done');
        GM_setValue("receive_translate", "");
    });
})();