Greasy Fork is available in English.

DeepL Twitter translation

Add "Translate tweet with DeepL" button

  1. // ==UserScript==
  2. // @name DeepL Twitter translation
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Add "Translate tweet with DeepL" button
  6. // @author Remonade
  7. // @match https://twitter.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @require https://code.jquery.com/jquery-3.6.3.min.js
  13. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  15. // ==/UserScript==
  16.  
  17. /* globals jQuery, $, GM_config */
  18.  
  19. (() => {
  20. 'use strict';
  21.  
  22. var availableLanguages = ["Bulgarian / BG",
  23. "Czech / CS",
  24. "Danish / DA",
  25. "German / DE",
  26. "Greek / EL",
  27. "English (British) / EN-GB",
  28. "English (American) / EN-US",
  29. "Spanish / ES",
  30. "Estonian / ET",
  31. "Finnish / FI",
  32. "French / FR",
  33. "Hungarian / HU",
  34. "Indonesian / ID",
  35. "Italian / IT",
  36. "Japanese / JA",
  37. "Lithuanian / LT",
  38. "Latvian / LV",
  39. "Dutch / NL",
  40. "Polish / PL",
  41. "Portuguese (Brazilian) / PT-BR",
  42. "Portuguese (European) / PT-PT",
  43. "Romanian / RO",
  44. "Russian / RU",
  45. "Slovak / SK",
  46. "Slovenian / SL",
  47. "Swedish / SV",
  48. "Turkish / TR",
  49. "Ukrainian / UK",
  50. "Chinese (simplified) / ZH" ];
  51. availableLanguages.sort();
  52.  
  53. GM_config.init({
  54. "id": "TranslateDeeplSettings",
  55. "title": "Translate with DeepL settings",
  56. "fields":
  57. {
  58. "TargetLang":
  59. {
  60. "label": "Target language",
  61. "section": ["Translation settings"],
  62. "type": "select",
  63. "options": availableLanguages,
  64. "default": "English (American) / EN-US"
  65. },
  66. "DeeplApiKey":
  67. {
  68. "label": "DeepL API key",
  69. "type": "text",
  70. "default": ""
  71. },
  72. "TranslateHashtags":
  73. {
  74. "label": "Translate hashtags",
  75. "type": "checkbox",
  76. "default": true
  77. }
  78. }
  79. });
  80.  
  81. GM_registerMenuCommand("Settings", () => {
  82. GM_config.open();
  83. });
  84.  
  85. function isHTML(str) {
  86. let doc = new DOMParser().parseFromString(str, "text/html");
  87. return Array.from(doc.body.childNodes).some(node => node.nodeType === 1);
  88. }
  89.  
  90. function injectDeeplTranslationButton(tweetTextContainer) {
  91. var translateButtonContainer = $(tweetTextContainer).siblings()[0];
  92.  
  93. if(translateButtonContainer != undefined) {
  94. let tweetLang = tweetTextContainer.attr("lang"),
  95. tweetContent = "",
  96. deeplButtonContainer = $(translateButtonContainer).clone().appendTo($(translateButtonContainer).parent());
  97.  
  98. tweetTextContainer.children().each((index,item) => {
  99. if(item.nodeName === "SPAN") {
  100. var tweetPart = $(item).html().trim();
  101. var isHtml = isHTML(tweetPart);
  102. if(tweetPart && tweetPart != "" && !isHtml) {
  103. tweetContent += " " + tweetPart;
  104. }
  105. else if(isHtml) {
  106. var itemChild = $(item).children().get(0);
  107.  
  108. // HASHTAG
  109. if(GM_config.get("TranslateHashtags") && itemChild.nodeName == "A" && $(itemChild).attr("href").includes("hashtag")) {
  110. tweetPart = $(itemChild).html().trim();
  111. isHtml = isHTML(tweetPart);
  112. if(tweetPart && tweetPart != "" && !isHtml) {
  113. tweetContent += "\n" + tweetPart.replace("#", "%23");
  114. }
  115. }
  116. }
  117. }
  118. else if(item.nodeName == "IMG") {
  119. if($(item).attr("alt") !== undefined) {
  120. tweetContent += " " + $(item).attr("alt");
  121. }
  122. }
  123. });
  124.  
  125. deeplButtonContainer.children("span").html("Translate Tweet with DeepL");
  126. deeplButtonContainer.hover(function() {
  127. $(this).css("text-decoration", "underline");
  128. }, function() {
  129. $(this).css("text-decoration", "none");
  130. });
  131.  
  132. deeplButtonContainer.on("click", () => {
  133. var TargetLangCode = GM_config.get("TargetLang").split('/')[1].trim();
  134.  
  135. if(GM_config.get("DeeplApiKey") !== "") {
  136. var translationContainer = $("#tweetDeeplTranslation")[0];
  137. if(translationContainer === undefined) {
  138. GM_xmlhttpRequest({
  139. method: "POST",
  140. url: GM_config.get("DeeplApiKey").endsWith(":fx") ? "https://api-free.deepl.com/v2/translate" : "https://api.deepl.com/v2/translate",
  141. headers: {
  142. "Authorization": "DeepL-Auth-Key " + GM_config.get("DeeplApiKey"),
  143. "Content-Type": "application/x-www-form-urlencoded"
  144. },
  145. data: "text=" + tweetContent + "&target_lang=" + TargetLangCode,
  146. onload: (response) => {
  147. if(response.responseText !== undefined) {
  148. var result = JSON.parse(response.responseText);
  149. if(result.translations.length > 0) {
  150. var translation = result.translations[0].text;
  151. translateButtonContainer = $(tweetTextContainer).siblings()[0];
  152. translationContainer = $(tweetTextContainer).clone().appendTo($(translateButtonContainer).parent());
  153. translationContainer.removeAttr("lang");
  154. translationContainer.removeAttr("data-testid");
  155. translationContainer.attr("id", "tweetDeeplTranslation");
  156. translationContainer.html(translation);
  157. $("span", deeplButtonContainer).html("Translated by DeepL");
  158. var deeplButtonContainerTmp = deeplButtonContainer;
  159. deeplButtonContainer = deeplButtonContainer.clone(true, true).appendTo($(translateButtonContainer).parent());
  160. deeplButtonContainerTmp.remove();
  161. }
  162. else {
  163. alert("No translation return by DeepL API");
  164. }
  165. }
  166. else {
  167. alert("Error during call to DeepL API");
  168. }
  169. },
  170. onerror: (response) => {
  171. alert("Error during call to DeepL API");
  172. console.error("Error during call to DeepL API", response);
  173. }
  174. });
  175. }
  176. else {
  177. translationContainer.remove();
  178. $("span", deeplButtonContainer).html("Translate Tweet with DeepL");
  179. }
  180. }
  181. else {
  182. tweetContent = tweetContent.replaceAll("/", "\\/").replace(/(?:\r\n|\r|\n)/g, '%0D').trim();
  183. window.open(`https://www.deepl.com/translator#${tweetLang}/${TargetLangCode}/${tweetContent}`,'_blank');
  184. }
  185. });
  186. }
  187. }
  188.  
  189. function addObserverIfHeadNodeAvailable() {
  190. const target = $("head > title")[0],
  191. MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
  192. observer = new MutationObserver((mutations) => {
  193. var tweetTexts = [];
  194. mutations.forEach((mutation) => {
  195. var tweetTextContainer = $("div[data-testid='tweetText']", mutation.addedNodes)[0];
  196. if(tweetTextContainer !== undefined && !tweetTexts.includes(tweetTextContainer)) {
  197. tweetTexts.push(tweetTextContainer);
  198. }
  199. });
  200.  
  201. tweetTexts.forEach((tweetTextContainer) => {
  202. injectDeeplTranslationButton($(tweetTextContainer));
  203. });
  204. });
  205. if(!target) {
  206. return;
  207. }
  208. clearInterval(waitForHeadNodeInterval);
  209. observer.observe($("body")[0], { subtree: true, characterData: true, childList: true });
  210. }
  211.  
  212. let waitForHeadNodeInterval = setInterval(addObserverIfHeadNodeAvailable, 100);
  213. })();