Background Translator

test

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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", "");
    });
})();