Replaces Grok AI translations on X/Twitter with Google Translate. Supports 70+ languages. Adds translate button directly in the feed.
// ==UserScript==
// @name X Translate
// @namespace https://x.com
// @version 1.4
// @description Replaces Grok AI translations on X/Twitter with Google Translate. Supports 70+ languages. Adds translate button directly in the feed.
// @match https://x.com/*
// @match https://twitter.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect translate.googleapis.com
// @license MIT
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
const browserLang = navigator.language.split("-")[0];
function getTargetLang() {
return GM_getValue("targetLang", browserLang);
}
const LANGUAGES = [
["af","Afrikaans"],["sq","Albanian"],["am","Amharic"],["ar","Arabic"],
["hy","Armenian"],["az","Azerbaijani"],["eu","Basque"],["be","Belarusian"],
["bn","Bengali"],["bs","Bosnian"],["bg","Bulgarian"],["my","Burmese"],
["ca","Catalan"],["zh","Chinese"],["hr","Croatian"],["cs","Czech"],
["da","Danish"],["nl","Dutch"],["en","English"],["et","Estonian"],
["fil","Filipino"],["fi","Finnish"],["fr","French"],["fy","Frisian"],
["gl","Galician"],["ka","Georgian"],["de","German"],["el","Greek"],
["gn","Guarani"],["gu","Gujarati"],["he","Hebrew"],["hi","Hindi"],
["hu","Hungarian"],["is","Icelandic"],["id","Indonesian"],["ga","Irish"],
["it","Italian"],["ja","Japanese"],["kn","Kannada"],["kk","Kazakh"],
["km","Khmer"],["ko","Korean"],["ky","Kyrgyz"],["lo","Lao"],
["lv","Latvian"],["ln","Lingala"],["lt","Lithuanian"],["mk","Macedonian"],
["ms","Malay"],["ml","Malayalam"],["mt","Maltese"],["mr","Marathi"],
["mn","Mongolian"],["ne","Nepali"],["no","Norwegian"],["nb","Norwegian Bokmål"],
["fa","Persian"],["pl","Polish"],["pt","Portuguese"],["pa","Punjabi"],
["ro","Romanian"],["ru","Russian"],["sr","Serbian"],["sk","Slovak"],
["sl","Slovenian"],["es","Spanish"],["sw","Swahili"],["sv","Swedish"],
["ta","Tamil"],["te","Telugu"],["th","Thai"],["tr","Turkish"],
["uk","Ukrainian"],["ur","Urdu"],["uz","Uzbek"],["vi","Vietnamese"],
["cy","Welsh"],["zu","Zulu"],
];
function showLanguagePicker() {
document.getElementById("gt-lang-picker")?.remove();
const current = getTargetLang();
const overlay = document.createElement("div");
overlay.id = "gt-lang-picker";
overlay.style.cssText =
"position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:999999;" +
"display:flex;align-items:center;justify-content:center;";
const dialog = document.createElement("div");
dialog.style.cssText =
"background:#fff;border-radius:16px;padding:24px;width:340px;max-height:80vh;" +
"display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,sans-serif;" +
"color:#0f1419;box-shadow:0 8px 28px rgba(0,0,0,0.25);";
const title = document.createElement("div");
title.textContent = "Translation Language";
title.style.cssText = "font-size:18px;font-weight:700;margin-bottom:16px;";
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Search languages...";
search.style.cssText =
"width:100%;padding:10px 12px;border:1px solid #cfd9de;border-radius:8px;" +
"font-size:14px;outline:none;box-sizing:border-box;margin-bottom:12px;";
const list = document.createElement("div");
list.style.cssText = "overflow-y:auto;flex:1;max-height:50vh;";
function renderList(filter = "") {
list.innerHTML = "";
const lf = filter.toLowerCase();
for (const [code, name] of LANGUAGES) {
if (lf && !name.toLowerCase().includes(lf) && !code.includes(lf)) continue;
const item = document.createElement("div");
item.textContent = `${name} (${code})`;
item.style.cssText =
"padding:10px 12px;border-radius:8px;cursor:pointer;font-size:14px;" +
(code === current ? "background:#e8f5fd;font-weight:600;color:#1d9bf0;" : "");
item.addEventListener("mouseenter", () => {
if (code !== current) item.style.background = "#f7f9f9";
});
item.addEventListener("mouseleave", () => {
if (code !== current) item.style.background = "";
});
item.addEventListener("click", () => {
GM_setValue("targetLang", code);
overlay.remove();
location.reload();
});
list.appendChild(item);
}
}
search.addEventListener("input", () => renderList(search.value));
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
dialog.append(title, search, list);
const resetBtn = document.createElement("button");
resetBtn.textContent = `Reset to browser default (${browserLang})`;
resetBtn.style.cssText =
"margin-top:12px;padding:8px;border:1px solid #cfd9de;border-radius:8px;" +
"background:none;cursor:pointer;font-size:13px;color:#536471;width:100%;";
resetBtn.addEventListener("click", () => {
GM_setValue("targetLang", browserLang);
overlay.remove();
location.reload();
});
dialog.appendChild(resetBtn);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
search.focus();
renderList();
}
GM_registerMenuCommand("Set translation language", showLanguagePicker);
function googleTranslate(text) {
return new Promise((resolve, reject) => {
const url =
"https://translate.googleapis.com/translate_a/single?" +
new URLSearchParams({
client: "gtx",
sl: "auto",
tl: getTargetLang(),
dt: "t",
q: text,
});
GM_xmlhttpRequest({
method: "GET",
url,
onload(res) {
try {
const data = JSON.parse(res.responseText);
const translated = data[0].map((s) => s[0]).join("");
const detectedLang = data[2];
resolve({ text: translated, sourceLang: detectedLang });
} catch (e) {
reject(e);
}
},
onerror: reject,
});
});
}
function getTweetText(button) {
let node = button.parentElement;
while (node) {
const textEl = node.querySelector('[data-testid="tweetText"], [data-testid="UserDescription"]');
if (textEl) return { element: textEl, text: textEl.innerText };
node = node.parentElement;
}
return null;
}
const GT_PATH = "M12.87 15.07l-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.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";
function replaceIcon(btn) {
const parent = btn.parentElement;
const svg = parent?.querySelector("svg");
if (!svg) return;
svg.setAttribute("viewBox", "0 0 24 24");
svg.innerHTML = `<path d="${GT_PATH}" fill="currentColor"/>`;
svg.dataset.gtReplaced = "true";
}
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function buildTranslationSource(root) {
const placeholders = [];
let text = "";
const walk = (node) => {
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
text += child.textContent;
} else if (child.nodeType === Node.ELEMENT_NODE) {
const tag = child.tagName;
if (tag === "A" || tag === "IMG") {
const idx = placeholders.length;
placeholders.push(child.outerHTML);
text += `\uE000${idx}\uE001`;
} else if (tag === "BR") {
text += "\n";
} else {
walk(child);
}
}
}
};
walk(root);
return { text, placeholders };
}
async function doTranslate(tweetTextEl, btnSpan) {
if (tweetTextEl.dataset.gtTranslated === "true") {
tweetTextEl.innerHTML = tweetTextEl.dataset.gtOriginalHtml;
tweetTextEl.dataset.gtTranslated = "";
if (btnSpan) btnSpan.textContent = "Show translation";
return;
}
const origBtnText = btnSpan?.textContent;
if (btnSpan) btnSpan.textContent = "Translating...";
try {
const { text: source, placeholders } = buildTranslationSource(tweetTextEl);
const result = await googleTranslate(source);
const html = escapeHtml(result.text)
.replace(/\n/g, "<br>")
.replace(/\uE000\s*(\d+)\s*\uE001/g, (m, i) => placeholders[+i] ?? m);
tweetTextEl.dataset.gtOriginalHtml = tweetTextEl.innerHTML;
tweetTextEl.dataset.gtTranslated = "true";
tweetTextEl.innerHTML = html;
if (btnSpan) btnSpan.textContent = "Hide translation";
} catch (err) {
console.error("Google Translate error:", err);
if (btnSpan) btnSpan.textContent = origBtnText || "Show translation";
}
}
function handleTranslateButton(btn) {
if (btn.dataset.googleTranslate === "patched") return;
btn.dataset.googleTranslate = "patched";
replaceIcon(btn);
btn.addEventListener(
"click",
async (e) => {
e.stopImmediatePropagation();
e.preventDefault();
e.stopPropagation();
const tweet = getTweetText(btn);
if (!tweet) return;
doTranslate(tweet.element, btn.querySelector("span"));
},
true
);
}
function injectFeedButtons() {
const targetLang = getTargetLang();
const tweetTexts = document.querySelectorAll('[data-testid="tweetText"]');
for (const el of tweetTexts) {
if (el.dataset.gtFeedPatched) continue;
el.dataset.gtFeedPatched = "true";
const lang = el.getAttribute("lang");
if (!lang || lang === targetLang || lang === "und") continue;
const article = el.closest("article");
if (!article) continue;
const existingBtn = article.querySelector('[data-google-translate="patched"]');
if (existingBtn) continue;
const btn = document.createElement("div");
btn.style.cssText =
"display:flex;align-items:center;gap:4px;cursor:pointer;padding:4px 0;" +
"color:rgb(29,155,240);font-size:13px;margin-top:4px;width:fit-content;align-self:flex-start;";
btn.innerHTML =
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" ` +
`width="1.1em" height="1.1em"><path d="${GT_PATH}"/></svg>` +
`<span>Show translation</span>`;
btn.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
doTranslate(el, btn.querySelector("span"));
});
el.parentElement.insertBefore(btn, el);
}
}
function findAndPatchButtons() {
const allSpans = document.querySelectorAll("span");
for (const span of allSpans) {
if (span.textContent.trim() === "Show translation" || span.textContent.trim() === "Hide translation") {
const btn = span.closest('[role="button"]') || span.closest("a") || span.parentElement;
if (btn) handleTranslateButton(btn);
}
}
}
function patchAll() {
findAndPatchButtons();
injectFeedButtons();
}
const observer = new MutationObserver(() => patchAll());
observer.observe(document.body, { childList: true, subtree: true });
patchAll();
})();