X (Twitter) Google Translator + PDF

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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