Bitcointalk Userscript Translator

Bitcointalk Translator with modern UI and multi-language support

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();