Raw Source
eretly / DiscordAutoTranslator

// ==UserScript==
// @name         DiscordAutoTranslator
// @namespace    http://tampermonkey.net/
// @version      1.14
// @description  Automatically translates messages in channels/private messages into the selected language in Discord Web. 
// @match        *://discord.com/*
// @author       Timka251 & eretly
// @grant        GM_xmlhttpRequest
// @icon         https://i.pinimg.com/236x/68/95/31/689531dc04ba222ab7af0fa34dc63644.jpg
// @run-at       document-end
// @license      BSD-3-Clause
// @downloadURL https://update.greasyfork.org/scripts/506133/DiscordAutoTranslator.user.js
// @updateURL https://update.greasyfork.org/scripts/506133/DiscordAutoTranslator.meta.js
// ==/UserScript==

/*
 * Copyright 2024 eretly
 * Licensed under the BSD 3-Clause License.
 */

(function () {
    'use strict';

    const languageSelector = document.createElement('div');
    languageSelector.style.position = 'fixed';
    languageSelector.style.bottom = '10px';
    languageSelector.style.right = '10px';
    languageSelector.style.backgroundColor = '#2f3136';
    languageSelector.style.padding = '10px';
    languageSelector.style.zIndex = '1000';
    languageSelector.style.border = '1px solid #ccc';
    languageSelector.style.borderRadius = '5px';
    languageSelector.style.display = 'none';

    const sourceLangSelect = document.createElement('select');
    const targetLangSelect = document.createElement('select');
    const toggleButton = document.createElement('button');
    toggleButton.style.backgroundColor = '#7289da';
    toggleButton.style.color = 'white';
    toggleButton.style.border = 'none';
    toggleButton.style.borderRadius = '5px';
    toggleButton.style.cursor = 'pointer';
    toggleButton.style.marginTop = '5px';
    toggleButton.style.padding = '5px';
    toggleButton.style.display = 'block';

    toggleButton.textContent = 'Enable Translator';

    const languages = {
        'af': 'Afrikaans',
        'sq': 'Albanian',
        'am': 'Amharic',
        'ar': 'Arabic',
        'hy': 'Armenian',
        'as': 'Assamese',
        'ay': 'Aymara',
        'az': 'Azerbaijani',
        'bm': 'Bambara',
        'eu': 'Basque',
        'be': 'Belarusian',
        'bn': 'Bengali',
        'bho': 'Bhojpuri',
        'bs': 'Bosnian',
        'bg': 'Bulgarian',
        'ca': 'Catalan',
        'ceb': 'Cebuano',
        'zh-CN': 'Chinese (Simplified)',
        'zh-TW': 'Chinese (Traditional)',
        'co': 'Corsican',
        'hr': 'Croatian',
        'cs': 'Czech',
        'da': 'Danish',
        'dv': 'Dhivehi',
        'doi': 'Dogri',
        'nl': 'Dutch',
        'en': 'English',
        'eo': 'Esperanto',
        'et': 'Estonian',
        'ee': 'Ewe',
        'fil': 'Filipino (Tagalog)',
        'fi': 'Finnish',
        'fr': 'French',
        'fy': 'Frisian',
        'gl': 'Galician',
        'ka': 'Georgian',
        'de': 'German',
        'el': 'Greek',
        'gn': 'Guarani',
        'gu': 'Gujarati',
        'ht': 'Haitian Creole',
        'ha': 'Hausa',
        'haw': 'Hawaiian',
        'he': 'Hebrew',
        'hi': 'Hindi',
        'hmn': 'Hmong',
        'hu': 'Hungarian',
        'is': 'Icelandic',
        'ig': 'Igbo',
        'ilo': 'Ilocano',
        'id': 'Indonesian',
        'ga': 'Irish',
        'it': 'Italian',
        'ja': 'Japanese',
        'jv': 'Javanese',
        'kn': 'Kannada',
        'kk': 'Kazakh',
        'km': 'Khmer',
        'rw': 'Kinyarwanda',
        'gom': 'Konkani',
        'ko': 'Korean',
        'kri': 'Krio',
        'ku': 'Kurdish',
        'ckb': 'Kurdish (Sorani)',
        'ky': 'Kyrgyz',
        'lo': 'Lao',
        'la': 'Latin',
        'lv': 'Latvian',
        'ln': 'Lingala',
        'lt': 'Lithuanian',
        'lg': 'Luganda',
        'lb': 'Luxembourgish',
        'mk': 'Macedonian',
        'mai': 'Maithili',
        'mg': 'Malagasy',
        'ms': 'Malay',
        'ml': 'Malayalam',
        'mt': 'Maltese',
        'mi': 'Maori',
        'mr': 'Marathi',
        'mni-Mtei': 'Meiteilon (Manipuri)',
        'lus': 'Mizo',
        'mn': 'Mongolian',
        'my': 'Myanmar (Burmese)',
        'ne': 'Nepali',
        'no': 'Norwegian',
        'ny': 'Nyanja (Chichewa)',
        'or': 'Odia (Oriya)',
        'om': 'Oromo',
        'ps': 'Pashto',
        'fa': 'Persian',
        'pl': 'Polish',
        'pt': 'Portuguese (Portugal, Brazil)',
        'pa': 'Punjabi',
        'qu': 'Quechua',
        'ro': 'Romanian',
        'ru': 'Russian',
        'sm': 'Samoan',
        'sa': 'Sanskrit',
        'gd': 'Scots Gaelic',
        'nso': 'Sepedi',
        'sr': 'Serbian',
        'st': 'Sesotho',
        'sn': 'Shona',
        'sd': 'Sindhi',
        'si': 'Sinhala (Sinhalese)',
        'sk': 'Slovak',
        'sl': 'Slovenian',
        'so': 'Somali',
        'es': 'Spanish',
        'su': 'Sundanese',
        'sw': 'Swahili',
        'sv': 'Swedish',
        'tl': 'Tagalog (Filipino)',
        'tg': 'Tajik',
        'ta': 'Tamil',
        'tt': 'Tatar',
        'te': 'Telugu',
        'th': 'Thai',
        'ti': 'Tigrinya',
        'ts': 'Tsonga',
        'tr': 'Turkish',
        'tk': 'Turkmen',
        'ak': 'Twi (Akan)',
        'uk': 'Ukrainian',
        'ur': 'Urdu',
        'ug': 'Uyghur',
        'uz': 'Uzbek',
        'vi': 'Vietnamese',
        'cy': 'Welsh',
        'xh': 'Xhosa',
        'yi': 'Yiddish',
        'yo': 'Yoruba',
        'zu': 'Zulu'
    };

    const sortedLanguages = Object.entries(languages).sort((a, b) => a[1].localeCompare(b[1]));

    sortedLanguages.forEach(([code, name]) => {
        const option1 = document.createElement('option');
        option1.value = code;
        option1.textContent = name;
        sourceLangSelect.appendChild(option1);

        const option2 = document.createElement('option');
        option2.value = code;
        option2.textContent = name;
        targetLangSelect.appendChild(option2);
    });

    const style = document.createElement('style');
    style.textContent = `
        select {
            color: white;
            background-color: #2f3136;
            border: 1px solid #ccc;
            border-radius: 5px;
            padding: 5px;
            margin: 5px 0;
        }
        .source-lang-label {
            color: white;
            margin-left: 5px;
        }
        .target-lang-label {
            color: white;
            margin-left: 5px;
        }
    `;
    document.head.appendChild(style);

    const sourceLangLabel = document.createElement('span');
    sourceLangLabel.classList.add('source-lang-label');
    sourceLangLabel.textContent = 'Source Language';

    const targetLangLabel = document.createElement('span');
    targetLangLabel.classList.add('target-lang-label');
    targetLangLabel.textContent = 'Target Language';

    const sourceLangDiv = document.createElement('div');
    sourceLangDiv.appendChild(sourceLangSelect);
    sourceLangDiv.appendChild(sourceLangLabel);

    const targetLangDiv = document.createElement('div');
    targetLangDiv.appendChild(targetLangSelect);
    targetLangDiv.appendChild(targetLangLabel);

    languageSelector.appendChild(sourceLangDiv);
    languageSelector.appendChild(targetLangDiv);
    languageSelector.appendChild(toggleButton);
    document.body.appendChild(languageSelector);

    const savedSourceLang = localStorage.getItem('sourceLang') || 'en';
    const savedTargetLang = localStorage.getItem('targetLang') || 'ru';
    let isTranslatorActive = localStorage.getItem('isTranslatorActive') === 'true';

    sourceLangSelect.value = savedSourceLang;
    targetLangSelect.value = savedTargetLang;

    let sourceLang = savedSourceLang;
    let targetLang = savedTargetLang;
    let activeRequests = [];

    if (isTranslatorActive) {
        toggleButton.textContent = 'Disable Translator';
        translateAllMessages();
    }

    function updateLanguages() {
        sourceLang = sourceLangSelect.value;
        targetLang = targetLangSelect.value;
        localStorage.setItem('sourceLang', sourceLang);
        localStorage.setItem('targetLang', targetLang);
        if (isTranslatorActive) {
            translateAllMessages();
        }
    }

    sourceLangSelect.addEventListener('change', updateLanguages);
    targetLangSelect.addEventListener('change', updateLanguages);

    function updateTranslatorState() {
        isTranslatorActive = !isTranslatorActive;
        localStorage.setItem('isTranslatorActive', isTranslatorActive);
        toggleButton.textContent = isTranslatorActive ? 'Disable Translator' : 'Enable Translator';

        if (isTranslatorActive) {
            translateAllMessages();
        } else {
            resetTranslations();
            cancelActiveRequests();
        }
    }

    toggleButton.addEventListener('click', updateTranslatorState);

    function translateText(text, callback) {
        const url = `https://translate.google.com/m?hl=${targetLang}&sl=${sourceLang}&tl=${targetLang}&ie=UTF-8&prev=_m&q=${encodeURIComponent(text)}`;

        const request = GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function (response) {
                if (response.status === 200) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");
                    const translatedTextElement = doc.querySelector('.result-container');

                    if (translatedTextElement) {
                        callback(translatedTextElement.textContent.trim());
                    } else {
                        console.error("Translation failed");
                    }
                } else {
                    console.error("Error when receiving transfer, status: " + response.status);
                }
                activeRequests = activeRequests.filter(req => req !== request);
            },
            onerror: function () {
                console.error("Network error during transfer request");
                activeRequests = activeRequests.filter(req => req !== request);
            }
        });

        activeRequests.push(request);
    }

    function cancelActiveRequests() {
        activeRequests.forEach(request => {
            if (request && request.abort) {
                request.abort();
            }
        });
        activeRequests = [];
    }

    function annotateMessage(div) {
        const originalText = div.textContent.trim();
        const container = document.createElement('div');
        container.style.position = 'relative';

        const translatedDiv = document.createElement('div');
        translatedDiv.classList.add('translated-message');
        translatedDiv.style.color = 'rgb(135, 155, 164)';
        translatedDiv.style.marginTop = '0px';
        translatedDiv.style.paddingLeft = '8px';

        translateText(originalText, function (translatedText) {
            translatedDiv.textContent = translatedText;
            container.appendChild(translatedDiv);
            div.parentNode.insertBefore(container, div.nextSibling);
        });
    }

    function checkNewDiv() {
        const divs = document.querySelectorAll('div[id^="message-content-"]');
        divs.forEach(div => {
            if (!div.dataset.processed) {
                const text = div.textContent;
                const lang = detectLanguage(text);

                if (lang === sourceLang && isTranslatorActive) {
                    annotateMessage(div);
                }

                div.dataset.processed = 'true';
            }
        });
    }

    function detectLanguage(text) {
        return text.match(/[a-zA-Z]/) ? 'en' : 'ru';
    }

    function resetTranslations() {
        const translatedMessages = document.querySelectorAll('.translated-message');
        translatedMessages.forEach(msg => msg.remove());
    }

    function translateAllMessages() {
        resetTranslations();
        const divs = document.querySelectorAll('div[id^="message-content-"]');
        divs.forEach(div => {
            const text = div.textContent;
            const lang = detectLanguage(text);

            if (lang === sourceLang && isTranslatorActive) {
                annotateMessage(div);
            }
        });
    }

    document.addEventListener('keydown', (event) => {
        if (event.altKey && (event.key === 't' || event.key === 'е')) { // Alt + T or Alt + Е
            event.preventDefault();
            languageSelector.style.display = languageSelector.style.display === 'none' ? 'block' : 'none';
        }
    });

    setInterval(checkNewDiv, 1000);
})();