Bitcointalk Userscript Translator

Bitcointalk Translator with modern UI and multi-language support

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Bitcointalk Userscript Translator
// @namespace    https://bitcointalk.org/
// @version      3.0.0
// @description  Bitcointalk Translator with modern UI and multi-language support
// @author       Crypto Community
// @match        https://bitcointalk.org/*
// @icon         https://bitcointalk.org/favicon.ico
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      translate.googleapis.com
// @run-at       document-end
// ==/UserScript==

(function () {

    'use strict';

    /* =========================================================
       CONFIG
    ========================================================= */

    const CONFIG = {

        targetLanguage:
            GM_getValue('btc_target_lang', 'en'),

        sideBySide:
            GM_getValue('btc_side_by_side', false),

        autoTranslate:
            GM_getValue('btc_auto_translate', false),

        accent:
            GM_getValue('btc_accent', '#f7931a')

    };

    /* =========================================================
       STYLES
    ========================================================= */

    GM_addStyle(`

        .btc-translate-btn{

            display:inline-flex;
            align-items:center;
            gap:5px;

            padding:4px 10px;
            margin-top:6px;

            border-radius:8px;

            background:rgba(247,147,26,.12);

            border:1px solid rgba(247,147,26,.25);

            color:${CONFIG.accent};

            cursor:pointer;

            font-size:12px;
            font-weight:600;

            transition:.2s ease;
        }

        .btc-translate-btn:hover{

            transform:translateY(-1px);

            background:rgba(247,147,26,.2);
        }

        .btc-translation-box{

            margin-top:12px;

            padding:14px;

            border-radius:12px;

            background:rgba(0,0,0,.18);

            border-left:3px solid ${CONFIG.accent};

            line-height:1.8;

            animation:btcFade .2s ease;

            word-break:break-word;

            overflow-wrap:break-word;
        }

        .btc-head{

            display:flex;

            justify-content:space-between;

            align-items:center;

            margin-bottom:10px;
        }

        .btc-tools{

            display:flex;
            gap:8px;
        }

        .btc-tool{

            border:none;

            padding:4px 8px;

            border-radius:6px;

            cursor:pointer;

            background:rgba(255,255,255,.08);

            color:#fff;

            font-size:12px;
        }

        .btc-panel{

            position:fixed;

            right:20px;
            bottom:20px;

            width:260px;

            z-index:999999;

            background:#111;

            color:#fff;

            padding:16px;

            border-radius:16px;

            box-shadow:0 10px 30px rgba(0,0,0,.45);

            font-family:Arial,sans-serif;
        }

        .btc-panel-title{

            font-size:15px;

            font-weight:bold;

            color:${CONFIG.accent};

            margin-bottom:14px;
        }

        .btc-panel select{

            width:100%;

            margin-bottom:12px;

            padding:8px;

            border-radius:8px;

            border:none;

            background:#222;

            color:#fff;

            font-size:14px;
        }

        .btc-thread-btn{

            width:100%;

            border:none;

            padding:10px;

            border-radius:10px;

            background:${CONFIG.accent};

            color:#fff;

            cursor:pointer;

            font-weight:bold;
        }

        .btc-arabic{

            direction:rtl;

            text-align:right;

            font-family:
                "Tahoma",
                "Arial",
                sans-serif;

            line-height:2;
        }

        @keyframes btcFade{

            from{

                opacity:0;

                transform:translateY(8px);
            }

            to{

                opacity:1;

                transform:translateY(0);
            }
        }

        @media(max-width:768px){

            .btc-panel{

                width:calc(100vw - 30px);

                left:15px;

                right:15px;

                bottom:15px;
            }
        }

    `);

    /* =========================================================
       CACHE
    ========================================================= */

    class TranslationCache {

        constructor(){

            this.prefix = 'btc_translation_';
        }

        get(key){

            return localStorage.getItem(
                this.prefix + key
            );
        }

        set(key, value){

            localStorage.setItem(
                this.prefix + key,
                value
            );
        }

        hash(str){

            let hash = 0;

            for(let i = 0; i < str.length; i++){

                hash = ((hash << 5) - hash)
                    + str.charCodeAt(i);

                hash |= 0;
            }

            return hash;
        }
    }

    const cache = new TranslationCache();

    /* =========================================================
       ESCAPE HTML
    ========================================================= */

    function escapeHTML(str){

        return str
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    /* =========================================================
       TRANSLATION ENGINE
    ========================================================= */

    async function translateText(text, target = 'en') {

        if(!text || text.length < 2){

            return text;
        }

        const cacheKey =
            cache.hash(text + target);

        const cached =
            cache.get(cacheKey);

        if(cached){

            return cached;
        }

        try{

            const url =
                `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;

            const response =
                await fetch(url);

            const data =
                await response.json();

            let translated = '';

            data[0].forEach(item => {

                translated += item[0];
            });

            if(!translated){

                translated = text;
            }

            cache.set(cacheKey, translated);

            return translated;

        }catch(error){

            console.error(
                'Translation Error:',
                error
            );

            return text;
        }
    }

    /* =========================================================
       TRANSLATION BOX
    ========================================================= */

    function createTranslationBox(original, translated){

        const isArabic =
            CONFIG.targetLanguage === 'ar';

        const box =
            document.createElement('div');

        box.className =
            'btc-translation-box';

        if(isArabic){

            box.classList.add('btc-arabic');
        }

        box.innerHTML = `

            <div class="btc-head">

                <strong>
                    🌐 Translation
                </strong>

                <div class="btc-tools">

                    <button class="btc-tool btc-copy">
                        Copy
                    </button>

                    <button class="btc-tool btc-speak">
                        🔊
                    </button>

                    <button class="btc-tool btc-close">
                        ✖
                    </button>

                </div>

            </div>

            <div class="btc-content">

                ${
                    CONFIG.sideBySide

                    ?

                    `

                    <div style="margin-bottom:14px;">

                        <b>Original:</b>

                        <div style="margin-top:6px;">
                            ${escapeHTML(original)}
                        </div>

                    </div>

                    <div>

                        <b>Translated:</b>

                        <div style="margin-top:6px;">
                            ${escapeHTML(translated)}
                        </div>

                    </div>

                    `

                    :

                    escapeHTML(translated)
                }

            </div>

        `;

        /* COPY */

        box.querySelector('.btc-copy')
            .addEventListener('click', () => {

                navigator.clipboard.writeText(
                    translated
                );
            });

        /* SPEECH */

        box.querySelector('.btc-speak')
            .addEventListener('click', () => {

                const speech =
                    new SpeechSynthesisUtterance(
                        translated
                    );

                speech.lang =
                    CONFIG.targetLanguage;

                speechSynthesis.speak(speech);
            });

        /* CLOSE */

        box.querySelector('.btc-close')
            .addEventListener('click', () => {

                box.remove();
            });

        return box;
    }

    /* =========================================================
       PROCESS POSTS
    ========================================================= */

    async function processPost(post){

        if(post.dataset.btcReady){

            return;
        }

        post.dataset.btcReady = 'true';

        const footer =
            post.querySelector('.smalltext');

        const content =
            post.querySelector('.post');

        if(!footer || !content){

            return;
        }

        const button =
            document.createElement('div');

        button.className =
            'btc-translate-btn';

        button.innerHTML =
            '🌐 Translate';

        footer.appendChild(button);

        button.addEventListener(
            'click',
            async () => {

                const old =
                    post.querySelector(
                        '.btc-translation-box'
                    );

                if(old){

                    old.remove();

                    return;
                }

                const text =
                    content.innerText.trim();

                if(!text){

                    return;
                }

                button.innerHTML =
                    '⏳ Translating...';

                const translated =
                    await translateText(
                        text,
                        CONFIG.targetLanguage
                    );

                const box =
                    createTranslationBox(
                        text,
                        translated
                    );

                content.appendChild(box);

                button.innerHTML =
                    '✅ Done';

                setTimeout(() => {

                    button.innerHTML =
                        '🌐 Translate';

                }, 1500);

            }
        );

        if(CONFIG.autoTranslate){

            button.click();
        }
    }

    /* =========================================================
       OBSERVER
    ========================================================= */

    function observeForum(){

        const observer =
            new MutationObserver(() => {

                const posts =
                    document.querySelectorAll(
                        'td.windowbg, td.windowbg2'
                    );

                posts.forEach(
                    processPost
                );

            });

        observer.observe(
            document.body,
            {
                childList:true,
                subtree:true
            }
        );

        const initialPosts =
            document.querySelectorAll(
                'td.windowbg, td.windowbg2'
            );

        initialPosts.forEach(
            processPost
        );
    }

    /* =========================================================
       SETTINGS PANEL
    ========================================================= */

    function createPanel(){

        const panel =
            document.createElement('div');

        panel.className =
            'btc-panel';

        panel.innerHTML = `

            <div class="btc-panel-title">

                Bitcointalk Translator

            </div>

            <label>
                Translation Language
            </label>

            <select id="btc-lang">

                <option value="ar">العربية (Arabic)</option>

                <option value="id">Indonesian</option>

                <option value="es">Spanish</option>

                <option value="zh-CN">Chinese</option>

                <option value="hr">Croatian</option>

                <option value="de">German</option>

                <option value="el">Greek</option>

                <option value="he">Hebrew</option>

                <option value="fr">Français</option>

                <option value="hi">India</option>

                <option value="it">Italian</option>

                <option value="ja">Japanese</option>

                <option value="nl">Nederlands</option>

                <option value="yo">Nigeria</option>

                <option value="ko">Korean</option>

                <option value="tl">Pilipinas</option>

                <option value="pt">Portuguese</option>

                <option value="ru">Russian</option>

                <option value="ro">Romanian</option>

                <option value="sv">Skandinavisk</option>

                <option value="tr">Turkish</option>

                <option value="ur">Pakistan</option>

                <option value="bn">বাংলা</option>

            </select>

            <label style="display:block;margin-bottom:10px;">

                <input
                    type="checkbox"
                    id="btc-side"
                    ${CONFIG.sideBySide ? 'checked' : ''}
                >

                Side-by-Side Mode

            </label>

            <label style="display:block;margin-bottom:14px;">

                <input
                    type="checkbox"
                    id="btc-auto"
                    ${CONFIG.autoTranslate ? 'checked' : ''}
                >

                Auto Translate

            </label>

            <button class="btc-thread-btn">

                Translate Entire Thread

            </button>

        `;

        document.body.appendChild(panel);

        const lang =
            panel.querySelector('#btc-lang');

        lang.value =
            CONFIG.targetLanguage;

        lang.addEventListener(
            'change',
            e => {

                CONFIG.targetLanguage =
                    e.target.value;

                GM_setValue(
                    'btc_target_lang',
                    e.target.value
                );
            }
        );

        panel.querySelector('#btc-side')
            .addEventListener(
                'change',
                e => {

                    CONFIG.sideBySide =
                        e.target.checked;

                    GM_setValue(
                        'btc_side_by_side',
                        e.target.checked
                    );
                }
            );

        panel.querySelector('#btc-auto')
            .addEventListener(
                'change',
                e => {

                    CONFIG.autoTranslate =
                        e.target.checked;

                    GM_setValue(
                        'btc_auto_translate',
                        e.target.checked
                    );
                }
            );

        panel.querySelector('.btc-thread-btn')
            .addEventListener(
                'click',
                async () => {

                    const buttons =
                        document.querySelectorAll(
                            '.btc-translate-btn'
                        );

                    for(const btn of buttons){

                        btn.click();

                        await sleep(400);
                    }
                }
            );
    }

    /* =========================================================
       UTILITIES
    ========================================================= */

    function sleep(ms){

        return new Promise(resolve => {

            setTimeout(resolve, ms);

        });
    }

    /* =========================================================
       SHORTCUTS
    ========================================================= */

    function shortcuts(){

        document.addEventListener(
            'keydown',
            e => {

                if(
                    e.altKey
                    &&
                    e.key === 't'
                ){

                    const first =
                        document.querySelector(
                            '.btc-translate-btn'
                        );

                    if(first){

                        first.click();
                    }
                }
            }
        );
    }

    /* =========================================================
       INIT
    ========================================================= */

    function init(){

        console.log(
            'Bitcointalk Translator Ultimate Loaded'
        );

        observeForum();

        createPanel();

        shortcuts();
    }

    init();

})();