// ==UserScript== // @name Auto Translate Pro (Ultimate) // @namespace auto-translate-pro-ultimate // @version 3.0.0 // @description 极致性能的网页翻译:自动检测、双语对照、无缝替换、多引擎支持、移动端适配 // @match *://*/* // @exclude *://translate.google.*/* // @exclude *://fanyi.baidu.com/* // @exclude *://transmart.qq.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect translate.googleapis.com // @connect api-edge.cognitive.microsofttranslator.com // @connect edge.microsoft.com // @connect transmart.qq.com // @connect www2.deepl.com // @connect translate.alibaba.com // @connect m.youdao.com // @connect fanyi.baidu.com // @run-at document-end // @downloadURL none // ==/UserScript== (async () => { 'use strict'; // 屏蔽 XML 等非网页结构 if (document.contentType === 'application/xml') return; /* ========================================== * 1. 核心配置与状态管理 * ========================================== */ const deviceLang = (navigator.language || 'zh-CN').split('-')[0].toLowerCase(); let config = { engine: await GM_getValue('at_engine', 'microsoft'), lang: await GM_getValue('at_lang', deviceLang), auto: await GM_getValue('at_auto', true), bilingual: await GM_getValue('at_bilingual', false), hlColor: await GM_getValue('at_hlColor', '#4a9eff') }; const saveConfig = (key, val) => { config[key] = val; GM_setValue('at_' + key, val); }; let state = { translating: false, translatedCount: 0 }; /* ========================================== * 2. 极速缓存 (LRU Cache) - 避免重复请求 * ========================================== */ const Cache = new Map(); const CACHE_MAX = 5000; const getCache = (text) => Cache.get(text); const setCache = (text, res) => { if (Cache.size >= CACHE_MAX) Cache.delete(Cache.keys().next().value); Cache.set(text, res); }; /* ========================================== * 3. 底层工具箱 * ========================================== */ const fetchAPI = (opts) => new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...opts, timeout: 15000, onload: resolve, onerror: reject, ontimeout: reject }); }); const uuid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); /* ========================================== * 4. 翻译引擎矩阵 (全懒加载设计) * ========================================== */ const LC = { zh:'zh', en:'en', ja:'ja', ko:'ko', fr:'fr', de:'de', es:'es', ru:'ru', it:'it', tr:'tr' }; const Engines = { // [微软] - 默认霸主,支持完美批量 microsoft: { token: null, expire: 0, async auth() { if (this.token && Date.now() < this.expire) return; const r = await fetchAPI({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth' }); this.token = r.responseText; this.expire = Date.now() + 8 * 60 * 1000; // 8分钟缓存 }, async batch(texts, lang) { await this.auth(); const to = lang === 'zh' ? 'zh-Hans' : (LC[lang] || lang); const out = new Array(texts.length).fill(null); // 微软最大支持25条/批,大幅降低并发风暴 for (let i = 0; i < texts.length; i += 25) { const chunk = texts.slice(i, i + 25); try { const r = await fetchAPI({ method: 'POST', url: `https://api-edge.cognitive.microsofttranslator.com/translate?from=&to=${to}&api-version=3.0`, headers: { 'authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' }, data: JSON.stringify(chunk.map(Text => ({ Text }))), }); if(r.status === 200) { JSON.parse(r.responseText).forEach((res, j) => { out[i + j] = res.translations[0].text; }); } } catch (e) { console.warn('MS Batch Fail:', e); } } return out; } }, // [谷歌] - 稳定免费 google: { async translate(text, lang) { const to = lang === 'zh' ? 'zh-CN' : (LC[lang] || lang); const r = await fetchAPI({ method: 'GET', url: `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${to}&q=${encodeURIComponent(text)}` }); return JSON.parse(r.responseText)[0].map(s => s[0]).join(''); } }, // [腾讯] - 交互翻译,适合长难句 tencent: { client_key: null, async translate(text, lang) { this.client_key = this.client_key || `browser-chrome-120.0-os-${uuid()}-${Date.now()}`; const r = await fetchAPI({ method: 'POST', url: 'https://transmart.qq.com/api/imt', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ header: { fn: 'auto_translation', client_key: this.client_key }, type: 'plain', model_category: 'normal', text_domain: 'general', source: { lang: 'auto', text_list: [text] }, target: { lang: LC[lang] || lang } }) }); return JSON.parse(r.responseText).auto_translation[0]; } }, // [DeepL] - 质量天花板 (逆向ID算法) deepl: { async translate(text, lang) { const to = lang.toUpperCase(); const id = 1e4 * Math.round(1e4 * Math.random()); const iCount = (text.match(/[i]/g) || []).length + 1; const body = JSON.stringify({ jsonrpc: '2.0', method: 'LMT_handle_jobs', id, params: { jobs: [{ kind: 'default', sentences: [{ text, id: 0, prefix: '' }], raw_en_context_before: [], raw_en_context_after: [], preferred_num_beams: 4 }], lang: { preference: { weight: {}, default: 'default' }, source_lang_user_selected: 'auto', target_lang: to }, commonJobParams: { mode: 'translate', browserType: 1 }, timestamp: Date.now() + (iCount - Date.now() % iCount) } }).replace('hod":"', ((id + 3) % 13 === 0 || (id + 5) % 29 === 0) ? 'hod" : "' : 'hod": "'); const r = await fetchAPI({ method: 'POST', url: 'https://www2.deepl.com/jsonrpc?method=LMT_handle_jobs', headers: { 'Content-Type': 'application/json', 'Origin': 'https://www.deepl.com' }, data: body }); return JSON.parse(r.responseText).result.translations[0].beams[0].sentences[0].text; } }, // [阿里] - CSRF 鉴权 alibaba: { token: null, async auth() { if (this.token) return; const r = await fetchAPI({ method: 'GET', url: 'https://translate.alibaba.com/api/translate/csrftoken' }); this.token = JSON.parse(r.responseText).token; }, async translate(text, lang) { await this.auth(); const b = '----WebKitFormBoundary' + uuid().slice(0, 16); const r = await fetchAPI({ method: 'POST', url: 'https://translate.alibaba.com/api/translate/text', headers: { 'content-type': `multipart/form-data; boundary=${b}`, 'origin': 'https://translate.alibaba.com' }, data: `--${b}\r\nContent-Disposition: form-data; name="srcLang"\r\n\r\nauto\r\n` + `--${b}\r\nContent-Disposition: form-data; name="tgtLang"\r\n\r\n${LC[lang] || lang}\r\n` + `--${b}\r\nContent-Disposition: form-data; name="domain"\r\n\r\ngeneral\r\n` + `--${b}\r\nContent-Disposition: form-data; name="query"\r\n\r\n${text}\r\n` + `--${b}\r\nContent-Disposition: form-data; name="_csrf"\r\n\r\n${this.token}\r\n--${b}--\r\n` }); return JSON.parse(r.responseText).data.translateText; } }, // [百度手机版] - GTK 签名算法 baidu: { token: null, gtk: null, async auth() { if (this.token && this.gtk) return; const r = await fetchAPI({ method: 'GET', url: 'https://fanyi.baidu.com', headers: { 'user-agent': 'Mozilla/5.0 (Linux; Android 10)' }}); this.token = /token:\s*['"]([^'"]+)/.exec(r.responseText)?.[1]; this.gtk = /['"](\d{6}\.\d{9})['"]/.exec(r.responseText)?.[1]; }, sign(t, gtk) { const S = (a, b) => { for (let c = 0; c < b.length - 2; c += 3) { let d = b.charAt(c+2); d = d >= 'a' ? d.charCodeAt(0)-87 : +d; d = b.charAt(c+1) === '+' ? a >>> d : a << d; a = b.charAt(c) === '+' ? a+d & 4294967295 : a ^ d; } return a; }; const [f, m] = gtk.split('.').map(Number); const bytes = []; for (let i = 0; i < t.length; i++) { let c = t.charCodeAt(i); if (c < 128) bytes.push(c); else if (c < 2048) bytes.push(c>>6|192, c&63|128); else bytes.push(c>>12|224, c>>6&63|128, c&63|128); } let v = f; for (let b of bytes) v = S(v+b, '+-a^+6'); v = S(v, '+-3^+b+-f') ^ m; if (v < 0) v = (v & 2147483647) + 2147483648; v %= 1e6; return `${v}.${v^f}`; }, async translate(text, lang) { await this.auth(); const to = LC[lang] || lang; const r = await fetchAPI({ method: 'POST', url: 'https://fanyi.baidu.com/basetrans', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://fanyi.baidu.com/' }, data: `query=${encodeURIComponent(text)}&from=auto&to=${to}&token=${this.token}&sign=${this.sign(text, this.gtk)}` }); return JSON.parse(r.responseText).trans[0].dst; } } }; /* ========================================== * 5. 高效 DOM 拦截与处理 * ========================================== */ const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE', 'SVG', 'NOSCRIPT', 'IFRAME', 'TEXTAREA', 'INPUT']); const SKIP_CLASSES = /translate-ui|notranslate|katex|mathjax|prism|hljs/i; // 智能语言侦测(极速版) const isTargetLang = (text) => { if(text.length < 2 || /^[\d\s.,!?]+$/.test(text)) return true; // 纯数字符号跳过 if(config.lang === 'zh') return (text.match(/[\u4e00-\u9fa5]/g)?.length || 0) > text.length * 0.3; if(config.lang === 'en') return /^[\x20-\x7E\s]+$/.test(text); return false; }; // TreeWalker:O(n) 复杂度极速遍历,绝不栈溢出 const collectTexts = (root) => { const nodes = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { const parent = node.parentElement; if (!parent || parent.hasAttribute('data-at-done') || SKIP_TAGS.has(parent.tagName) || SKIP_CLASSES.test(parent.className)) { return NodeFilter.FILTER_REJECT; } const text = node.nodeValue.trim(); if (!text || isTargetLang(text)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); while (walker.nextNode()) nodes.push(walker.currentNode); return nodes; }; /* ========================================== * 6. 核心翻译调度器 (Pipeline) * ========================================== */ const applyToDOM = (node, original, translated) => { if (!node.parentElement || node.parentElement.hasAttribute('data-at-done')) return; const wrapper = document.createElement('span'); wrapper.setAttribute('data-at-done', '1'); wrapper.setAttribute('data-at-ori', original); // 极致安全的排版方案:不破坏 flex/grid,不强行换行 if (config.bilingual) { wrapper.className = 'at-bilingual-wrap'; const oriSpan = document.createElement('span'); oriSpan.className = 'at-ori'; oriSpan.textContent = original; const transSpan = document.createElement('span'); transSpan.className = 'at-trans'; transSpan.textContent = translated; wrapper.append(oriSpan, transSpan); } else { wrapper.textContent = translated; } node.replaceWith(wrapper); state.translatedCount++; }; const processTranslation = async (rootNode = document.body) => { if (state.translating) return; state.translating = true; updateUI(); const nodes = collectTexts(rootNode); if (!nodes.length) { state.translating = false; updateUI(); return; } const engine = Engines[config.engine]; const texts = nodes.map(n => n.nodeValue.trim()); let results = new Array(texts.length).fill(null); // 1. 缓存拦截 const uncachedIdx = []; const uncachedTxt = []; texts.forEach((t, i) => { const cached = getCache(t); if (cached) results[i] = cached; else { uncachedIdx.push(i); uncachedTxt.push(t); } }); // 2. 引擎调度 (批量或并发) if (uncachedTxt.length > 0) { if (engine.batch) { const batchRes = await engine.batch(uncachedTxt, config.lang); batchRes.forEach((res, i) => { if (res) { results[uncachedIdx[i]] = res; setCache(uncachedTxt[i], res); } }); } else { // 限制并发量为 5,防止触发 API 频控 for (let i = 0; i < uncachedTxt.length; i += 5) { const chunk = uncachedTxt.slice(i, i + 5); const ps = chunk.map(async (t, j) => { try { const res = await engine.translate(t, config.lang); if (res) { results[uncachedIdx[i+j]] = res; setCache(t, res); } } catch(e) { console.error('Tr Fail:', e); } }); await Promise.allSettled(ps); } } } // 3. 渲染至 DOM nodes.forEach((node, i) => { if (results[i] && results[i] !== texts[i]) { applyToDOM(node, texts[i], results[i]); } }); state.translating = false; updateUI(); }; const restoreTranslation = () => { document.querySelectorAll('[data-at-done="1"]').forEach(wrapper => { const originalText = wrapper.getAttribute('data-at-ori'); if (originalText) { wrapper.replaceWith(document.createTextNode(originalText)); } }); state.translatedCount = 0; updateUI(); }; /* ========================================== * 7. 动态感知 (MutationObserver 防抖) * ========================================== */ let mutTimer = null; const observer = new MutationObserver(mutations => { if (!config.auto || state.translating) return; clearTimeout(mutTimer); mutTimer = setTimeout(() => { const hasNewNodes = mutations.some(m => Array.from(m.addedNodes).some(n => n.nodeType === 1 || n.nodeType === 3)); if (hasNewNodes) processTranslation(); }, 800); }); /* ========================================== * 8. 极简悬浮 UI 与样式 * ========================================== */ GM_addStyle(` /* 高级内联排版防破坏 */ .at-bilingual-wrap { display: inline; } .at-ori { display: inline; opacity: 0.5; margin-right: 6px; font-size: 0.95em; } .at-trans { display: inline; color: var(--at-hl-color, #4a9eff); font-weight: 500; } /* UI 组件 */ .at-ui-container { position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; font-family: system-ui, sans-serif; } .at-fab { width: 44px; height: 44px; border-radius: 50%; background: rgba(0,0,0,0.6); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; border: none; backdrop-filter: blur(8px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: all 0.2s; outline: none; } .at-fab:hover { transform: scale(1.05); } .at-fab.active { background: #4a9eff; } .at-fab.busy { animation: at-pulse 1.5s infinite; } @keyframes at-pulse { 0% { box-shadow: 0 0 0 0 rgba(74,158,255,0.6); } 70% { box-shadow: 0 0 0 10px rgba(74,158,255,0); } 100% { box-shadow: 0 0 0 0 rgba(74,158,255,0); } } .at-panel { position: absolute; bottom: 56px; right: 0; width: 220px; background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); border-radius: 12px; padding: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); opacity: 0; pointer-events: none; transform: translateY(10px); transition: all 0.2s; } .at-panel.open { opacity: 1; pointer-events: auto; transform: translateY(0); } .at-panel label { font-size: 12px; color: #666; font-weight: bold; display: block; margin: 8px 0 4px; } .at-panel select { width: 100%; padding: 6px; border-radius: 6px; border: 1px solid #ddd; outline: none; font-size: 13px; } .at-row { display: flex; justify-content: space-between; align-items: center; margin: 12px 0; } .at-toggle { width: 40px; height: 22px; background: #ccc; border-radius: 12px; position: relative; cursor: pointer; transition: 0.2s; } .at-toggle.on { background: #4a9eff; } .at-toggle::after { content: ''; position: absolute; width: 18px; height: 18px; background: #fff; border-radius: 50%; top: 2px; left: 2px; transition: 0.2s; } .at-toggle.on::after { left: 20px; } .at-actions { display: flex; gap: 8px; } .at-btn { flex: 1; padding: 8px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: 0.2s; font-size: 13px; } .at-btn-primary { background: #4a9eff; color: #fff; } .at-btn-default { background: #eee; color: #555; } .at-btn:active { transform: scale(0.95); } .at-stat { text-align: center; font-size: 11px; color: #999; margin-top: 8px; height: 14px; } @media (prefers-color-scheme: dark) { .at-panel { background: rgba(30,30,30,0.95); color: #eee; } .at-panel label { color: #aaa; } .at-panel select { background: #444; border-color: #555; color: #eee; } .at-btn-default { background: #444; color: #ccc; } } `); // 设置 CSS 变量控制高亮颜色 document.documentElement.style.setProperty('--at-hl-color', config.hlColor); const uiHtml = `