Bitcointalk Userscript Translator

Bitcointalk Translator with modern UI and multi-language support

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

})();