// ==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);
})();