Bitcointalk Translator

Bitcointalk translator 4.1.1

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Bitcointalk Translator
// @namespace    https://bitcointalk.org/
// @version      4.1.1
// @description  Bitcointalk translator 4.1.1
// @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';

    const CONFIG = {
        lang: GM_getValue('btc_lang', 'bn'),
        side: GM_getValue('btc_side', false),
        auto: GM_getValue('btc_auto', false),
        panelOpen: false
    };

    function addStyle(css) {
        if (typeof GM_addStyle === 'function') {
            GM_addStyle(css);
            return;
        }

        const style = document.createElement('style');
        style.textContent = css;
        (document.head || document.documentElement).appendChild(style);
    }

    addStyle(`
        :root{
            --btc-main:#5b8cff;
            --btc-second:#7f5cff;
        }

        .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;
        }

        .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);
        }

        .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);
        }

        .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-segment{
            padding:12px 0;
            border-top:1px solid rgba(255,255,255,.08);
        }

        .btc-segment:first-child{
            padding-top:0;
            border-top:none;
        }

        .btc-segment-label{
            margin-bottom:6px;
            color:#8ab4ff;
            font-size:12px;
            font-weight:700;
        }

        .btc-original{
            margin-bottom:10px;
            color:rgba(255,255,255,.78);
        }

        .btc-ar{
            direction:rtl;
            text-align:right;
            font-family:Tahoma,Arial,sans-serif;
            line-height:2.1;
        }

        .btc-selection-btn{
            position:absolute;
            z-index:1000000;
            display:none;
            padding:7px 11px;
            border-radius:10px;
            background:linear-gradient(135deg,var(--btc-main),var(--btc-second));
            color:white;
            box-shadow:0 10px 24px rgba(0,0,0,.32);
            font-size:12px;
            font-weight:700;
            cursor:pointer;
            user-select:none;
        }

        .btc-selection-popup{
            position:absolute;
            z-index:1000000;
            width:min(360px,calc(100vw - 28px));
            padding:14px;
            border-radius:16px;
            background:linear-gradient(180deg,rgba(20,24,35,.98),rgba(15,18,28,.98));
            color:white;
            border:1px solid rgba(255,255,255,.08);
            box-shadow:0 20px 50px rgba(0,0,0,.45);
            line-height:1.7;
            font-size:14px;
            overflow-wrap:anywhere;
        }

        @media(max-width:768px){
            .btc-panel{
                width:calc(100vw - 30px);
            }
        }
    `);

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

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

    function normalizeText(text) {
        return text
            .replace(/\u00a0/g, ' ')
            .replace(/[ \t]+\n/g, '\n')
            .replace(/\n[ \t]+/g, '\n')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
    }

    function isIgnoredNode(node) {
        if (node.nodeType !== Node.ELEMENT_NODE) {
            return false;
        }

        return Boolean(
            node.closest(
                '.btc-translation,.btc-floating-btn,.btc-panel,.btc-selection-btn,.btc-selection-popup,script,style,noscript'
            )
        );
    }

    function getNodeText(node) {
        const clone = node.cloneNode(true);

        clone
            .querySelectorAll('.btc-translation,.btc-floating-btn,.btc-panel,.btc-selection-btn,.btc-selection-popup,script,style,noscript')
            .forEach(function (el) {
                el.remove();
            });

        return normalizeText(clone.innerText || clone.textContent || '');
    }

    function pushSegment(segments, type, text) {
        const clean = normalizeText(text);

        if (!clean) {
            return;
        }

        const previous = segments[segments.length - 1];

        if (previous && previous.type === type && previous.text === clean) {
            return;
        }

        segments.push({
            type: type,
            text: clean
        });
    }

    function closestElement(node, selector) {
        if (!node) {
            return null;
        }

        const element =
            node.nodeType === Node.ELEMENT_NODE
                ? node
                : node.parentElement;

        return element ? element.closest(selector) : null;
    }

    function extractPostSegments(content) {
        const segments = [];
        let replyParts = [];

        function flushReply() {
            pushSegment(segments, 'reply', replyParts.join('\n\n'));
            replyParts = [];
        }

        function walk(node) {
            if (isIgnoredNode(node)) {
                return;
            }

            if (node.nodeType === Node.TEXT_NODE) {
                const text = normalizeText(node.textContent || '');

                if (text) {
                    replyParts.push(text);
                }

                return;
            }

            if (node.nodeType !== Node.ELEMENT_NODE) {
                return;
            }

            if (node.matches('.quote,blockquote')) {
                flushReply();
                pushSegment(segments, 'quote', getNodeText(node));
                return;
            }

            if (node.matches('br')) {
                replyParts.push('\n');
                return;
            }

            Array.from(node.childNodes).forEach(walk);

            if (node.matches('p,div,li,ul,ol,table,tr')) {
                replyParts.push('\n');
            }
        }

        Array.from(content.childNodes).forEach(walk);
        flushReply();

        if (!segments.length) {
            pushSegment(segments, 'reply', getNodeText(content));
        }

        return segments;
    }

    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(function (s) {
                    translated += s.trans || '';
                });
            }

            translated = translated.trim();

            if (!translated) {
                translated = text;
            }

            cache.set(key, translated);

            return translated;
        } catch (e) {
            console.error(e);
            return text;
        }
    }

    async function translateSegments(segments) {
        const translated = [];

        for (const segment of segments) {
            translated.push({
                type: segment.type,
                text: segment.text,
                translated: await translateText(segment.text, CONFIG.lang)
            });
        }

        return translated;
    }

    function createTranslationBox(segments) {
        const box = document.createElement('div');
        const allTranslated = segments.map(function (segment) {
            return segment.translated;
        }).join('\n\n');

        let quoteCount = 0;

        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">Speak</button>
                    <button class="btc-tool btc-close">Close</button>
                </div>
            </div>
            <div class="btc-segments">
                ${segments.map(function (segment) {
                    const label = segment.type === 'quote' ? `Quote ${++quoteCount}` : 'User text';

                    return `
                        <div class="btc-segment btc-segment-${segment.type}">
                            <div class="btc-segment-label">${label}</div>
                            ${
                                CONFIG.side
                                    ? `
                                        <div class="btc-original">
                                            <b>Original:</b>
                                            <div>${escapeHTML(segment.text)}</div>
                                        </div>
                                        <div>
                                            <b>Translated:</b>
                                            <div>${escapeHTML(segment.translated)}</div>
                                        </div>
                                    `
                                    : `<div>${escapeHTML(segment.translated)}</div>`
                            }
                        </div>
                    `;
                }).join('')}
            </div>
        `;

        box.querySelector('.btc-copy').onclick = function () {
            navigator.clipboard.writeText(allTranslated);
        };

        box.querySelector('.btc-speak').onclick = function () {
            const speech = new SpeechSynthesisUtterance(allTranslated);
            speech.lang = CONFIG.lang;
            speechSynthesis.speak(speech);
        };

        box.querySelector('.btc-close').onclick = function () {
            box.remove();
        };

        return box;
    }

    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 function () {
            const old = post.querySelector('.btc-translation');

            if (old) {
                old.remove();
                return;
            }

            const segments = extractPostSegments(content);

            if (!segments.length) {
                return;
            }

            btn.innerHTML = 'Translating';

            const translatedSegments = await translateSegments(segments);
            const box = createTranslationBox(translatedSegments);

            content.appendChild(box);

            btn.innerHTML = 'Done';

            setTimeout(function () {
                btn.innerHTML = 'Translate';
            }, 1200);
        };

        if (CONFIG.auto) {
            btn.click();
        }
    }

    function observe() {
        const observer = new MutationObserver(function () {
            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);
    }

    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">Hindi</option>
                <option value="it">Italian</option>
                <option value="ja">Japanese</option>
                <option value="nl">Nederlands</option>
                <option value="yo">Yoruba</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">Swedish</option>
                <option value="tr">Turkish</option>
                <option value="ur">Urdu</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);

        function updatePanelPosition() {
            const rect = btn.getBoundingClientRect();
            const left = Math.max(10, Math.min(window.innerWidth - panel.offsetWidth - 10, rect.left - 230));
            const top = Math.max(10, Math.min(window.innerHeight - panel.offsetHeight - 10, rect.top - 10));

            panel.style.left = left + 'px';
            panel.style.top = top + 'px';
        }

        updatePanelPosition();

        btn.addEventListener('click', function () {
            if (btn.dragging) {
                return;
            }

            CONFIG.panelOpen = !CONFIG.panelOpen;

            if (CONFIG.panelOpen) {
                updatePanelPosition();
                panel.classList.add('active');
            } else {
                panel.classList.remove('active');
            }
        });

        let dragging = false;
        let offsetX = 0;
        let offsetY = 0;

        btn.addEventListener('mousedown', function (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', function (e) {
            if (!dragging) {
                return;
            }

            btn.dragging = true;

            btn.style.left = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, e.clientX - offsetX)) + 'px';
            btn.style.top = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, e.clientY - offsetY)) + 'px';
            btn.style.right = 'auto';
            btn.style.bottom = 'auto';
        });

        document.addEventListener('mouseup', function () {
            dragging = false;
            document.body.style.userSelect = '';
        });

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

        lang.value = CONFIG.lang;

        lang.onchange = function (e) {
            CONFIG.lang = e.target.value;
            GM_setValue('btc_lang', e.target.value);
        };

        panel.querySelector('#btc-side').onchange = function (e) {
            CONFIG.side = e.target.checked;
            GM_setValue('btc_side', e.target.checked);
        };

        panel.querySelector('#btc-auto').onchange = function (e) {
            CONFIG.auto = e.target.checked;
            GM_setValue('btc_auto', e.target.checked);
        };

        panel.querySelector('.btc-thread-btn').onclick = async function () {
            const buttons = document.querySelectorAll('.btc-translate-btn');

            for (const b of buttons) {
                const post = b.closest('td');

                if (post && !post.querySelector('.btc-translation')) {
                    b.click();
                    await new Promise(function (resolve) {
                        setTimeout(resolve, 300);
                    });
                }
            }
        };
    }

    function createSelectionTranslator() {
        const button = document.createElement('div');

        button.className = 'btc-selection-btn';
        button.textContent = 'Translate selection';

        document.body.appendChild(button);

        let selectedText = '';

        function removePopup() {
            document
                .querySelectorAll('.btc-selection-popup')
                .forEach(function (el) {
                    el.remove();
                });
        }

        function hideButton() {
            button.style.display = 'none';
        }

        document.addEventListener('mouseup', function () {
            setTimeout(function () {
                const selection = window.getSelection();

                if (!selection || selection.isCollapsed) {
                    hideButton();
                    return;
                }

                if (closestElement(selection.anchorNode, '.btc-panel,.btc-floating-btn,.btc-selection-btn,.btc-selection-popup')) {
                    hideButton();
                    return;
                }

                selectedText = normalizeText(selection.toString());

                if (!selectedText) {
                    hideButton();
                    return;
                }

                const rect = selection.getRangeAt(0).getBoundingClientRect();

                button.style.left = Math.min(window.scrollX + rect.left, window.scrollX + window.innerWidth - 150) + 'px';
                button.style.top = (window.scrollY + rect.bottom + 8) + 'px';
                button.style.display = 'block';
            }, 0);
        });

        document.addEventListener('mousedown', function (e) {
            if (!closestElement(e.target, '.btc-selection-btn,.btc-selection-popup')) {
                hideButton();
            }
        });

        button.addEventListener('mousedown', function (e) {
            e.preventDefault();
        });

        button.addEventListener('click', async function () {
            if (!selectedText) {
                return;
            }

            removePopup();
            button.textContent = 'Translating';

            const translated = await translateText(selectedText, CONFIG.lang);
            const popup = document.createElement('div');

            popup.className = 'btc-selection-popup';

            if (CONFIG.lang === 'ar') {
                popup.classList.add('btc-ar');
            }

            popup.style.left = button.style.left;
            popup.style.top = (parseFloat(button.style.top) + 34) + 'px';
            popup.innerHTML = `
                <div class="btc-tools">
                    <strong>Selection</strong>
                    <div class="btc-tool-buttons">
                        <button class="btc-tool btc-copy">Copy</button>
                        <button class="btc-tool btc-close">Close</button>
                    </div>
                </div>
                <div>${escapeHTML(translated)}</div>
            `;

            popup.querySelector('.btc-copy').onclick = function () {
                navigator.clipboard.writeText(translated);
            };

            popup.querySelector('.btc-close').onclick = function () {
                popup.remove();
            };

            document.body.appendChild(popup);

            button.textContent = 'Translate selection';
            hideButton();
        });
    }

    document.addEventListener('keydown', function (e) {
        if (e.altKey && e.key === 't') {
            const btn = document.querySelector('.btc-translate-btn');

            if (btn) {
                btn.click();
            }
        }
    });

    function init() {
        if (!document.body) {
            setTimeout(init, 100);
            return;
        }

        observe();
        createFloatingUI();
        createSelectionTranslator();

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

    init();
})();