Translate every post of bitcointalk morethan 20+ languages.
// ==UserScript==
// @name Bitcointalk Translator
// @namespace Royal Cap
// @version 2.0
// @description Translate every post of bitcointalk morethan 20+ languages.
// @match https://bitcointalk.org/index.php?topic=*
// @grant GM_xmlhttpRequest
// @connect translate.googleapis.com
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const langs = {
"en": "English",
"ru": "Русский (Russian)",
"zh-CN": "中文 (Chinese)",
"es": "Español (Spanish)",
"pt": "Português (Portuguese)",
"de": "Deutsch (German)",
"fr": "Français (French)",
"it": "Italiano (Italian)",
"nl": "Nederlands (Dutch)",
"pl": "Polski (Polish)",
"tr": "Türkçe (Turkish)",
"id": "Bahasa Indonesia",
"hi": "हिन्दी (Hindi)",
"bn": "বাংলা (Bengali)",
"vi": "Tiếng Việt (Vietnamese)",
"th": "ไทย (Thai)",
"ja": "日本語 (Japanese)",
"ko": "한국어 (Korean)",
"ar": "العربية (Arabic)",
"fa": "فارسی (Persian)",
"ur": "اردو (Urdu)",
"ro": "Română (Romanian)",
"hu": "Magyar (Hungarian)",
"cs": "Čeština (Czech)",
"sk": "Slovenský (Slovak)",
"el": "Ελληνικά (Greek)",
"bg": "Български (Bulgarian)",
"uk": "Українська (Ukrainian)",
"sr": "Srpski (Serbian)",
"hr": "Hrvatski (Croatian)",
"lt": "Lietuvių (Lithuanian)",
"lv": "Latviešu (Latvian)",
"fil": "Filipino",
"ms": "Malay"
};
const rtlLangs = ["ar", "fa", "ur", "he"];
let savedLang = localStorage.getItem("btc_lang") || "bn";
let cache = JSON.parse(localStorage.getItem("btc_cache") || "{}");
function saveCache() {
localStorage.setItem("btc_cache", JSON.stringify(cache));
}
function translate(text, lang, retry = 1) {
return new Promise(resolve => {
const key = lang + "::" + text;
if (cache[key]) {
resolve(cache[key]);
return;
}
GM_xmlhttpRequest({
method: "GET",
url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${lang}&dt=t&q=${encodeURIComponent(text)}`,
onload: res => {
try {
const data = JSON.parse(res.responseText);
const translated = data[0].map(x => x[0]).join("");
cache[key] = translated;
saveCache();
resolve(translated);
} catch {
if (retry > 0) {
resolve(translate(text, lang, retry - 1));
} else {
resolve(text);
}
}
},
onerror: () => {
if (retry > 0) {
resolve(translate(text, lang, retry - 1));
} else {
resolve(text);
}
}
});
});
}
async function translateAllText(root, lang) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
let nodes = [];
let texts = [];
while (walker.nextNode()) {
let node = walker.currentNode;
let txt = node.nodeValue.trim();
if (txt) {
nodes.push(node);
texts.push(txt);
}
}
const MAX_CHARS = 1800;
let results = [];
let buffer = [];
let length = 0;
async function processChunk(chunk) {
const combined = chunk.join(" ||| ");
const translated = await translate(combined, lang);
return translated.split(" ||| ");
}
for (let txt of texts) {
if (length + txt.length > MAX_CHARS) {
const res = await processChunk(buffer);
results.push(...res);
buffer = [];
length = 0;
}
buffer.push(txt);
length += txt.length;
}
if (buffer.length) {
const res = await processChunk(buffer);
results.push(...res);
}
nodes.forEach((node, i) => {
node.nodeValue = results[i] || node.nodeValue;
});
}
function process() {
const posts = document.querySelectorAll("td.td_headerandpost");
posts.forEach(post => {
if (post.dataset.done) return;
const content = post.querySelector("div.post");
if (!content) return;
// UI
const bar = document.createElement("div");
bar.className = "ts-ignore";
const btn = document.createElement("button");
btn.textContent = "🌐 Translate";
const select = document.createElement("select");
Object.entries(langs).forEach(([c, n]) => {
const o = document.createElement("option");
o.value = c;
o.textContent = n;
if (c === savedLang) o.selected = true;
select.appendChild(o);
});
select.onchange = () => {
savedLang = select.value;
localStorage.setItem("btc_lang", savedLang);
};
bar.append(btn, select);
content.prepend(bar);
let translated = false;
btn.addEventListener("click", async (e) => {
e.preventDefault();
if (!translated) {
btn.textContent = "⏳ Translating...";
const clone = content.cloneNode(true);
clone.querySelectorAll(".ts-ignore").forEach(el => el.remove());
await translateAllText(clone, select.value);
const old = content.querySelector(".ts-box");
if (old) old.remove();
const box = document.createElement("div");
box.className = "ts-box ts-ignore";
// 🌍 RTL support
if (rtlLangs.includes(select.value)) {
box.style.direction = "rtl";
box.style.textAlign = "right";
} else {
box.style.direction = "ltr";
box.style.textAlign = "left";
}
box.style.marginTop = "10px";
box.style.padding = "10px";
box.style.border = "1px solid #ddd";
box.style.background = "#fafafa";
box.style.lineHeight = "1.6";
const header = document.createElement("div");
header.textContent = "🌐 Translated (" + langs[select.value] + ")";
header.style.fontWeight = "bold";
header.style.marginBottom = "6px";
box.appendChild(header);
box.appendChild(clone);
content.appendChild(box);
translated = true;
btn.textContent = "🌐 Hide";
} else {
const box = content.querySelector(".ts-box");
if (box) box.remove();
translated = false;
btn.textContent = "🌐 Translate";
}
});
post.dataset.done = "1";
});
}
window.addEventListener("load", () => {
setTimeout(process, 1200);
});
})();