Bitcointalk Userscript Translator

Bitcointalk Translator with modern UI and multi-language support

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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

})();