// ==UserScript==
// @name Twitter External Translator
// @name:zh Twitter外部翻译器
// @name:zh-CN Twitter外部翻译器
// @name:zh-TW Twitter外部翻译器
// @name:nl Twitter Externe Vertaler
// @name:fr Traducteur externe Twitter
// @name:de Externer Twitter-Übersetzer
// @name:it Traduttore esterno di Twitter
// @name:ja ツイッター外部翻訳者
// @name:pl Zewnętrzny tłumacz Twittera
// @name:pt Tradutor externo do Twitter
// @name:ru-RU Twitter Внешний переводчик
// @name:ru Twitter Внешний переводчик
// @name:es Traductor externo de Twitter
// @description Adds 3rd party translators to Twitter
// @description:zh 将第三方翻译添加到推特
// @description:zh-CN 将第三方翻译添加到推特
// @description:zh-TW 將第三方翻譯添加到推特
// @description:nl Voegt vertalers van derden toe aan Twitter
// @description:fr Ajout de traducteurs tiers à Twitter
// @description:de Fügt Drittanbieter-Übersetzer zu Twitter hinzu
// @description:it Aggiunge traduttori di terze parti a Twitter
// @description:pl Dodaje tłumaczy innych firm do Twittera
// @description:pt Adiciona tradutores de terceiros ao Twitter
// @description:ja サードパーティの翻訳者をツイッターに追加
// @description:ru-RU Добавляет сторонних переводчиков в Twitter
// @description:ru Добавляет сторонних переводчиков в Twitter
// @description:es Añade traductores de terceros a Twitter
// @author Magic of Lolis
// @version 0.12
// @namespace https://github.com/magicoflolis/userscriptrepo/tree/master/ExternalTranslator#twitter-external-translator
// @homepageURL https://github.com/magicoflolis/userscriptrepo/tree/master/ExternalTranslator#twitter-external-translator
// @require https://code.jquery.com/jquery-3.6.0.slim.min.js
// @icon https://abs.twimg.com/favicons/twitter.ico
// @include https://twitter.com/*
// @include https://tweetdeck.twitter.com/*
// @grant none
// @run-at document-start
// @downloadURL none
// ==/UserScript==
"use strict";
((log = '[MoL]') => {
//#region Config
/**
* You'll need to edit the config manually for now if you're using this
* as a user script.
*/
let cfg = {
/** Preferred language
* @type {'en'|'zh'|'nl'|'fr'|'de'|'it'|'ja'|'pl'|'pt'|'ru'|'es'} */
lang: ('en'),
/** Preferred translator, lowercase only!
* @type {'deepl'|'yandex'|'bing'|'google'|'mymemory'|'translate'} */
translator: ('deepl'),
/** Preferred display
* @type {'text'|'icon'|'text + icon'} */
display: ('text + icon'),
};
//#endregion
// Web icons are encoded in Data URI.
// Can be decoded: https://www.site24x7.com/tools/datauri-to-image.html
let icons = {
deepl: `
`,
yandex: `
`,
bing: `
`,
google: `
`,
mymemory: `
`,
translate: `
`,
},
txt = {
en: {
sel: `English (en)`,
tw: `Translate with`,
lg: `Language`,
tr: `Translator`,
ds: `Display`,
ti: `Text + Icon`,
t: `Text`,
i: `Icon`,
s: `Save`,
},
zh: {
sel: `中文 (zh)`,
tw: `翻译与`,
lg: `语种`,
tr: `译者`,
ds: `显示`,
ti: `文本+图标`,
t: `案文`,
i: `图标`,
s: `保存`,
},
nl: {
sel: `Nederlands (nl)`,
tw: `Vertaal met`,
lg: `Taal`,
tr: `Vertaler`,
ds: `Weergave`,
ti: `Tekst + Pictogram`,
t: `Tekst`,
i: `Icoon`,
s: `Save`,
},
fr: {
sel: `Français (fr)`,
tw: `Traduire avec`,
lg: `Langue`,
tr: `Traducteur`,
ds: `Afficher`,
ti: `Texte + Icône`,
t: `Texte`,
i: `Icône`,
s: `Sauvez`,
},
de: {
sel: `Deutsch (de)`,
tw: `Übersetzen mit`,
lg: `Sprache`,
tr: `Übersetzer`,
ds: `Anzeige`,
ti: `Text + Symbol`,
t: `Text`,
i: `Icon`,
s: `Speichern`,
},
it: {
sel: `Italiano (it)`,
tw: `Tradurre con`,
lg: `Lingua`,
tr: `Traduttore`,
ds: `Visualizza`,
ti: `Testo + icona`,
t: `Testo`,
i: `Icona`,
s: `Salva`,
},
ja: {
sel: `日本語 (ja)`,
tw: `で翻訳する`,
lg: `言語`,
tr: `翻訳者`,
ds: `ディスプレイ`,
ti: `テキスト+アイコン`,
t: `テキスト`,
i: `アイコン`,
s: `保存`,
},
pl: {
sel: `Polski (pl)`,
tw: `Tłumaczenie za pomocą`,
lg: `Język`,
tr: `Tłumacz`,
ds: `Wyświetlacz`,
ti: `Tekst + Ikona`,
t: `Tekst`,
i: `Ikona`,
s: `Zapisz`,
},
pt: {
sel: `Português (pt)`,
tw: `Traduzir com`,
lg: `Idioma`,
tr: `Tradutora`,
ds: `Mostrar`,
ti: `Texto + Ícone`,
t: `Texto`,
i: `Ícone`,
s: `Guardar`,
},
ru: {
sel: `Russisch (ru)`,
tw: `Перевод с`,
lg: `Язык`,
tr: `Переводчик`,
ds: `Показать`,
ti: `Текст + иконка`,
t: `Текст`,
i: `иконка`,
s: `Сохранить`,
},
es: {
sel: `Español (es)`,
tw: `Traducir con`,
lg: `Idioma`,
tr: `Traductor`,
ds: `Mostrar`,
ti: `Texto + Icono`,
t: `Texto`,
i: `Icono`,
s: `Guardar`,
}
};
function isHTML(str, doc = new DOMParser().parseFromString(str, "text/html")) {
return Array.from(doc.body.childNodes).some(node => node.nodeType === 1);
}
async function injectTranslationButton(content = '',magicBtn,btContainer,btLang,site) {
let translateTweet = $("div[lang]").eq(0).siblings().eq(0).children("span"), // "Translate Tweet" button
translateBio = $('div[data-testid="UserDescription"]').eq(0).siblings().eq(0).children("span"), // "Translate Bio" button
trTweet = $("div[lang]").eq(0).siblings().eq(1), // [Tweet] "Translate with ..." button
trBio = $('div[data-testid="UserDescription"]').eq(0).siblings().eq(1), // [Bio] "Translate with ..." button
name = (cfg.translator == 'yandex') ? `Yandex ${icons.yandex}` : (cfg.translator == 'bing') ? `Bing ${icons.bing}` : (cfg.translator == 'google') ? `Google ${icons.google}` : (cfg.translator == 'mymemory') ? `MyMemory ${icons.mymemory}` : (cfg.translator == 'translate') ? `translate.com ${icons.translate}` : `DeepL ${icons.deepl}`,
nIcons = (cfg.translator == 'yandex') ? icons.yandex : (cfg.translator == 'bing') ? icons.bing : (cfg.translator == 'google') ? icons.google : (cfg.translator == 'mymemory') ? icons.mymemory : (cfg.translator == 'translate') ? icons.translate : icons.deepl,
checkDisplay = (cfg.display == 'text') ? icons = { deepl: '', yandex: '', bing: '', google: '', mymemory: '', translate: '' } : (cfg.display == 'icon') ? name = nIcons : false,
tweetbtn = () => {
btContainer = translateTweet.parent().siblings().eq(0), // "Tweet"
btLang = btContainer.attr("lang");
magicBtn = translateTweet.parent().clone().appendTo(translateTweet.parent().parent());
btContainer.children("span").each((index,item) => {
let tweet = $(item).html().trim();
(tweet && tweet != '' && !isHTML(tweet)) ? content += ` ${tweet}` : false;
});
(!btLang) ? btLang = "auto" : false;
(cfg.lang == 'zh') ? magicBtn.children("span").html(`${txt.zh.tw} ${name}`) : (cfg.lang == 'nl') ? magicBtn.children("span").html(`${txt.nl.tw} ${name}`) : (cfg.lang == 'fr') ? magicBtn.children("span").html(`${txt.fr.tw} ${name}`) : (cfg.lang == 'de') ? magicBtn.children("span").html(`${txt.de.tw} ${name}`) : (cfg.lang == 'it') ? magicBtn.children("span").html(`${txt.it.tw} ${name}`) : (cfg.lang == 'ja') ? magicBtn.children("span").html(`${txt.ja.tw} ${name}`) : (cfg.lang == 'pl') ? magicBtn.children("span").html(`${txt.pl.tw} ${name}`) : (cfg.lang == 'pt') ? magicBtn.children("span").html(`${txt.pt.tw} ${name}`) : (cfg.lang == 'ru') ? magicBtn.children("span").html(`${txt.es.tw} ${name}`) : (cfg.lang == 'es') ? magicBtn.children("span").html(`${txt.es.tw} ${name}`) : (cfg.lang == 'en') ? magicBtn.children("span").html(`${txt.en.tw} ${name}`) : magicBtn.children("span").html(`${txt.en.tw} ${name}`);
site = (cfg.translator == 'yandex') ? `https://translate.yandex.com/?lang=${btLang}-${cfg.lang}&text=${content}` : (cfg.translator == 'bing') ? `https://www.bing.com/translator/?text=${content}&from=${btLang}&to=${cfg.lang}` : (cfg.translator == 'google') ? `https://translate.google.com/?q=${content}&sl=${btLang}&tl=${cfg.lang}` : (cfg.translator == 'mymemory') ? `https://mymemory.translated.net/${cfg.lang}/${btLang}/${cfg.lang}/${content}` : (cfg.translator == 'translate') ? `https://www.translate.com/#${btLang}/${cfg.lang}/${content}` : `https://www.deepl.com/translator#${btLang}/${cfg.lang}/${content}`;
magicBtn.hover(function() {
$(this).css("text-decoration", "underline");
}, function() {
$(this).css("text-decoration", "none");
});
magicBtn.on("click", () => {
window.open(`${site}`,'_blank');
})
},
biobtn = () => {
btContainer = translateBio.parent().siblings().eq(0); // "User Bio"
btLang = $("div[lang]").attr("lang");
magicBtn = translateBio.parent().clone().appendTo(translateBio.parent().parent());
btContainer.children("span").each((index,item) => {
let bio = $(item).html().trim();
(bio && bio != '' && !isHTML(bio)) ? content += ` ${bio}` : false;
});
(!btLang) ? btLang = "auto" : false;
(cfg.lang == 'zh') ? magicBtn.children("span").html(`${txt.zh.tw} ${name}`) : (cfg.lang == 'nl') ? magicBtn.children("span").html(`${txt.nl.tw} ${name}`) : (cfg.lang == 'fr') ? magicBtn.children("span").html(`${txt.fr.tw} ${name}`) : (cfg.lang == 'de') ? magicBtn.children("span").html(`${txt.de.tw} ${name}`) : (cfg.lang == 'it') ? magicBtn.children("span").html(`${txt.it.tw} ${name}`) : (cfg.lang == 'ja') ? magicBtn.children("span").html(`${txt.ja.tw} ${name}`) : (cfg.lang == 'pl') ? magicBtn.children("span").html(`${txt.pl.tw} ${name}`) : (cfg.lang == 'pt') ? magicBtn.children("span").html(`${txt.pt.tw} ${name}`) : (cfg.lang == 'ru') ? magicBtn.children("span").html(`${txt.es.tw} ${name}`) : (cfg.lang == 'es') ? magicBtn.children("span").html(`${txt.es.tw} ${name}`) : (cfg.lang == 'en') ? magicBtn.children("span").html(`${txt.en.tw} ${name}`) : magicBtn.children("span").html(`${txt.en.tw} ${name}`);
magicBtn.hover(function() {
$(this).css("text-decoration", "underline");
}, function() {
$(this).css("text-decoration", "none");
});
site = (cfg.translator == 'yandex') ? `https://translate.yandex.com/?lang=${btLang}-${cfg.lang}&text=${content}` : (cfg.translator == 'bing') ? `https://www.bing.com/translator/?text=${content}&from=${btLang}&to=${cfg.lang}` : (cfg.translator == 'google') ? `https://translate.google.com/?q=${content}&sl=${btLang}&tl=${cfg.lang}` : (cfg.translator == 'mymemory') ? `https://mymemory.translated.net/${cfg.lang}/${btLang}/${cfg.lang}/${content}` : (cfg.translator == 'translate') ? `https://www.translate.com/#${btLang}/${cfg.lang}/${content}` : `https://www.deepl.com/translator#${btLang}/${cfg.lang}/${content}`;
magicBtn.on("click", () => {
window.open(`${site}`,'_blank');
})
};
// Resizes icons
if($('.exIcon').length) {
$('.exIcon').attr('width', '10');
};
const check = (!trBio.length && translateBio.length) ? biobtn() : (!trTweet.length && translateTweet.length) ? tweetbtn() : checkDisplay;
return check
}
async function TweetDeck(magicBtn,btContainer,btLang,site) {
let translateTweet = $('a.js-translate-call-to-action'), // "Translate Tweet" button
trTweet = translateTweet.eq(1), // [Tweet] "Translate with ..." button
name = (cfg.translator == 'yandex') ? `Yandex ${icons.yandex}` : (cfg.translator == 'bing') ? `Bing ${icons.bing}` : (cfg.translator == 'google') ? `Google ${icons.google}` : (cfg.translator == 'mymemory') ? `MyMemory ${icons.mymemory}` : (cfg.translator == 'translate') ? `translate.com ${icons.translate}` : `DeepL ${icons.deepl}`,
nIcons = (cfg.translator == 'yandex') ? icons.yandex : (cfg.translator == 'bing') ? icons.bing : (cfg.translator == 'google') ? icons.google : (cfg.translator == 'mymemory') ? icons.mymemory : (cfg.translator == 'translate') ? icons.translate : icons.deepl,
checkDisplay = (cfg.display == 'text') ? icons = { deepl: '', yandex: '', bing: '', google: '', mymemory: '', translate: '' } : (cfg.display == 'icon') ? name = nIcons : false,
tweetbtn = () => {
checkDisplay
btContainer = translateTweet.siblings().eq(2), // "Tweet"
content = btContainer.text(), // Content of "Tweet"
btLang = btContainer.attr("lang");
magicBtn = translateTweet.before(translateTweet.clone()); // Create external translation button
(!btLang) ? btLang = "auto" : false;
(cfg.lang == 'zh') ? magicBtn.html(`${txt.zh.tw} ${name}`) : (cfg.lang == 'nl') ? magicBtn.html(`${txt.nl.tw} ${name}`) : (cfg.lang == 'fr') ? magicBtn.html(`${txt.fr.tw} ${name}`) : (cfg.lang == 'de') ? magicBtn.html(`${txt.de.tw} ${name}`) : (cfg.lang == 'it') ? magicBtn.html(`${txt.it.tw} ${name}`) : (cfg.lang == 'ja') ? magicBtn.html(`${txt.ja.tw} ${name}`) : (cfg.lang == 'pl') ? magicBtn.html(`${txt.pl.tw} ${name}`) : (cfg.lang == 'pt') ? magicBtn.html(`${txt.pt.tw} ${name}`) : (cfg.lang == 'ru') ? magicBtn.html(`${txt.es.tw} ${name}`) : (cfg.lang == 'es') ? magicBtn.html(`${txt.es.tw} ${name}`) : (cfg.lang == 'en') ? magicBtn.html(`${txt.en.tw} ${name}`) : magicBtn.html(`${txt.en.tw} ${name}`);
site = (cfg.translator == 'yandex') ? `https://translate.yandex.com/?lang=${btLang}-${cfg.lang}&text=${content}` : (cfg.translator == 'bing') ? `https://www.bing.com/translator/?text=${content}&from=${btLang}&to=${cfg.lang}` : (cfg.translator == 'google') ? `https://translate.google.com/?q=${content}&sl=${btLang}&tl=${cfg.lang}` : (cfg.translator == 'mymemory') ? `https://mymemory.translated.net/${cfg.lang}/${btLang}/${cfg.lang}/${content}` : (cfg.translator == 'translate') ? `https://www.translate.com/#${btLang}/${cfg.lang}/${content}` : `https://www.deepl.com/translator#${btLang}/${cfg.lang}/${content}`;
magicBtn.on("click", () => {
window.open(`${site}`,'_blank');
})
};
// Resizes icons
if($('.exIcon').length) {
$('.exIcon').attr('width', '14');
};
return (!trTweet.length && translateTweet.length) ? tweetbtn() : trTweet.attr('style', 'display: flex !important; align-items: end !important;');
}
let callback = (_mutations, observer) => {
observer.disconnect();
(document.location.hostname == 'tweetdeck.twitter.com') ? TweetDeck() : (document.location.hostname == 'twitter.com') ? injectTranslationButton() : console.log(`${log} Achievement: "How Did We Get Here?"`);
observer.observe(target, init);
};
// Its a headache observing single tweet element, inconsistent load times.
const target = document.querySelector("body"),
init = { subtree: true, characterData: true, childList: true };
new MutationObserver(callback).observe(target, init)
})();