Translator bilingual (Microsoft Engine)

Toggle translation without losing original formatting, resilient button injection via Edge Microsoft API

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Translator bilingual (Microsoft Engine)
// @namespace    http://tampermonkey.net/
// @version      21.03.2026
// @description  Toggle translation without losing original formatting, resilient button injection via Edge Microsoft API
// @author       Sspuramcopigemi
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @connect      edge.microsoft.com
// ==/UserScript==

(function() {
    'use strict';

    let modeActive = false;

    const isMarathi = (text) => /[\u0900-\u097F]/.test(text);

    async function translateText(text, targetLang) {
        return new Promise((resolve) => {
            // Replaced with the Microsoft Edge Free Translation Endpoint
            const url = `https://edge.microsoft.com/translate/auth`;
            
            // Step 1: Microsoft requires a quick handshake request to issue a temporary token
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                timeout: 3000,
                onload: (tokenRes) => {
                    const token = tokenRes.responseText;
                    if (!token) { resolve(null); return; }

                    // Step 2: Use the token to fetch the translation from Microsoft engine
                    const translateUrl = `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${targetLang}`;
                    
                    GM_xmlhttpRequest({
                        method: "POST",
                        url: translateUrl,
                        headers: {
                            "Authorization": `Bearer ${token}`,
                            "Content-Type": "application/json"
                        },
                        data: JSON.stringify([{ "Text": text }]),
                        timeout: 5000,
                        onload: (res) => {
                            try {
                                const result = JSON.parse(res.responseText);
                                // Microsoft API format returns data as: [{ translations: [{ text: "..." }] }]
                                resolve(result[0].translations[0].text || null);
                            } catch (e) { resolve(null); }
                        },
                        onerror: () => resolve(null)
                    });
                },
                onerror: () => resolve(null)
            });
        });
    }

    // ✅ Menu command added
    GM_registerMenuCommand("Toggle En-Mr Mode", () => {
        modeActive = !modeActive;
        const btn = document.querySelector('#translator-toggle button');
        if (btn) btn.textContent = modeActive ? 'ON' : 'OFF';
        if (modeActive) { enableSentenceMode(); } else { disableSentenceMode(); }
    });

    function injectTranslatorButton() {
        if (document.querySelector('#translator-toggle')) return;

        const toggleBox = document.createElement('div');
        toggleBox.id = 'translator-toggle';
        const toggleBtn = document.createElement('button');
        toggleBtn.textContent = 'OFF';
        toggleBox.appendChild(toggleBtn);

        // Strong CSS
        const style = document.createElement('style');
        style.textContent = `
            #translator-toggle {
                position: fixed !important;
                bottom: 10px !important;
                left: 60px !important;
                padding: 2px 6px !important;
                background: transparent !important;
                border: 1px solid transparent !important;
                border-radius: 6px !important;
                z-index: 2147483647 !important;
            }
            #translator-toggle button {
                border: none !important;
                background: transparent !important;
                color: #696969 !important;
                font-size: 12px !important;
                cursor: pointer !important;
                font-family: system-ui, sans-serif !important;
            }
            #translator-toggle button:hover { text-decoration: underline !important; }
        `;
        document.head.appendChild(style);

        // Append to both html and body
        if (document.body) document.body.appendChild(toggleBox);
        document.documentElement.appendChild(toggleBox);

        toggleBtn.addEventListener('click', () => {
            modeActive = !modeActive;
            toggleBtn.textContent = modeActive ? 'ON' : 'OFF';
            if (modeActive) { enableSentenceMode(); } else { disableSentenceMode(); }
        });
    }

    // Inject after load
    window.addEventListener('load', injectTranslatorButton);

    // Reinjection checks
    const observer = new MutationObserver(() => {
        injectTranslatorButton();
        if (modeActive) enableSentenceMode(); // Keeps checking for newly loaded articles on page switches
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });
    setInterval(injectTranslatorButton, 2000);

    function enableSentenceMode() {
        // FIXED: Expanded the element matcher to intercept general text div elements used by News on Air
        const elements = Array.from(document.querySelectorAll('p, li, blockquote, h1, h2, h3, div[class*="content"], div[class*="story"], div[class*="article"]'))
                              .filter(el => {
                                  if (el.closest('#translator-toggle')) return false;
                                  // Ensure we don't accidentally split giant parent blocks that have structured children wrappers
                                  if (el.tagName === 'DIV' && el.querySelector('p, li')) return false; 
                                  return el.innerText && el.innerText.trim().length > 5;
                              });

        for (let el of elements) {
            if (el.getAttribute('data-sentence-mode')) continue;

            el.setAttribute('data-original-html', el.innerHTML); 
            const segments = el.innerText.trim().match(/[^.!?।।]+[.!?।।]?/g) || [];
            el.innerHTML = '';
            el.setAttribute('data-sentence-mode', 'true');

            for (const fullSentence of segments) {
                if (!fullSentence || fullSentence.trim().length < 2) continue;

                const spanSentence = document.createElement('span');
                spanSentence.innerText = `${fullSentence.trim()} `;
                spanSentence.style.cursor = 'pointer';
                spanSentence.style.color = '#444';
                spanSentence.style.fontWeight = '500';

                spanSentence.addEventListener('click', async () => {
                    const existing = spanSentence.querySelector('.translation-span');
                    if (existing) { existing.remove(); return; }

                    const targetLang = isMarathi(fullSentence) ? 'en' : 'mr';
                    const translation = await translateText(fullSentence, targetLang);
                    if (translation) {
                        const spanTrans = document.createElement('span');
                        spanTrans.innerText = ` [${translation}] `;
                        spanTrans.style = `color: ${targetLang === 'en' ? '#007bff' : '#d81b60'}; font-weight: 500; background: rgba(0,0,0,0.04); padding: 0 2px; margin-left: 5px; border-radius: 3px; font-size: 0.95em;`;
                        spanTrans.className = 'translation-span';
                        spanSentence.appendChild(spanTrans);
                    }
                });
                el.appendChild(spanSentence);
            }
        }
    }

    function disableSentenceMode() {
        const elements = Array.from(document.querySelectorAll('[data-sentence-mode]'));
        for (let el of elements) {
            const originalHTML = el.getAttribute('data-original-html');
            if (originalHTML) {
                el.innerHTML = originalHTML;
            }
            el.removeAttribute('data-sentence-mode');
            el.removeAttribute('data-original-html');
        }
    }
})();