// ==UserScript== // @name Translate Japanese Tweets // @namespace http://tampermonkey.net/ // @version 2024-05-23 // @description Translates Japanese tweets automatically // @author You // @match https://x.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=x.com // @grant GM.xmlHttpRequest // @connect translate.googleapis.com // @license MIT // @downloadURL none // ==/UserScript== const translationQueue = []; const translatedElementSet = new Set(); const GOOGLE_API_REQUEST_INTERVAL = 300; const DOM_QUERY_INTERVAL = 500; function containsJapaneseCharacters(text) { const regex = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]/; return text.match(regex) != null; } function translateTextGoogleTranslate(text, callback) { // Who doesn't love themselves a free API with no keys or nothing GM.xmlHttpRequest({ method: "GET", url: `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=ja&tl=en&q=${encodeURI(text)}`, headers: { "User-Agent": "Mozilla/5.0", "Accept": "text/xml" }, onload: function(response) { callback(JSON.parse(response.responseText)); } }); } function translateElement(el) { // Twitter clamps tweets longer than 10 lines, which we can easily exceed // with translated tweets. So, bypass it el.style["-webkit-line-clamp"] = 1000; // Get the text from Tweet let text = ""; el.querySelectorAll("span:not([aria-hidden='true']), img, a[role='link']").forEach(x => { if (x.tagName === "IMG") { text += x.getAttribute("alt"); } else { text += x.textContent.trim().replace(/(?:\r\n|\r|\n)/g, "\\n"); } }); // Double check! if (!containsJapaneseCharacters(text)) { return; } // Send to translation queue translationQueue.push({ text: text, callback: res => { if (res === null || res[0] === null) { console.error("Whoops, just got ratelimited!"); return; } let translation = ""; // res[0] has items containing the translated lines res[0].forEach(x => translation += x[0]); // Split by new lines, or the lines that we purposefully added let translationSplit = translation.split(/(\\n|\n)+/); translation = ""; translationSplit.forEach(x => { x = x.trim(); if (x === "\\n" || x === "") { return; } translation += `${x}
` }); el.innerHTML += `
${translation}
`; } }); } function tweetTranslateLoop() { const spanList = [...document.querySelectorAll("[data-testid='tweetText'][lang='ja']")].reverse(); for (const span of spanList) { if (translatedElementSet.has(span)) { continue; } translatedElementSet.add(span); translateElement(span); } } function tweetTranslateAPISendLoop() { const item = translationQueue.pop(0); if (!item) { return; } translateTextGoogleTranslate(item.text, item.callback); } setInterval(tweetTranslateLoop, DOM_QUERY_INTERVAL); // Need this thing to avoid Google's ratelimiting setInterval(tweetTranslateAPISendLoop, GOOGLE_API_REQUEST_INTERVAL);