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 r###lt = JSON.parse(response.responseText); if(r###lt.translations.length > 0) { var translation = r###lt.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); })();