X (Twitter) Google Translator + PDF

X -> Native Google Translator via Twitter API. PDF translation via Yandex popup.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X (Twitter) Google Translator + PDF
// @namespace    http://tampermonkey.net/
// @version      6.0
// @description  X -> Native Google Translator via Twitter API. PDF translation via Yandex popup.
// @author       Antigravity
// @match        https://twitter.com/*
// @match        https://x.com/*
// @match        https://translate.yandex.ru/*
// @match        https://translate.yandex.com/*
// @match        *://*/*.pdf
// @match        *://*/*.pdf?*
// @match        file:///*/*.pdf
// @icon         https://abs.twimg.com/favicons/twitter.3.ico
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_cookie
// @require      https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js
// @connect      browser.translate.yandex.net
// @connect      translate.yandex.net
// @connect      pbs.twimg.com
// @connect      x.com
// @connect      twitter.com
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    const IS_YANDEX = location.hostname.includes('yandex');
    const IS_PDF = location.pathname.toLowerCase().endsWith('.pdf') || document.contentType === 'application/pdf';
    const STORAGE_KEY = 'x_yandex_image_payload';

    // --- YANDEX SIDE ---
    if (IS_YANDEX) {
        window.addEventListener('load', () => {
            if (!location.href.includes('/ocr')) return;
            const imageData = GM_getValue(STORAGE_KEY);
            if (!imageData) return;
            try {
                const parts = imageData.split(',');
                const mimeMatch = parts[0].match(/:(.*?);/);
                const mimeType = mimeMatch ? mimeMatch[1] : 'image/jpeg';
                const base64 = parts[1];
                const byteCharacters = atob(base64);
                const byteNumbers = new Array(byteCharacters.length);
                for (let i = 0; i < byteCharacters.length; i++) {
                    byteNumbers[i] = byteCharacters.charCodeAt(i);
                }
                const blob = new Blob([new Uint8Array(byteNumbers)], { type: mimeType });
                const file = new File([blob], "image.jpg", { type: mimeType });
                setTimeout(() => {
                    const fileInput = document.querySelector('input[type="file"]');
                    if (fileInput) {
                        const dt = new DataTransfer();
                        dt.items.add(file);
                        fileInput.files = dt.files;
                        fileInput.dispatchEvent(new Event('change', { bubbles: true }));
                        GM_deleteValue(STORAGE_KEY);
                    }
                }, 1500);
            } catch (e) { console.error('[X-Translator] Error:', e); }
        });
        return;
    }

    // --- YANDEX TRANSLATE API (for PDF) ---
    function translateTextYandex(text, tgtLang = 'ru') {
        return new Promise((resolve) => {
            const doTranslate = (srcLang) => {
                GM_xmlhttpRequest({
                    method: "POST",
                    url: `https://browser.translate.yandex.net/api/v1/tr.json/translate?lang=${srcLang}-${tgtLang}&text=${encodeURIComponent(text)}&srv=browser_video_translation`,
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                    data: "maxRetryCount=2&fetchAbortTimeout=500",
                    onload: (r) => { try { resolve(JSON.parse(r.responseText).text?.[0] || text); } catch { resolve(text); } },
                    onerror: () => resolve(text)
                });
            };
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://translate.yandex.net/api/v1/tr.json/detect?srv=browser_video_translation&text=${encodeURIComponent(text.substring(0, 300))}`,
                onload: (r) => { try { doTranslate(JSON.parse(r.responseText).lang || 'en'); } catch { doTranslate('en'); } },
                onerror: () => doTranslate('en')
            });
        });
    }

    // --- PDF SIDE ---
    if (IS_PDF) {
        if (typeof pdfjsLib !== 'undefined') {
            pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
        }

        // Just add a floating button, don't replace the viewer
        const btn = document.createElement('button');
        btn.innerHTML = '🌐 Перевести PDF';
        btn.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 2147483647;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 30px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            box-shadow: 0 4px 20px rgba(102, 126, 234, 0.5);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            transition: transform 0.2s, box-shadow 0.2s;
        `;
        btn.onmouseenter = () => { btn.style.transform = 'scale(1.05)'; };
        btn.onmouseleave = () => { btn.style.transform = 'scale(1)'; };

        btn.onclick = async () => {
            btn.disabled = true;
            btn.innerHTML = '⏳ Загрузка...';

            try {
                const pdf = await pdfjsLib.getDocument(location.href).promise;
                let allText = [];

                for (let i = 1; i <= pdf.numPages; i++) {
                    btn.innerHTML = `📄 Страница ${i}/${pdf.numPages}`;
                    const page = await pdf.getPage(i);
                    const tc = await page.getTextContent();

                    // Group by Y position
                    const lines = new Map();
                    for (const item of tc.items) {
                        if (!item.str?.trim()) continue;
                        const y = Math.round(item.transform[5]);
                        if (!lines.has(y)) lines.set(y, []);
                        lines.get(y).push({ str: item.str, x: item.transform[4] });
                    }

                    // Sort and join
                    const sorted = [...lines.entries()].sort((a, b) => b[0] - a[0]);
                    const pageText = sorted.map(([_, items]) => {
                        items.sort((a, b) => a.x - b.x);
                        return items.map(i => i.str).join(' ');
                    }).join('\n');

                    allText.push(`--- СТРАНИЦА ${i} ---\n${pageText}`);
                }

                const fullText = allText.join('\n\n');

                btn.innerHTML = '🔄 Перевод...';

                // Translate in chunks
                const chunks = [];
                let remaining = fullText;
                while (remaining.length > 0) {
                    chunks.push(remaining.substring(0, 4500));
                    remaining = remaining.substring(4500);
                }

                let translated = '';
                for (let i = 0; i < chunks.length; i++) {
                    btn.innerHTML = `🔄 Перевод ${i + 1}/${chunks.length}`;
                    translated += await translateTextYandex(chunks[i]);
                }

                // Open in new window
                const win = window.open('', '_blank', 'width=800,height=600');
                win.document.write(`
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Перевод PDF</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #1a1a2e;
            color: #e0e0e0;
            padding: 30px;
            line-height: 1.8;
        }
        h1 {
            color: #a78bfa;
            margin-bottom: 20px;
            font-size: 18px;
        }
        .content {
            background: #252542;
            padding: 25px;
            border-radius: 12px;
            white-space: pre-wrap;
            font-size: 15px;
        }
        .page-header {
            color: #818cf8;
            font-weight: bold;
            margin: 20px 0 10px 0;
        }
    </style>
</head>
<body>
    <h1>📄 Перевод документа</h1>
    <div class="content">${translated.replace(/--- СТРАНИЦА (\d+) ---/g, '<div class="page-header">📄 Страница $1</div>')}</div>
</body>
</html>
                `);
                win.document.close();

                btn.innerHTML = '✅ Готово!';
                setTimeout(() => { btn.innerHTML = '🌐 Перевести PDF'; btn.disabled = false; }, 2000);

            } catch (e) {
                console.error(e);
                btn.innerHTML = '❌ Ошибка';
                setTimeout(() => { btn.innerHTML = '🌐 Перевести PDF'; btn.disabled = false; }, 2000);
            }
        };

        // Wait for body
        const addBtn = () => { if (document.body) document.body.appendChild(btn); else setTimeout(addBtn, 100); };
        addBtn();
        return;
    }

    // --- TWITTER/X SIDE ---

    // Get cookies from document.cookie
    function getCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) return parts.pop().split(';').shift();
        return null;
    }

    // Twitter API Translation
    function translateTweetAPI(tweetId) {
        return new Promise((resolve, reject) => {
            const ct0 = getCookie('ct0');
            if (!ct0) {
                reject(new Error('No ct0 cookie found. Make sure you are logged in.'));
                return;
            }

            const url = `https://x.com/i/api/1.1/strato/column/None/tweetId=${tweetId},destinationLanguage=None,translationSource=Some(Google),feature=None,timeout=None,onlyCached=None/translation/service/translateTweet`;

            fetch(url, {
                method: 'GET',
                headers: {
                    'accept': '*/*',
                    'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
                    'x-csrf-token': ct0,
                    'x-twitter-active-user': 'yes',
                    'x-twitter-auth-type': 'OAuth2Session',
                    'x-twitter-client-language': 'ru'
                },
                credentials: 'include'
            })
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => {
                    if (data.translationState === 'Success' && data.translation) {
                        resolve({
                            translation: data.translation,
                            sourceLanguage: data.sourceLanguage,
                            localizedSourceLanguage: data.localizedSourceLanguage,
                            destinationLanguage: data.destinationLanguage
                        });
                    } else {
                        reject(new Error(data.translationState || 'Translation failed'));
                    }
                })
                .catch(reject);
        });
    }

    // Extract tweet ID from article element
    function getTweetId(article) {
        // Try to find tweet ID from links
        const timeLink = article.querySelector('a[href*="/status/"]');
        if (timeLink) {
            const match = timeLink.href.match(/\/status\/(\d+)/);
            if (match) return match[1];
        }
        // Try from data attributes
        const tweetLink = article.querySelector('[data-testid="User-Name"] a[href*="/status/"]');
        if (tweetLink) {
            const match = tweetLink.href.match(/\/status\/(\d+)/);
            if (match) return match[1];
        }
        return null;
    }

    // Create translation block in Twitter's native style
    function createTranslationBlock(data) {
        const container = document.createElement('div');
        container.className = 'css-175oi2r r-14gqq1x x-translation-block';
        container.style.marginTop = '10px';

        // Translation header with Google logo
        const header = document.createElement('div');
        header.className = 'css-146c3p1 r-dnmrzs r-1udh08x r-1udbk01 r-3s2u2q r-bcqeeo r-qvutc0 r-37j5jr r-q4m81j r-n6v787 r-1cwl3u0 r-16dba41 r-6koalj r-1w6e6rj r-14gqq1x';
        header.style.cssText = 'color: rgb(15, 20, 25); display: flex; align-items: center; gap: 4px; margin-bottom: 8px;';
        header.dir = 'ltr';

        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'css-1jxf684 r-bcqeeo r-qvutc0 r-poiln3 r-n6v787 r-1cwl3u0 r-1loqt21 r-fdjqy7';
        toggleBtn.type = 'button';
        toggleBtn.style.cssText = 'color: rgb(29, 155, 240); background: none; border: none; cursor: pointer; font: inherit;';
        toggleBtn.innerHTML = `<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Язык оригинала: ${data.localizedSourceLanguage}, переведено с помощью</span>`;

        const googleLink = document.createElement('a');
        googleLink.href = 'https://translate.google.com';
        googleLink.rel = 'noopener noreferrer nofollow';
        googleLink.target = '_blank';
        googleLink.className = 'css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3 r-n6v787 r-1cwl3u0 r-1537yvj r-enhg6t r-1loqt21';
        googleLink.style.color = 'rgb(83, 100, 113)';
        googleLink.innerHTML = `<svg viewBox="0 0 74 24" aria-hidden="true" style="height: 14px; width: auto; vertical-align: middle;"><g><path d="M9.833 17.667C5.013 17.667.96 13.74.96 8.92S5.007.173 9.833.173c2.667 0 4.567 1.047 5.993 2.413L14.14 4.273c-1.027-.96-2.413-1.707-4.307-1.707-3.52 0-6.273 2.84-6.273 6.36s2.753 6.36 6.273 6.36c2.28 0 3.587-.92 4.413-1.747.68-.68 1.133-1.668 1.3-3.008H10v-2.4h7.873c.087.428.127.94.127 1.495 0 1.793-.493 4.013-2.067 5.587-1.54 1.6-3.5 2.453-6.1 2.453z" fill="#4285F4"></path><path d="M30.633 12.04c0 3.24-2.533 5.633-5.633 5.633-3.107 0-5.633-2.387-5.633-5.633 0-3.267 2.527-5.633 5.633-5.633 3.1.006 5.633 2.373 5.633 5.633zm-2.466 0c0-2.027-1.467-3.413-3.167-3.413s-3.167 1.387-3.167 3.413c0 2.007 1.467 3.413 3.167 3.413s3.167-1.406 3.167-3.413z" fill="#EA4335"></path><path d="M43.3 12.033c0 3.24-2.527 5.633-5.633 5.633s-5.633-2.387-5.633-5.633c0-3.267 2.527-5.633 5.633-5.633S43.3 8.773 43.3 12.033zm-2.467 0c0-2.027-1.467-3.413-3.167-3.413S34.5 10.007 34.5 12.033c0 2.007 1.467 3.413 3.167 3.413s3.166-1.406 3.166-3.413z" fill="#FBBC05"></path><path d="M55.333 6.747V16.86c0 4.16-2.453 5.867-5.353 5.867-2.733 0-4.373-1.833-4.993-3.327l2.153-.893c.387.92 1.32 2.007 2.84 2.007 1.853 0 3.007-1.153 3.007-3.307v-.813H52.9c-.553.68-1.62 1.28-2.967 1.28-2.813 0-5.267-2.453-5.267-5.613 0-3.18 2.453-5.652 5.267-5.652 1.347 0 2.413.6 2.967 1.26h.087v-.92h2.346zM53.16 12.06c0-1.987-1.32-3.433-3.007-3.433-1.707 0-3.007 1.453-3.007 3.433 0 1.96 1.3 3.393 3.007 3.393 1.68-.006 3.007-1.433 3.007-3.393z" fill="#4285F4"></path><path d="M59.807.78v16.553h-2.473V.78h2.473z" fill="#34A853"></path><path d="M69.693 13.893l1.92 1.28c-.62.92-2.113 2.493-4.693 2.493-3.2 0-5.587-2.473-5.587-5.633 0-3.347 2.413-5.633 5.313-5.633 2.92 0 4.353 2.327 4.82 3.587l.253.64-7.534 3.113c.573 1.133 1.473 1.707 2.733 1.707s2.133-.62 2.773-1.554zm-5.906-2.026l5.033-2.093c-.28-.707-1.107-1.193-2.093-1.193-1.254 0-3.007 1.107-2.94 3.287z" fill="#EA4335"></path></g></svg>`;

        header.appendChild(toggleBtn);
        header.appendChild(document.createTextNode(' '));
        header.appendChild(googleLink);

        // Translation text
        const translationDiv = document.createElement('div');
        translationDiv.dir = 'auto';
        translationDiv.lang = data.destinationLanguage;
        translationDiv.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-1inkyih r-16dba41 r-bnwqim r-135wba7';
        translationDiv.style.color = 'rgb(15, 20, 25)';
        translationDiv.dataset.testid = 'tweetText';

        const translationSpan = document.createElement('span');
        translationSpan.className = 'css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3';
        translationSpan.textContent = data.translation;

        translationDiv.appendChild(translationSpan);

        container.appendChild(header);
        container.appendChild(translationDiv);

        // Toggle functionality
        toggleBtn.onclick = (e) => {
            e.stopPropagation();
            e.preventDefault();
            translationDiv.style.display = translationDiv.style.display === 'none' ? 'block' : 'none';
        };

        return container;
    }

    const STYLE = `
        .x-yandex-btn{display:flex;align-items:center;justify-content:center;margin-left:10px;cursor:pointer;border-radius:9999px;padding:6px;color:rgb(83,100,113);transition:background 0.2s}
        .x-yandex-btn:hover{background:rgba(29,155,240,0.1);color:rgb(29,155,240)}
        .x-yandex-btn svg{width:1.25em;height:1.25em;fill:currentColor}
        .x-yandex-group{display:flex;align-items:center}
        .x-translate-loading{opacity:0.7;font-style:italic;margin-top:10px;padding:10px;background:rgba(29,155,240,0.1);border-radius:8px}
        .x-translate-error{color:rgb(244,33,46);margin-top:10px;padding:10px;background:rgba(244,33,46,0.1);border-radius:8px}
        .x-yandex-overlay-btn{position:absolute;top:8px;right:8px;background:rgba(0,0,0,0.6);color:white;border-radius:4px;padding:5px 8px;font-size:13px;font-weight:700;cursor:pointer;z-index:2147483647;backdrop-filter:blur(4px);display:flex;align-items:center;gap:5px;border:1px solid rgba(255,255,255,0.2);transition:background 0.2s}
        .x-yandex-overlay-btn:hover{background:rgba(0,0,0,0.8)}
        .x-yandex-overlay-btn svg{width:1.2em;height:1.2em;fill:currentColor}
    `;

    const ICONS = {
        translate: `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"></path></svg>`,
        photo: `<svg viewBox="0 0 24 24"><path d="M19.75 2H4.25C3.01 2 2 3.01 2 4.25v15.5C2 20.99 3.01 22 4.25 22h15.5c1.24 0 2.25-1.01 2.25-2.25V4.25C22 3.01 20.99 2 19.75 2zM4.25 3.5h15.5c.413 0 .75.337.75.75v9.676l-3.858-3.858c-.14-.14-.33-.22-.53-.22h-.003c-.2 0-.393.08-.532.224l-4.317 4.384-1.813-1.806c-.14-.14-.33-.22-.53-.22-.193 0-.39.08-.53.224L3.5 17.65V4.25c0-.413.337-.75.75-.75zm0 17c-.413 0-.75-.337-.75-.75v-2.076l6.154-6.154 3.69 3.69c.143.144.333.225.535.225.2 0 .39-.08.53-.224l3.118-3.167L20.5 18.068v2.682c0 .413-.337.75-.75.75H4.25z"></path></svg>`
    };

    function addStyles() {
        if (document.getElementById('x-yandex-style')) return;
        const s = document.createElement('style');
        s.id = 'x-yandex-style';
        s.textContent = STYLE;
        document.head.appendChild(s);
    }

    function handleImage(btn, url) {
        const old = btn.innerHTML;
        btn.innerHTML = '<span>Loading...</span>';
        GM_xmlhttpRequest({
            method: "GET", url, responseType: "blob",
            onload: (r) => {
                if (r.status === 200) {
                    const reader = new FileReader();
                    reader.onloadend = () => {
                        GM_setValue(STORAGE_KEY, reader.result);
                        btn.innerHTML = '<span>Opening...</span>';
                        GM_openInTab('https://translate.yandex.ru/ocr', { active: true });
                        setTimeout(() => btn.innerHTML = old, 2000);
                    };
                    reader.readAsDataURL(r.response);
                } else {
                    btn.innerHTML = '<span>Error</span>';
                    setTimeout(() => btn.innerHTML = old, 2000);
                }
            },
            onerror: () => { btn.innerHTML = '<span>Error</span>'; setTimeout(() => btn.innerHTML = old, 2000); }
        });
    }

    function process() {
        // Image buttons
        document.querySelectorAll('img[src*="pbs.twimg.com/media"]').forEach(img => {
            const c = img.closest('div[data-testid="tweetPhoto"]') || img.parentElement;
            if (c && !c.dataset.yandexOverlay) {
                const btn = document.createElement('div');
                btn.className = 'x-yandex-overlay-btn';
                btn.innerHTML = `${ICONS.photo} <span>Translate</span>`;
                btn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); handleImage(btn, img.src); };
                if (getComputedStyle(c).position === 'static') c.style.position = 'relative';
                c.appendChild(btn);
                c.dataset.yandexOverlay = 'true';
            }
        });

        // Text buttons - use Twitter's native translation API
        document.querySelectorAll('article[data-testid="tweet"]:not([data-x-translate])').forEach(t => {
            const bar = t.querySelector('div[role="group"]');
            const txt = t.querySelector('div[data-testid="tweetText"]');
            if (bar && txt) {
                const tweetId = getTweetId(t);
                if (!tweetId) return;

                const btn = document.createElement('div');
                btn.className = 'x-yandex-btn';
                btn.title = 'Translate via Google';
                btn.innerHTML = ICONS.translate;
                btn.onclick = async (e) => {
                    e.stopPropagation();

                    // Check if translation already exists
                    let existingTranslation = txt.parentNode.querySelector('.x-translation-block');
                    if (existingTranslation) {
                        existingTranslation.style.display = existingTranslation.style.display === 'none' ? 'block' : 'none';
                        return;
                    }

                    // Show loading
                    const loadingDiv = document.createElement('div');
                    loadingDiv.className = 'x-translate-loading';
                    loadingDiv.textContent = 'Переводим...';
                    txt.parentNode.insertBefore(loadingDiv, txt.nextSibling);

                    try {
                        const data = await translateTweetAPI(tweetId);
                        loadingDiv.remove();

                        const translationBlock = createTranslationBlock(data);
                        txt.parentNode.insertBefore(translationBlock, txt.nextSibling);

                    } catch (err) {
                        loadingDiv.className = 'x-translate-error';
                        loadingDiv.textContent = 'Ошибка: ' + err.message;
                        console.error('[X-Translator]', err);
                    }
                };
                const g = document.createElement('div');
                g.className = 'x-yandex-group';
                g.appendChild(btn);
                bar.appendChild(g);
                t.dataset.xTranslate = 'true';
            }
        });
    }

    addStyles();
    new MutationObserver(process).observe(document.body, { childList: true, subtree: true });
    process();
})();