Bitcointalk Translator

Bitcointalk translator with draggable modern panel

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bitcointalk Translator
// @namespace    https://bitcointalk.org/
// @version      4.0.0
// @description  Bitcointalk translator with draggable modern panel
// @author       GhostOfBitcoin
// @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 = {

        lang:
            GM_getValue('btc_lang', 'bn'),

        side:
            GM_getValue('btc_side', false),

        auto:
            GM_getValue('btc_auto', false),

        panelOpen:
            false
    };

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

    GM_addStyle(`

        :root{

            --btc-main:#5b8cff;
            --btc-second:#7f5cff;
            --btc-dark:#10131a;
            --btc-dark2:#171b24;
            --btc-text:#ffffff;
        }

        /* =====================================================
           FLOAT BUTTON
        ===================================================== */

        .btc-floating-btn{

            position:fixed;

            width:52px;
            height:52px;

            border-radius:50%;

            bottom:20px;
            right:20px;

            z-index:999999;

            background:
                linear-gradient(
                    135deg,
                    var(--btc-main),
                    var(--btc-second)
                );

            display:flex;
            align-items:center;
            justify-content:center;

            cursor:grab;

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

            user-select:none;

            transition:
                transform .15s ease,
                box-shadow .2s ease;
        }

        .btc-floating-btn:hover{

            transform:scale(1.06);
        }

        .btc-floating-btn svg{

            width:24px;
            height:24px;

            fill:white;
        }

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

        .btc-panel{

            position:fixed;

            width:290px;

            background:
                linear-gradient(
                    180deg,
                    rgba(20,24,35,.98),
                    rgba(15,18,28,.98)
                );

            backdrop-filter:blur(12px);

            color:white;

            border-radius:18px;

            padding:18px;

            z-index:999998;

            box-shadow:
                0 20px 50px rgba(0,0,0,.45);

            border:
                1px solid rgba(255,255,255,.06);

            display:none;

            animation:btcPanel .18s ease;
        }

        .btc-panel.active{

            display:block;
        }

        @keyframes btcPanel{

            from{

                opacity:0;
                transform:translateY(8px);
            }

            to{

                opacity:1;
                transform:translateY(0);
            }
        }

        .btc-title{

            font-size:16px;
            font-weight:700;

            margin-bottom:16px;

            background:
                linear-gradient(
                    90deg,
                    #8ab4ff,
                    #b388ff
                );

            -webkit-background-clip:text;
            -webkit-text-fill-color:transparent;
        }

        .btc-panel select{

            width:100%;

            padding:10px 12px;

            border:none;

            border-radius:12px;

            background:#1d2330;

            color:white;

            outline:none;

            margin-bottom:14px;

            font-size:14px;
        }

        .btc-switch{

            display:flex;

            align-items:center;

            gap:8px;

            margin-bottom:14px;

            font-size:14px;
        }

        .btc-thread-btn{

            width:100%;

            padding:11px;

            border:none;

            border-radius:12px;

            background:
                linear-gradient(
                    135deg,
                    var(--btc-main),
                    var(--btc-second)
                );

            color:white;

            font-weight:700;

            cursor:pointer;

            transition:.2s ease;
        }

        .btc-thread-btn:hover{

            transform:translateY(-1px);
        }

        /* =====================================================
           TRANSLATE BUTTON
        ===================================================== */

        .btc-translate-btn{

            display:inline-flex;

            align-items:center;

            gap:5px;

            padding:5px 10px;

            margin-top:7px;

            border-radius:8px;

            background:
                rgba(91,140,255,.12);

            border:
                1px solid rgba(91,140,255,.22);

            color:#8ab4ff;

            font-size:12px;

            font-weight:700;

            cursor:pointer;

            transition:.15s ease;
        }

        .btc-translate-btn:hover{

            background:
                rgba(91,140,255,.2);
        }

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

        .btc-translation{

            margin-top:14px;

            padding:16px;

            border-radius:16px;

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

            border:
                1px solid rgba(255,255,255,.06);

            line-height:1.8;

            font-size:14px;

            animation:btcPanel .18s ease;

            overflow-wrap:anywhere;
        }

        .btc-tools{

            display:flex;

            justify-content:space-between;

            align-items:center;

            margin-bottom:12px;
        }

        .btc-tool-buttons{

            display:flex;
            gap:7px;
        }

        .btc-tool{

            border:none;

            padding:5px 10px;

            border-radius:8px;

            background:#222a38;

            color:white;

            cursor:pointer;

            font-size:12px;
        }

        .btc-ar{

            direction:rtl;

            text-align:right;

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

            line-height:2.1;
        }

        @media(max-width:768px){

            .btc-panel{

                width:calc(100vw - 30px);
            }
        }

    `);

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

    class Cache{

        constructor(){

            this.prefix = 'btc_cache_';
        }

        hash(str){

            let hash = 0;

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

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

                hash |= 0;
            }

            return hash;
        }

        get(key){

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

        set(key,val){

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

    const cache = new Cache();

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

    function escapeHTML(str){

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

    /* =========================================================
       TRANSLATOR
    ========================================================= */

    async function translateText(text,target){

        if(!text) return text;

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

        const cached =
            cache.get(key);

        if(cached){

            return cached;
        }

        try{

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

            const res =
                await fetch(url);

            const data =
                await res.json();

            let translated = '';

            if(data.sentences){

                data.sentences.forEach(s=>{

                    translated += s.trans || '';
                });
            }

            translated =
                translated.trim();

            if(!translated){

                translated = text;
            }

            cache.set(key, translated);

            return translated;

        }catch(e){

            console.error(e);

            return text;
        }
    }

    /* =========================================================
       TRANSLATION UI
    ========================================================= */

    function createTranslationBox(original,translated){

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

        box.className =
            'btc-translation';

        if(CONFIG.lang === 'ar'){

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

        box.innerHTML = `

            <div class="btc-tools">

                <strong>
                    🌐 Translation
                </strong>

                <div class="btc-tool-buttons">

                    <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>

                ${
                    CONFIG.side

                    ?

                    `

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

                        <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>
        `;

        box.querySelector('.btc-copy')
            .onclick = () => {

                navigator.clipboard
                    .writeText(translated);
            };

        box.querySelector('.btc-speak')
            .onclick = () => {

                const speech =
                    new SpeechSynthesisUtterance(
                        translated
                    );

                speech.lang =
                    CONFIG.lang;

                speechSynthesis.speak(
                    speech
                );
            };

        box.querySelector('.btc-close')
            .onclick = () => {

                box.remove();
            };

        return box;
    }

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

    async function processPost(post){

        if(post.dataset.btcDone){

            return;
        }

        post.dataset.btcDone = '1';

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

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

        if(!footer || !content){

            return;
        }

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

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

        btn.innerHTML =
            '🌐 Translate';

        footer.appendChild(btn);

        btn.onclick = async () => {

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

            if(old){

                old.remove();

                return;
            }

            let text =
                content.innerText.trim();

            if(!text){

                return;
            }

            btn.innerHTML =
                '⏳ Translating';

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

            const box =
                createTranslationBox(
                    text,
                    translated
                );

            content.appendChild(box);

            btn.innerHTML =
                '✅ Done';

            setTimeout(()=>{

                btn.innerHTML =
                    '🌐 Translate';

            },1200);
        };

        if(CONFIG.auto){

            btn.click();
        }
    }

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

    function observe(){

        const observer =
            new MutationObserver(()=>{

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

                posts.forEach(
                    processPost
                );
            });

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

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

        posts.forEach(
            processPost
        );
    }

    /* =========================================================
       FLOAT BUTTON
    ========================================================= */

    function createFloatingUI(){

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

        btn.className =
            'btc-floating-btn';

        btn.innerHTML = `

            <svg viewBox="0 0 24 24">

                <path d="M12 2a10 10 0 100 20 10 10 0 000-20zm6.93 9h-3.02a15.7 15.7 0 00-1.32-5.01A8.02 8.02 0 0118.93 11zm-6.93 9c-.83-1.2-1.53-3.02-1.86-5h3.72c-.33 1.98-1.03 3.8-1.86 5zm-2.14-7a13.7 13.7 0 010-2h4.28a13.7 13.7 0 010 2H9.86zm.28 2h3.72c-.33 1.98-1.03 3.8-1.86 5-.83-1.2-1.53-3.02-1.86-5zm-4.07-4a8.02 8.02 0 014.34-5.01A15.7 15.7 0 009.09 11H6.07zm0 2h3.02c.12 1.78.56 3.5 1.32 5.01A8.02 8.02 0 016.07 13zm8.84 5.01c.76-1.51 1.2-3.23 1.32-5.01h3.02a8.02 8.02 0 01-4.34 5.01z"/>

            </svg>
        `;

        document.body.appendChild(btn);

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

        panel.className =
            'btc-panel';

        panel.innerHTML = `

            <div class="btc-title">
                Bitcointalk Translator
            </div>

            <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 class="btc-switch">

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

                Side by Side

            </label>

            <label class="btc-switch">

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

                Auto Translate

            </label>

            <button class="btc-thread-btn">
                Translate Entire Thread
            </button>

        `;

        document.body.appendChild(panel);

        /* =====================================================
           POSITION
        ===================================================== */

        function updatePanelPosition(){

            const rect =
                btn.getBoundingClientRect();

            panel.style.left =
                (rect.left - 230) + 'px';

            panel.style.top =
                (rect.top - 10) + 'px';
        }

        updatePanelPosition();

        /* =====================================================
           TOGGLE PANEL
        ===================================================== */

        btn.addEventListener('click', e => {

            if(btn.dragging){

                return;
            }

            CONFIG.panelOpen =
                !CONFIG.panelOpen;

            if(CONFIG.panelOpen){

                updatePanelPosition();

                panel.classList.add(
                    'active'
                );

            }else{

                panel.classList.remove(
                    'active'
                );
            }
        });

        /* =====================================================
           DRAG BUTTON
        ===================================================== */

        let dragging = false;

        let offsetX = 0;
        let offsetY = 0;

        btn.addEventListener('mousedown', e => {

            dragging = true;

            btn.dragging = false;

            offsetX =
                e.clientX
                - btn.offsetLeft;

            offsetY =
                e.clientY
                - btn.offsetTop;

            panel.classList.remove(
                'active'
            );

            document.body.style.userSelect =
                'none';
        });

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

                if(!dragging){

                    return;
                }

                btn.dragging = true;

                btn.style.left =
                    (e.clientX - offsetX)
                    + 'px';

                btn.style.top =
                    (e.clientY - offsetY)
                    + 'px';

                btn.style.right =
                    'auto';

                btn.style.bottom =
                    'auto';
            }
        );

        document.addEventListener(
            'mouseup',
            () => {

                dragging = false;

                document.body.style.userSelect =
                    '';
            }
        );

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

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

        lang.value =
            CONFIG.lang;

        lang.onchange = e => {

            CONFIG.lang =
                e.target.value;

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

        panel.querySelector('#btc-side')
            .onchange = e => {

                CONFIG.side =
                    e.target.checked;

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

        panel.querySelector('#btc-auto')
            .onchange = e => {

                CONFIG.auto =
                    e.target.checked;

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

        panel.querySelector('.btc-thread-btn')
            .onclick = async () => {

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

                for(const b of buttons){

                    b.click();

                    await new Promise(r=>{

                        setTimeout(r,300);

                    });
                }
            };
    }

    /* =========================================================
       SHORTCUT
    ========================================================= */

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

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

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

                if(btn){

                    btn.click();
                }
            }
        }
    );

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

    function init(){

        observe();

        createFloatingUI();

        console.log(
            'Bitcointalk Translator V4 Loaded'
        );
    }

    init();

})();