Bitcointalk Userscript Translator

Bitcointalk Translator with modern UI and multi-language support

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();

})();