Greasy Fork is available in English.
X -> Native Google Translator via Twitter API. PDF translation via Yandex popup.
// ==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();
})();