// ==UserScript== // @name X/Twitter & Discord 实时翻译插件 // @namespace http://tampermonkey.net/ // @version 1.8 // @description 支持推特实时翻译,Discord 翻译,支持翻译字体大小颜色可调整。精简版,仅保留翻译功能。 // @author Antigravity // @match *://twitter.com/* // @match *://x.com/* // @match *://pro.x.com/* // @match *://discord.com/* // @match *://www.patreon.com/* // @match *://ko-fi.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect translate.googleapis.com // @downloadURL https://update.greasyfork.icu/scripts/564823/XTwitter%20%20Discord%20%E5%AE%9E%E6%97%B6%E7%BF%BB%E8%AF%91%E6%8F%92%E4%BB%B6.user.js // @updateURL https://update.greasyfork.icu/scripts/564823/XTwitter%20%20Discord%20%E5%AE%9E%E6%97%B6%E7%BF%BB%E8%AF%91%E6%8F%92%E4%BB%B6.meta.js // ==/UserScript== (function () { 'use strict'; console.log("🚀 翻译插件已启动..."); const __SystemConfig = { decode: (str) => { try { return decodeURIComponent(escape(window.atob(str))); } catch (e) { return ""; } }, params: { svc_trans: "aHR0cHM6Ly90cmFuc2xhdGUuZ29vZ2xlYXBpcy5jb20vdHJhbnNsYXRlX2Evc2luZ2xl" } }; const Storage = { getConfig: () => ({ transColor: '#00E676', transFontSize: '14px', floatTop: '60%', transMode: 'below', // 'below', 'hover', 'bilingual' ...JSON.parse(GM_getValue('ling_config_simple', '{}')) }), setConfig: (cfg) => { GM_setValue('ling_config_simple', JSON.stringify(cfg)); updateStyles(); } }; function updateStyles() { const cfg = Storage.getConfig(); const oldStyle = document.getElementById('ling-style'); if (oldStyle) oldStyle.remove(); const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const css = ` .ling-trans-box { margin-top: 6px; padding: 8px 10px; background: #ffffff; border-left: 3px solid ${cfg.transColor}; border-radius: 4px; color: #000000; font-size: ${cfg.transFontSize}; line-height: 1.5; font-family: "Consolas", monospace; white-space: normal; } .ling-trans-label { display: block; margin-bottom: 4px; opacity: 0.6; font-size: 10px; line-height: 1.2; } .ling-trans-text { white-space: pre-wrap; overflow-wrap: anywhere; } .ling-trans-line { display: block; margin: 0 0 3px; } .ling-trans-line:last-child { margin-bottom: 0; } .ling-discord-box { margin-top: 4px; padding: 4px 8px; opacity: 0.9; background: rgba(255,255,255,0.9); border-left: 2px solid ${cfg.transColor}; color: #000000; } .ling-hover-tooltip { position: absolute; background: rgba(255,255,255,0.9); color: #000000, padding: 8px 12px; border-radius: 6px; font-size: ${cfg.transFontSize}; line-height: 1.4; z-index: 2147483647; max-width: 300px; word-wrap: break-word; box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none; opacity: 0; transition: opacity 0.2s; } .ling-hover-tooltip.show { opacity: 1; } .ling-bilingual { position: relative; } .ling-bilingual .original { opacity: 0.6; text-decoration: line-through; } .ling-bilingual .translation { color: #000000; font-size: ${cfg.transFontSize}; } .ling-dashboard { position: fixed; top: 15%; right: ${isMobile ? '10px' : '20px'}; background: #111; border: 1px solid #333; border-radius: 12px; padding: ${isMobile ? '10px' : '15px'}; z-index: 2147483646; box-shadow: 0 10px 30px rgba(0,0,0,0.8); min-width: ${isMobile ? '180px' : '200px'}; opacity: 0; visibility: hidden; transform: translateY(-10px); transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; } .ling-dashboard.active { opacity: 1; visibility: visible; transform: translateY(0); } .ling-float-toggle { position: fixed; right: ${isMobile ? '5px' : '10px'}; top: ${cfg.floatTop}; width: ${isMobile ? '35px' : '45px'}; height: ${isMobile ? '35px' : '45px'}; border-radius: 50%; background: #000; border: 2px solid #00E676; color: #fff; display: flex; justify-content: center; align-items: center; cursor: ${isMobile ? 'pointer' : 'grab'}; z-index: 2147483645; box-shadow: 0 4px 10px rgba(0,0,0,0.5); transition: transform 0.1s, opacity 0.2s; opacity: 0.8; user-select: none; font-size: ${isMobile ? '10px' : '14px'}; } .ling-float-toggle:hover { opacity: 1; transform: scale(1.05); } .ling-logo-text { font-family: 'Arial Black', sans-serif; font-weight: 900; font-size: ${isMobile ? '12px' : '14px'}; } #ling-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2147483647; display: flex; justify-content: center; align-items: center; } #ling-settings-box { background: #16181c; border: 1px solid #333; border-radius: 12px; padding: ${isMobile ? '15px' : '20px'}; width: ${isMobile ? '90%' : '300px'}; max-width: 400px; color: #fff; } .ling-row { margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; } .ling-row label { flex: 1; margin-bottom: ${isMobile ? '5px' : '0'}; } .ling-row input { flex: 1; max-width: 100px; } .ling-btn { background: #00E676; color: #000; border: none; padding: ${isMobile ? '10px' : '8px'}; border-radius: 5px; width: 100%; font-weight: bold; cursor: pointer; margin-top: 10px; font-size: ${isMobile ? '14px' : '16px'}; } @media (max-width: 768px) { .ling-dashboard { right: 10px; min-width: 180px; padding: 10px; } .ling-float-toggle { right: 5px; width: 40px; height: 40px; } .ling-hover-tooltip { max-width: 250px; font-size: 12px; } } `; const node = document.createElement('style'); node.id = 'ling-style'; node.innerHTML = css; document.head.appendChild(node); } function createProxyRequest(options) { return GM_xmlhttpRequest(options); } function getTranslateApiBase() { let apiBase = "https://translate.googleapis.com/translate_a/single"; try { apiBase = __SystemConfig.decode(__SystemConfig.params.svc_trans) || apiBase; } catch (e) { } return apiBase; } function readTranslateResponse(responseText) { const data = JSON.parse(responseText); const parts = []; if (data && data[0]) { data[0].forEach(i => { if (i[0]) parts.push(i[0]); }); } return parts.join(''); } function translateText(text, callback) { const url = `${getTranslateApiBase()}?client=gtx&sl=auto&tl=zh-CN&dt=t&q=${encodeURIComponent(text)}`; createProxyRequest({ method: "GET", url: url, timeout: 5000, onload: (res) => { try { callback(readTranslateResponse(res.responseText)); } catch (e) { callback(''); } }, onerror: () => callback(''), ontimeout: () => callback('') }); } function splitTranslatableLines(text) { return (text || '') .split(/\n+/) .map(line => line.trim()) .filter(Boolean); } function processContent(element, text, platform) { if (!text || element.dataset.lingProcessed) return; element.dataset.lingProcessed = "true"; const isForeign = !/[\u4e00-\u9fa5]/.test(text) || (text.match(/[\u4e00-\u9fa5]/g) || []).length / text.length < 0.3; if (isForeign && text.length > 3) { const textShort = text.length > 2000 ? text.substring(0, 2000) : text; const sourceLines = splitTranslatableLines(textShort); if (platform === 'twitter' && sourceLines.length > 1) { const translatedLines = new Array(sourceLines.length); let doneCount = 0; sourceLines.forEach((line, index) => { translateText(line, (translated) => { translatedLines[index] = translated; doneCount += 1; if (doneCount === sourceLines.length) { const visibleLines = translatedLines.filter(Boolean); if (visibleLines.length) renderBox(element, visibleLines, platform); } }); }); } else { translateText(textShort, (transResult) => { if (transResult) renderBox(element, transResult, platform); }); } } } function appendTranslationContent(container, transText) { const label = document.createElement('span'); label.className = 'ling-trans-label'; label.textContent = '[翻译]'; container.appendChild(label); const content = document.createElement('div'); content.className = 'ling-trans-text'; const lines = Array.isArray(transText) ? transText : String(transText || '').split(/\n+/); lines.forEach((line) => { const lineNode = document.createElement('span'); lineNode.className = 'ling-trans-line'; lineNode.textContent = line; content.appendChild(lineNode); }); container.appendChild(content); } function renderBox(element, transText, platform) { const cfg = Storage.getConfig(); const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // 获取元素的背景色和字体 const computedStyle = window.getComputedStyle(element); const bgColor = computedStyle.backgroundColor || 'rgba(0,0,0,0.8)'; const fontFamily = computedStyle.fontFamily || 'inherit'; const fontSize = computedStyle.fontSize || cfg.transFontSize; const lineHeight = computedStyle.lineHeight || '1.5'; if (cfg.transMode === 'hover' && !isMobile) { // 悬浮模式:鼠标悬停显示翻译 const tooltip = document.createElement('div'); tooltip.className = 'ling-hover-tooltip'; tooltip.textContent = Array.isArray(transText) ? transText.join('\n') : transText; tooltip.style.backgroundColor = 'rgba(255,255,255,0.9)'; tooltip.style.fontFamily = fontFamily; tooltip.style.fontSize = fontSize; tooltip.style.lineHeight = lineHeight; element.style.position = 'relative'; element.appendChild(tooltip); element.addEventListener('mouseenter', () => { tooltip.classList.add('show'); }); element.addEventListener('mouseleave', () => { tooltip.classList.remove('show'); }); } else if (cfg.transMode === 'bilingual') { // 双语模式:原文和译文交替显示 element.classList.add('ling-bilingual'); const original = document.createElement('span'); original.className = 'original'; original.textContent = element.textContent; const translation = document.createElement('div'); translation.className = 'translation'; translation.textContent = Array.isArray(transText) ? transText.join('\n') : transText; translation.style.whiteSpace = 'pre-wrap'; translation.style.fontFamily = fontFamily; translation.style.fontSize = fontSize; translation.style.lineHeight = lineHeight; element.innerHTML = ''; element.appendChild(original); element.appendChild(translation); } else { // 默认下方显示模式 const container = document.createElement('div'); container.className = (platform === 'discord' || platform === 'patreon' || platform === 'kofi') ? 'ling-trans-box ling-discord-box' : 'ling-trans-box'; appendTranslationContent(container, transText); container.style.backgroundColor = 'rgba(255,255,255,0.9)'; container.style.fontFamily = fontFamily; container.style.fontSize = fontSize; container.style.lineHeight = lineHeight; const isXMessage = element.getAttribute('data-testid')?.startsWith('message-text-'); const isKofiComment = platform === 'kofi' && element.classList.contains('kfds-top-mrgn-8'); if (((platform === 'twitter' && !isXMessage) || platform === 'patreon' || platform === 'kofi') && !isKofiComment) { element.insertAdjacentElement('afterend', container); } else { element.appendChild(container); } } } function toggleDashboard() { let dashboard = document.querySelector('.ling-dashboard'); if (!dashboard) { initDashboard(); dashboard = document.querySelector('.ling-dashboard'); } if (dashboard.classList.contains('active')) { dashboard.classList.remove('active'); setTimeout(() => { dashboard.style.display = 'none'; }, 200); } else { dashboard.style.display = 'block'; void dashboard.offsetWidth; setTimeout(() => { dashboard.classList.add('active'); }, 10); } } function initDashboard() { const cfg = Storage.getConfig(); const modeText = cfg.transMode === 'below' ? '下方显示' : cfg.transMode === 'hover' ? '悬浮显示' : '双语模式'; const div = document.createElement('div'); div.className = 'ling-dashboard'; div.innerHTML = `