DeepL Twitter translation

Add "Translate tweet with DeepL" button

// ==UserScript==
// @name         DeepL Twitter translation
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Add "Translate tweet with DeepL" button
// @author       Remonade
// @match        https://twitter.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @require https://code.jquery.com/jquery-3.6.3.min.js
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// ==/UserScript==

/* globals jQuery, $, GM_config */

(() => {
    'use strict';

    var availableLanguages = ["Bulgarian / BG",
                            "Czech / CS",
                            "Danish / DA",
                            "German / DE",
                            "Greek / EL",
                            "English (British) / EN-GB",
                            "English (American) / EN-US",
                            "Spanish / ES",
                            "Estonian / ET",
                            "Finnish / FI",
                            "French / FR",
                            "Hungarian / HU",
                            "Indonesian / ID",
                            "Italian / IT",
                            "Japanese / JA",
                            "Lithuanian / LT",
                            "Latvian / LV",
                            "Dutch / NL",
                            "Polish / PL",
                            "Portuguese (Brazilian) / PT-BR",
                            "Portuguese (European) / PT-PT",
                            "Romanian / RO",
                            "Russian / RU",
                            "Slovak / SK",
                            "Slovenian / SL",
                            "Swedish / SV",
                            "Turkish / TR",
                            "Ukrainian / UK",
                            "Chinese (simplified) / ZH" ];
    availableLanguages.sort();

    GM_config.init({
        "id": "TranslateDeeplSettings",
        "title": "Translate with DeepL settings",
        "fields":
        {
            "TargetLang":
            {
                "label": "Target language",
                "section": ["Translation settings"],
                "type": "select",
                "options": availableLanguages,
                "default": "English (American) / EN-US"
            },
            "DeeplApiKey":
            {
                "label": "DeepL API key",
                "type": "text",
                "default": ""
            },
            "TranslateHashtags":
            {
                "label": "Translate hashtags",
                "type": "checkbox",
                "default": true
            }
        }
    });

    GM_registerMenuCommand("Settings", () => {
        GM_config.open();
    });

    function isHTML(str) {
        let doc = new DOMParser().parseFromString(str, "text/html");
        return Array.from(doc.body.childNodes).some(node => node.nodeType === 1);
    }

    function injectDeeplTranslationButton(tweetTextContainer) {
        var translateButtonContainer = $(tweetTextContainer).siblings()[0];

        if(translateButtonContainer != undefined) {
            let tweetLang = tweetTextContainer.attr("lang"),
                tweetContent = "",
                deeplButtonContainer = $(translateButtonContainer).clone().appendTo($(translateButtonContainer).parent());

            tweetTextContainer.children().each((index,item) => {
                if(item.nodeName === "SPAN") {
                    var tweetPart = $(item).html().trim();
                    var isHtml = isHTML(tweetPart);
                    if(tweetPart && tweetPart != "" && !isHtml) {
                        tweetContent += " " + tweetPart;
                    }
                    else if(isHtml) {
                        var itemChild = $(item).children().get(0);

                        // HASHTAG
                        if(GM_config.get("TranslateHashtags") && itemChild.nodeName == "A" && $(itemChild).attr("href").includes("hashtag")) {
                            tweetPart = $(itemChild).html().trim();
                            isHtml = isHTML(tweetPart);
                            if(tweetPart && tweetPart != "" && !isHtml) {
                                tweetContent += "\n" + tweetPart.replace("#", "%23");
                            }
                        }
                    }
                }
                else if(item.nodeName == "IMG") {
                    if($(item).attr("alt") !== undefined) {
                        tweetContent += " " + $(item).attr("alt");
                    }
                }
            });

            deeplButtonContainer.children("span").html("Translate Tweet with DeepL");
            deeplButtonContainer.hover(function() {
                $(this).css("text-decoration", "underline");
            }, function() {
                $(this).css("text-decoration", "none");
            });

            deeplButtonContainer.on("click", () => {
                var TargetLangCode = GM_config.get("TargetLang").split('/')[1].trim();

                if(GM_config.get("DeeplApiKey") !== "") {
                    var translationContainer = $("#tweetDeeplTranslation")[0];
                    if(translationContainer === undefined) {
                        GM_xmlhttpRequest({
                            method: "POST",
                            url: GM_config.get("DeeplApiKey").endsWith(":fx") ? "https://api-free.deepl.com/v2/translate" : "https://api.deepl.com/v2/translate",
                            headers: {
                                "Authorization": "DeepL-Auth-Key " + GM_config.get("DeeplApiKey"),
                                "Content-Type": "application/x-www-form-urlencoded"
                            },
                            data: "text=" + tweetContent + "&target_lang=" + TargetLangCode,
                            onload: (response) => {
                                if(response.responseText !== undefined) {
                                    var result = JSON.parse(response.responseText);
                                    if(result.translations.length > 0) {
                                        var translation = result.translations[0].text;
                                        translateButtonContainer = $(tweetTextContainer).siblings()[0];
                                        translationContainer = $(tweetTextContainer).clone().appendTo($(translateButtonContainer).parent());
                                        translationContainer.removeAttr("lang");
                                        translationContainer.removeAttr("data-testid");
                                        translationContainer.attr("id", "tweetDeeplTranslation");
                                        translationContainer.html(translation);
                                        $("span", deeplButtonContainer).html("Translated by DeepL");
                                        var deeplButtonContainerTmp = deeplButtonContainer;
                                        deeplButtonContainer = deeplButtonContainer.clone(true, true).appendTo($(translateButtonContainer).parent());
                                        deeplButtonContainerTmp.remove();
                                    }
                                    else {
                                        alert("No translation return by DeepL API");
                                    }
                                }
                                else {
                                    alert("Error during call to DeepL API");
                                }
                            },
                            onerror: (response) => {
                                alert("Error during call to DeepL API");
                                console.error("Error during call to DeepL API", response);
                            }
                        });
                    }
                    else {
                        translationContainer.remove();
                        $("span", deeplButtonContainer).html("Translate Tweet with DeepL");
                    }
                }
                else {
                    tweetContent = tweetContent.replaceAll("/", "\\/").replace(/(?:\r\n|\r|\n)/g, '%0D').trim();
                    window.open(`https://www.deepl.com/translator#${tweetLang}/${TargetLangCode}/${tweetContent}`,'_blank');
                }
            });
        }
    }

    function addObserverIfHeadNodeAvailable() {
        const target = $("head > title")[0],
        MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
        observer = new MutationObserver((mutations) => {
            var tweetTexts = [];
            mutations.forEach((mutation) => {
                var tweetTextContainer = $("div[data-testid='tweetText']", mutation.addedNodes)[0];
                if(tweetTextContainer !== undefined && !tweetTexts.includes(tweetTextContainer)) {
                    tweetTexts.push(tweetTextContainer);
                }
            });

            tweetTexts.forEach((tweetTextContainer) => {
                injectDeeplTranslationButton($(tweetTextContainer));
            });
        });
        if(!target) {
            return;
        }
        clearInterval(waitForHeadNodeInterval);
        observer.observe($("body")[0], { subtree: true, characterData: true, childList: true });
    }

    let waitForHeadNodeInterval = setInterval(addObserverIfHeadNodeAvailable, 100);
})();