Twitter Translator

Translate tweets with DeepL or Google (with visual text indicators)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Twitter Translator
// @namespace    twitterDeepLTranslator
// @version      1.0
// @description  Translate tweets with DeepL or Google (with visual text indicators)
// @author       Runterya
// @match        https://x.com/*
// @match        https://twitter.com/*
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration Setup ---
    GM_config.init({
        'id': 'DeepL_Config',
        'title': 'Translator Settings',
        'fields': {
            'provider': {
                'label': 'Default Translator',
                'type': 'select',
                'options': ['Google', 'DeepL'],
                'default': 'Google'
            },
            'apiKey': {
                'label': 'DeepL API Key (<a href="https://www.deepl.com/en/your-account/keys" target="_blank" style="color: #1d9bf0; text-decoration: none;">Get your key here</a>)',
                'type': 'text',
                'default': ''
            },
            'targetLang': {
                'label': 'Target Language',
                'type': 'select',
                'options': [
                    'English (EN)',
                    'Turkish (TR)',
                    'German (DE)',
                    'Spanish (ES)',
                    'French (FR)',
                    'Italian (IT)',
                    'Japanese (JA)',
                    'Russian (RU)',
                    'Chinese (ZH)'
                ],
                'default': 'English (EN)'
            }
        },
        'css': `
            #DeepL_Config { background-color: #f8f9fa !important; color: #333 !important; padding: 20px !important; height: 380px !important; }
            #DeepL_Config .config_header { font-size: 20px !important; margin-bottom: 20px !important; }
            #DeepL_Config .field_label { display: block !important; font-weight: bold !important; margin-bottom: 5px !important; color: #000 !important; }
            #DeepL_Config select, #DeepL_Config input[type="text"] { display: block !important; width: 100% !important; height: 35px !important; padding: 5px !important; border: 1px solid #ccc !important; background-color: #fff !important; color: #000 !important; box-sizing: border-box !important; }
            #DeepL_Config .config_var { margin-bottom: 20px !important; }
        `
    });

    GM_registerMenuCommand('Configure Translator', () => GM_config.open());

    // --- Translation Engines ---
    function doGoogleTranslate(text, buttonElement, targetLang) {
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang.toLowerCase()}&dt=t&q=${encodeURIComponent(text)}`;

        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    const translatedText = data[0].map(x => x[0]).join('');
                    applyTranslation(buttonElement, translatedText, '#1d9bf0', 'Google');
                } catch(e) {
                    alert("Google Translate Error.");
                    buttonElement.style.opacity = '1';
                }
            },
            onerror: () => {
                alert("Google Request Failed.");
                buttonElement.style.opacity = '1';
            }
        });
    }

    function doDeepLTranslate(text, buttonElement, targetLang, apiKey) {
        let deepLLang = targetLang.toUpperCase();
        if (deepLLang === 'EN') deepLLang = 'EN-US';

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api-free.deepl.com/v2/translate",
            headers: {
                "Authorization": `DeepL-Auth-Key ${apiKey}`,
                "Content-Type": "application/x-www-form-urlencoded"
            },
            data: `text=${encodeURIComponent(text)}&target_lang=${deepLLang}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.translations) {
                        applyTranslation(buttonElement, data.translations[0].text, '#00ba7c', 'DeepL');
                    } else {
                        console.warn("DeepL API rejected request. Falling back to Google.");
                        doGoogleTranslate(text, buttonElement, targetLang);
                    }
                } catch(e) {
                    console.warn("DeepL Parse Error. Falling back to Google.");
                    doGoogleTranslate(text, buttonElement, targetLang);
                }
            },
            onerror: () => {
                console.warn("DeepL Network Error. Falling back to Google.");
                doGoogleTranslate(text, buttonElement, targetLang);
            }
        });
    }

    // --- Core Logic ---
    async function translateText(text, buttonElement) {
        const provider = GM_config.get('provider');
        const apiKey = GM_config.get('apiKey');
        const rawLang = GM_config.get('targetLang');

        const targetLangMatch = rawLang.match(/\(([^)]+)\)/);
        const targetLang = targetLangMatch ? targetLangMatch[1] : 'EN';

        buttonElement.style.opacity = '0.5';

        if (provider === 'DeepL' && apiKey && apiKey.trim() !== "") {
            doDeepLTranslate(text, buttonElement, targetLang, apiKey);
        } else {
            doGoogleTranslate(text, buttonElement, targetLang);
        }
    }

    function applyTranslation(buttonElement, translatedText, color, engineName) {
        const tweetTextNode = buttonElement.closest('article').querySelector('[data-testid="tweetText"]');
        if (tweetTextNode) {
            tweetTextNode.innerText = translatedText;
            buttonElement.querySelector('svg').style.color = color;
            buttonElement.style.opacity = '1';

            // Find or create the label
            let label = buttonElement.querySelector('.translator-label');
            if (!label) {
                label = document.createElement('span');
                label.className = 'translator-label';
                label.style.cssText = 'font-size: 11px; margin-left: 2px; font-weight: bold; cursor: pointer; text-decoration: underline;';
                label.onclick = (e) => { e.stopPropagation(); GM_config.open(); };
                buttonElement.appendChild(label);
            }

            // Dynamically update text and color based on the engine used
            label.innerText = engineName === 'DeepL' ? 'DeepL' : 'Google';
            label.style.color = color;
        }
    }

    // --- UI Injection ---
    function injectTranslateButton(node) {
        let attempts = 0;
        const tryInject = setInterval(() => {
            if (node.querySelector('.deepl-translate-btn')) { clearInterval(tryInject); return; }
            const caret = node.querySelector('[data-testid="caret"]');
            const container = caret ? caret.closest('div.r-18u37iz') : null;
            if (container) {
                clearInterval(tryInject);
                const btn = document.createElement('div');
                btn.className = 'deepl-translate-btn';
                btn.style.cssText = 'display: inline-flex; align-items: center; justify-content: center; cursor: pointer; min-width: 26px; height: 26px; border-radius: 9999px; margin-right: 4px; flex-shrink: 0;';
                btn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" style="color: rgb(113, 118, 123);"><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"></path></svg>';
                btn.onclick = (e) => {
                    e.preventDefault(); e.stopPropagation();
                    const tweetText = node.querySelector('[data-testid="tweetText"]')?.innerText;
                    if (tweetText) translateText(tweetText, btn);
                };
                container.prepend(btn);
            }
            if (++attempts >= 15) clearInterval(tryInject);
        }, 200);
    }

    const observer = new MutationObserver((mutations) => {
        mutations.forEach(m => m.addedNodes.forEach(node => {
            if (node.nodeType === 1) {
                node.querySelectorAll('article[data-testid="tweet"]').forEach(injectTranslateButton);
                if (node.tagName === 'ARTICLE' && node.getAttribute('data-testid') === 'tweet') injectTranslateButton(node);
            }
        }));
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();