Translate tweets with DeepL or Google (with visual text indicators)
// ==UserScript==
// @name Twitter Translator
// @namespace twitterDeepLTranslator
// @version 1.0
// @description Translate tweets with DeepL or Google (with visual text indicators)
// @author Runterya
// @match https://x.com/*
// @match https://twitter.com/*
// @license MIT
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// ==/UserScript==
(function() {
'use strict';
// --- Configuration Setup ---
GM_config.init({
'id': 'DeepL_Config',
'title': 'Translator Settings',
'fields': {
'provider': {
'label': 'Default Translator',
'type': 'select',
'options': ['Google', 'DeepL'],
'default': 'Google'
},
'apiKey': {
'label': 'DeepL API Key (<a href="https://www.deepl.com/en/your-account/keys" target="_blank" style="color: #1d9bf0; text-decoration: none;">Get your key here</a>)',
'type': 'text',
'default': ''
},
'targetLang': {
'label': 'Target Language',
'type': 'select',
'options': [
'English (EN)',
'Turkish (TR)',
'German (DE)',
'Spanish (ES)',
'French (FR)',
'Italian (IT)',
'Japanese (JA)',
'Russian (RU)',
'Chinese (ZH)'
],
'default': 'English (EN)'
}
},
'css': `
#DeepL_Config { background-color: #f8f9fa !important; color: #333 !important; padding: 20px !important; height: 380px !important; }
#DeepL_Config .config_header { font-size: 20px !important; margin-bottom: 20px !important; }
#DeepL_Config .field_label { display: block !important; font-weight: bold !important; margin-bottom: 5px !important; color: #000 !important; }
#DeepL_Config select, #DeepL_Config input[type="text"] { display: block !important; width: 100% !important; height: 35px !important; padding: 5px !important; border: 1px solid #ccc !important; background-color: #fff !important; color: #000 !important; box-sizing: border-box !important; }
#DeepL_Config .config_var { margin-bottom: 20px !important; }
`
});
GM_registerMenuCommand('Configure Translator', () => GM_config.open());
// --- Translation Engines ---
function doGoogleTranslate(text, buttonElement, targetLang) {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang.toLowerCase()}&dt=t&q=${encodeURIComponent(text)}`;
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
const translatedText = data[0].map(x => x[0]).join('');
applyTranslation(buttonElement, translatedText, '#1d9bf0', 'Google');
} catch(e) {
alert("Google Translate Error.");
buttonElement.style.opacity = '1';
}
},
onerror: () => {
alert("Google Request Failed.");
buttonElement.style.opacity = '1';
}
});
}
function doDeepLTranslate(text, buttonElement, targetLang, apiKey) {
let deepLLang = targetLang.toUpperCase();
if (deepLLang === 'EN') deepLLang = 'EN-US';
GM_xmlhttpRequest({
method: "POST",
url: "https://api-free.deepl.com/v2/translate",
headers: {
"Authorization": `DeepL-Auth-Key ${apiKey}`,
"Content-Type": "application/x-www-form-urlencoded"
},
data: `text=${encodeURIComponent(text)}&target_lang=${deepLLang}`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.translations) {
applyTranslation(buttonElement, data.translations[0].text, '#00ba7c', 'DeepL');
} else {
console.warn("DeepL API rejected request. Falling back to Google.");
doGoogleTranslate(text, buttonElement, targetLang);
}
} catch(e) {
console.warn("DeepL Parse Error. Falling back to Google.");
doGoogleTranslate(text, buttonElement, targetLang);
}
},
onerror: () => {
console.warn("DeepL Network Error. Falling back to Google.");
doGoogleTranslate(text, buttonElement, targetLang);
}
});
}
// --- Core Logic ---
async function translateText(text, buttonElement) {
const provider = GM_config.get('provider');
const apiKey = GM_config.get('apiKey');
const rawLang = GM_config.get('targetLang');
const targetLangMatch = rawLang.match(/\(([^)]+)\)/);
const targetLang = targetLangMatch ? targetLangMatch[1] : 'EN';
buttonElement.style.opacity = '0.5';
if (provider === 'DeepL' && apiKey && apiKey.trim() !== "") {
doDeepLTranslate(text, buttonElement, targetLang, apiKey);
} else {
doGoogleTranslate(text, buttonElement, targetLang);
}
}
function applyTranslation(buttonElement, translatedText, color, engineName) {
const tweetTextNode = buttonElement.closest('article').querySelector('[data-testid="tweetText"]');
if (tweetTextNode) {
tweetTextNode.innerText = translatedText;
buttonElement.querySelector('svg').style.color = color;
buttonElement.style.opacity = '1';
// Find or create the label
let label = buttonElement.querySelector('.translator-label');
if (!label) {
label = document.createElement('span');
label.className = 'translator-label';
label.style.cssText = 'font-size: 11px; margin-left: 2px; font-weight: bold; cursor: pointer; text-decoration: underline;';
label.onclick = (e) => { e.stopPropagation(); GM_config.open(); };
buttonElement.appendChild(label);
}
// Dynamically update text and color based on the engine used
label.innerText = engineName === 'DeepL' ? 'DeepL' : 'Google';
label.style.color = color;
}
}
// --- UI Injection ---
function injectTranslateButton(node) {
let attempts = 0;
const tryInject = setInterval(() => {
if (node.querySelector('.deepl-translate-btn')) { clearInterval(tryInject); return; }
const caret = node.querySelector('[data-testid="caret"]');
const container = caret ? caret.closest('div.r-18u37iz') : null;
if (container) {
clearInterval(tryInject);
const btn = document.createElement('div');
btn.className = 'deepl-translate-btn';
btn.style.cssText = 'display: inline-flex; align-items: center; justify-content: center; cursor: pointer; min-width: 26px; height: 26px; border-radius: 9999px; margin-right: 4px; flex-shrink: 0;';
btn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" style="color: rgb(113, 118, 123);"><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"></path></svg>';
btn.onclick = (e) => {
e.preventDefault(); e.stopPropagation();
const tweetText = node.querySelector('[data-testid="tweetText"]')?.innerText;
if (tweetText) translateText(tweetText, btn);
};
container.prepend(btn);
}
if (++attempts >= 15) clearInterval(tryInject);
}, 200);
}
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => m.addedNodes.forEach(node => {
if (node.nodeType === 1) {
node.querySelectorAll('article[data-testid="tweet"]').forEach(injectTranslateButton);
if (node.tagName === 'ARTICLE' && node.getAttribute('data-testid') === 'tweet') injectTranslateButton(node);
}
}));
});
observer.observe(document.body, { childList: true, subtree: true });
})();