Greasy Fork is available in English.

Bitcointalk Translator

Translate every post of bitcointalk morethan 20+ languages.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);
    });

})();