// ==UserScript== // @name 网页翻译——自动翻译为中文 // @namespace https://github.com/GeekScript/WebTranslator // @version 1.0 // @description 给每个非中文的网页右下角(可以调整到左下角)添加一个google翻译图标,该版本为中文翻译版本,只把外语翻译为中文 | 更多实用脚本👉关注公众号:【极客脚本库】 // @author 大角牛软件/u2222223(极客脚本库) // @match *://*/* // @connect translate.google.com // @connect translate.googleapis.com // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_notification // @grant GM_openInTab // @grant unsafeWindow // @license MIT // @noframes // ==/UserScript== (function () { 'use strict'; // --- Configuration & Constants --- const CONFIG = { excludeSites: [ /^(http|https).*((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/, /.*duyaoss\.com/, /.*lanzous\.com/, /.*w3school.*cn/, /.*iqiyi\.com/, /.*baidu.*/, /.*cnblogs\.com/, /.*csdn\.net/, /.*zhku\.edu\.cn/, /.*zhihuishu\.com/, /.*aliyuncs\.com/, /.*chaoxing\.com/, /.*youku\.com/, /.*examcoo\.com/, /.*mooc\.com/, /.*bilibili\.com/, /.*qq\.com/, /.*yy\.com/, /.*huya\.com/, /localhost/, /.*acfun\.cn/, /.*eleme\.cn/, /.*douyin\.com/ ], noTranslateSelectors: [ '.bbCodeCode', 'tt', 'pre[translate="no"]', 'pre', '.post_spoiler_show', '.c-article-section__content sub', '.c-article-section__content sup', '.c-article-equation', '.mathjax-tex' ], specialSiteFixes: [ { domain: 'curseforge.com', style: 'html { height: auto!important; }' }, { domain: 'gatesnotes.com', style: '.TGN_site { z-index: 0!important; }' } ] }; const LOGGER = { info: (msg) => { }, // Quiet warn: (msg) => console.warn(`⚠️ [网页翻译] ${msg}`), error: (msg) => console.error(`❌ [网页翻译] ${msg}`), promo: () => { } // Quiet }; // --- Trusted Types Policy (CSP Support) --- let policy; if (window.trustedTypes && window.trustedTypes.createPolicy) { try { policy = window.trustedTypes.createPolicy('geek_script_policy', { createHTML: (string) => string }); } catch (e) { LOGGER.warn('无法创建 Trusted Types 策略: ' + e); } } const getTrustedHTML = (html) => policy ? policy.createHTML(html) : html; // --- UI & Modal Logic (Spec Compliant) --- function showContactModal(isFirstRun = false) { const oldModal = document.getElementById('geek-modal'); if (oldModal) oldModal.remove(); GM_addStyle(` #geek-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 999999999; display: flex; justify-content: center; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; } .geek-modal-content { background: #fff; padding: 25px; border-radius: 12px; width: 380px; max-width: 90vw; box-shadow: 0 4px 20px rgba(0,0,0,0.2); text-align: center; position: relative; animation: geek-fadein 0.3s ease-out; } .geek-modal-title { font-size: 20px; font-weight: bold; color: #333; margin-bottom: 10px; } .geek-modal-subtitle { font-size: 14px; color: #666; margin-bottom: 15px; } .geek-modal-tip-box { background: #fff8e1; border: 1px solid #ffe0b2; border-radius: 8px; padding: 12px; margin: 15px 0; text-align: left; } .geek-modal-text { font-size: 14px; line-height: 1.6; color: #333; margin-bottom: 5px; } .geek-highlight { color: #e67e22; font-weight: bold; } .geek-footer-tip { font-size: 12px; color: #999; margin-top: 10px; padding-top: 8px; border-top: 1px dashed #ffe0b2; } .geek-qr-container { margin: 15px 0; position: relative; min-height: 150px; } .geek-qr-img { width: 200px; height: 200px; border-radius: 8px; } .geek-error-box { display: none; border: 1px solid #ffcdd2; background: #ffebee; color: #c62828; padding: 10px; border-radius: 8px; font-size: 13px; } .geek-btn { display: block; width: 100%; padding: 10px 0; margin-top: 15px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } .geek-btn-primary { background: #2196f3; color: white; } .geek-btn-primary:hover { background: #1976d2; } .geek-btn-secondary { background: transparent; color: #999; } .geek-btn-secondary:hover { color: #666; } @keyframes geek-fadein { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } `); const modal = document.createElement('div'); modal.id = 'geek-modal'; let title = isFirstRun ? '🚀 脚本运行成功' : '加入油猴爱好者群 / 反馈BUG'; let subtitle = isFirstRun ? '
本弹窗仅首次显示,后续不再打扰
' : ''; let tipBoxContent = isFirstRun ? `
🔥 关注公众号【极客脚本库】
回复 “交流群” 加入油猴爱好者群 & 获取更多脚本
` : `
关注公众号【极客脚本库】
回复 “交流群” 获取群号 & 反馈BUG
`; const btnClass = isFirstRun ? 'geek-btn-secondary' : 'geek-btn-primary'; const btnText = isFirstRun ? '我知道了' : '关闭'; modal.innerHTML = getTrustedHTML(`
${title}
${subtitle}
公众号二维码
⚠️ 图片加载失败,请手动搜索关注公众号:极客脚本库,回复“交流群”即可
${tipBoxContent}
`); document.body.appendChild(modal); modal.querySelector('#geek-close-btn').onclick = () => modal.remove(); modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; const img = modal.querySelector('.geek-qr-img'); img.onerror = () => { img.style.display = 'none'; modal.querySelector('.geek-error-box').style.display = 'block'; }; } // --- Main Logic Class --- class WebTranslator { constructor() { this.menuIds = []; this.settings = { position: GM_getValue('position', true), // true=left, false=right autoCheck: GM_getValue('autoCheck', true), showTip: GM_getValue('showTip', false), hotkey: GM_getValue('hotkey', []) }; this.init(); } init() { // Exclude check const currentUrl = window.location.href; LOGGER.info(`检查当前网址: ${currentUrl}`); if (CONFIG.excludeSites.some(regex => { const match = regex.test(currentUrl); if (match) LOGGER.warn(`当前网站在排除列表中: ${regex}`); return match; })) return; // Language check LOGGER.info('开始检查语言环境...'); if (!this.shouldEnable()) { LOGGER.info('当前网页被判定为无需翻译,停止运行'); return; } LOGGER.info('环境检查通过,准备启动'); LOGGER.promo(); this.registerMenu(); this.checkFirstRun(); this.injectStyles(); this.injectGoogleTranslate(); this.setupHotkeys(); this.applySiteFixes(); } shouldEnable() { const lang = document.documentElement.lang; const mainLang = document.characterSet.toLowerCase(); const pageTitle = document.title; LOGGER.info(`页面信息: lang=[${lang}], charset=[${mainLang}], title=[${pageTitle}]`); const isChinese = ( (lang && lang.startsWith('zh')) || (mainLang && mainLang.startsWith('gb')) || /[\u4E00-\u9FFF]/.test(pageTitle) ); if (isChinese) { LOGGER.info(`判定结果: 中文网站 (Reason: ${lang && lang.startsWith('zh') ? 'lang=zh' : (mainLang && mainLang.startsWith('gb') ? 'charset=gb' : 'title包含中文')})`); } else { LOGGER.info('判定结果: 非中文网站,允许翻译'); } return !isChinese; } checkFirstRun() { if (!GM_getValue('has_run_v1', false)) { setTimeout(() => { showContactModal(true); GM_setValue('has_run_v1', true); }, 2000); } } registerMenu() { // Contact & Bug Report (Spec Requirement) try { if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand("💬 反馈BUG & 加入油猴爱好者群", () => showContactModal(false)); } } catch (e) { LOGGER.warn('菜单注册失败: ' + e); } // Settings Menu const menus = [ { name: '悬浮球', key: 'position', value: this.settings.position, tip: { open: '⬅️ 居左', close: '➡️ 居右' }, callback: () => this.toggleSetting('position', () => this.updateButtonPosition()) }, { name: '悬停显示原文', key: 'showTip', value: this.settings.showTip, tip: { open: '✅ 开启', close: '⬜ 关闭' }, callback: () => this.toggleSetting('showTip', () => this.updateTipDisplay()) }, { name: '设置快捷键', key: 'hotkey', value: false, tip: { open: '', close: '' }, callback: () => this.showHotkeySettings() } ]; // Re-register helper this.updateMenus = () => { this.menuIds.forEach(id => GM_unregisterMenuCommand(id)); this.menuIds = []; // Re-add Contact GM_registerMenuCommand("💬 反馈BUG & 加入油猴爱好者群", () => showContactModal(false)); menus.forEach(m => { m.value = this.settings[m.key]; // Update local value ref const title = `${m.tip ? (m.value ? m.tip.open : m.tip.close) : ''} ${m.name}`; const id = GM_registerMenuCommand(title, m.callback); this.menuIds.push(id); }); }; // Initial register this.updateMenus(); } toggleSetting(key, callback) { this.settings[key] = !this.settings[key]; GM_setValue(key, this.settings[key]); GM_notification({ text: `已${this.settings[key] ? '开启' : '关闭'} [${key}]`, timeout: 1000 }); if (callback) callback(); this.updateMenus(); } injectStyles() { // General Styles GM_addStyle(` .geek-trans-btn-container { position: fixed; bottom: 50px; z-index: 2147483647; display: flex; align-items: center; gap: 10px; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 10px 14px; border-radius: 50px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); border: 1px solid rgba(255,255,255,0.5); cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); user-select: none; } .geek-trans-btn-container:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.25); background: #fff; } .geek-switch-label { font-size: 16px; font-weight: bold; color: #555; font-family: system-ui, -apple-system, sans-serif; } .geek-switch { width: 44px; height: 24px; background: #e0e0e0; border-radius: 20px; position: relative; transition: background 0.3s ease; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); } .geek-switch-on { background: #2196f3; } .geek-switch-handle { width: 20px; height: 20px; background: #fff; border-radius: 50%; position: absolute; top: 2px; left: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); } .geek-switch-on .geek-switch-handle { transform: translateX(20px); } /* Hide Google's default bar */ .skiptranslate iframe, .goog-te-banner-frame { display: none !important; } body { top: 0 !important; } #google_translate_element { display: none; } `); this.updateButtonPosition(); this.updateTipDisplay(); } updateButtonPosition() { const isLeft = this.settings.position; const styleId = 'geek-trans-pos-style'; const oldStyle = document.getElementById(styleId); if (oldStyle) oldStyle.remove(); const css = ` .geek-trans-btn-container { ${isLeft ? 'left: 20px;' : 'right: 20px;'} } `; const style = document.createElement('style'); style.id = styleId; style.innerHTML = getTrustedHTML(css); document.head.appendChild(style); } updateTipDisplay() { const show = this.settings.showTip; const styleId = 'geek-trans-tip-style'; const oldStyle = document.getElementById(styleId); if (oldStyle) oldStyle.remove(); let css = ''; if (show) { // 开启:保留高亮,允许显示 Tooltip (不添加隐藏样式) css = ` .goog-text-highlight { background-color: #c9d7f1 !important; box-shadow: 2px 2px 4px #99a !important; } `; } else { // 关闭:隐藏 Tooltip 和去除高亮 css = ` #goog-gt-tt, .goog-te-balloon-frame, .goog-tooltip, .goog-tooltip-active { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } .goog-text-highlight { background: none !important; box-shadow: none !important; border-bottom: none !important; } `; } const style = document.createElement('style'); style.id = styleId; style.innerHTML = getTrustedHTML(css); document.head.appendChild(style); } injectGoogleTranslate() { LOGGER.info('开始注入 Google 翻译脚本...'); // 强制清除 Google 翻译的 Cookie,防止自动翻译 this.clearTranslateCookie(); // 注入初始化函数到页面环境中 (完全模仿源文件,避免沙箱问题) const initScriptContent = ` function googleTranslateElementInit() { new google.translate.TranslateElement({ pageLanguage: 'auto', includedLanguages: 'zh-CN', layout: /mobile/i.test(navigator.userAgent) ? 0 : 2, autoDisplay: false }, 'google_translate_element'); } `; const initScript = document.createElement('script'); initScript.innerHTML = getTrustedHTML(initScriptContent); document.head.appendChild(initScript); // Container const div = document.createElement('div'); div.id = 'google_translate_element'; div.style.display = 'none'; // 保持隐藏 document.body.appendChild(div); // Inject Google Script const script = document.createElement('script'); script.src = 'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit'; script.onload = () => { LOGGER.info('Google 翻译脚本加载完成 (onload)'); // 脚本加载完成后,我们可以尝试创建按钮 // 注意:这里不需要等待 googleTranslateElementInit 执行,因为按钮点击时才去查找元素 this.createTranslateButtons(); }; script.onerror = () => { LOGGER.error('Google翻译脚本加载失败,请检查网络或代理设置'); }; document.head.appendChild(script); } clearTranslateCookie() { const domain = window.location.hostname; const domains = domain.split('.'); const cookieName = 'googtrans'; // 清除当前域 document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`; // 清除父域 while (domains.length > 1) { const d = domains.join('.'); document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${d}`; domains.shift(); } } createTranslateButtons() { // LOGGER.info('正在创建翻译按钮 UI...'); const oldContainer = document.querySelector('.geek-trans-btn-container'); if (oldContainer) oldContainer.remove(); const container = document.createElement('div'); container.className = 'geek-trans-btn-container notranslate'; const label = document.createElement('span'); label.className = 'geek-switch-label'; label.textContent = '中文'; const switchBg = document.createElement('div'); switchBg.className = 'geek-switch'; const handle = document.createElement('div'); handle.className = 'geek-switch-handle'; switchBg.appendChild(handle); container.appendChild(label); container.appendChild(switchBg); // Initial State Check if (document.documentElement.classList.contains('translated-ltr')) { switchBg.classList.add('geek-switch-on'); } this.switchBg = switchBg; // Store ref container.onclick = () => { const isOn = switchBg.classList.contains('geek-switch-on'); if (isOn) { switchBg.classList.remove('geek-switch-on'); this.doRecover(); } else { switchBg.classList.add('geek-switch-on'); this.doTranslate(); } }; document.body.appendChild(container); } doTranslate() { LOGGER.info('尝试执行翻译...'); // 如果已经在处理中,避免重复 if (this.isTranslating) return; this.isTranslating = true; const maxRetries = 20; // 6 seconds max let retries = 0; const attemptTranslate = () => { LOGGER.info(`查找翻译组件 (尝试 ${retries + 1}/${maxRetries})...`); // 1. Mobile / Simple Layout (Select Element) const combo = document.querySelector('.goog-te-combo'); if (combo) { LOGGER.info('找到翻译下拉框 (.goog-te-combo),正在切换语言...'); combo.value = 'zh-CN'; combo.dispatchEvent(new Event('change')); combo.dispatchEvent(new Event('input')); this.isTranslating = false; return; } // 2. PC / Iframe Layout // 查找可能的 iframe const frames = [ document.querySelector('.goog-te-menu-frame'), document.querySelector('iframe[title="语言翻译微件"]'), document.querySelector('iframe[title="Language Translate Widget"]') ]; let foundIframe = null; for (let f of frames) { if (f) { foundIframe = f; break; } } if (foundIframe) { try { const doc = foundIframe.contentDocument || foundIframe.contentWindow.document; const links = doc.querySelectorAll('a, span.text, div.text'); let clicked = false; for (let link of links) { const text = link.textContent || link.innerText; if (text.includes('Chinese') || text.includes('中文') || text.includes('简体中文')) { LOGGER.info('在 iframe 中找到中文选项,正在点击...'); link.click(); clicked = true; break; } } if (!clicked) { // Fallback: click the second link (usually Simplified Chinese in sorted lists if detected) // 源文件逻辑: document.querySelectorAll('table a')[1] const tableLinks = doc.querySelectorAll('table a'); if (tableLinks.length > 1) { LOGGER.info('点击 iframe 表格中的第二个链接 (Fallback)...'); tableLinks[1].click(); clicked = true; } } if (clicked) { this.isTranslating = false; return; } } catch (e) { LOGGER.warn('访问 iframe 内容出错: ' + e); } } // Retry logic retries++; if (retries < maxRetries) { setTimeout(attemptTranslate, 300); } else { LOGGER.error('超时:未找到可用的翻译组件。'); this.isTranslating = false; GM_notification({ text: '未找到翻译组件,可能是 Google 翻译加载失败', timeout: 3000 }); } }; attemptTranslate(); } doRecover() { // Check for Mobile iframe or PC iframe const frames = [ document.getElementById(':1.container'), document.getElementById(':2.container'), document.querySelector('.goog-te-banner-frame') ]; let found = false; for (let iframe of frames) { if (iframe) { try { const doc = iframe.contentDocument || iframe.contentWindow.document; // Mobile often uses :1.restore, PC often :2.restore // Or general ID containing 'restore' const btn = doc.getElementById(':1.restore') || doc.getElementById(':2.restore') || doc.querySelector('[id*="restore"]') || doc.querySelector('button'); // Sometimes it's just a button? No, be specific. if (btn) { LOGGER.info('找到恢复按钮,正在点击...'); btn.click(); found = true; return; } } catch (e) { LOGGER.warn('尝试在 iframe 中查找恢复按钮失败: ' + e); } } } if (!found) { LOGGER.warn('未找到恢复按钮,尝试备用恢复方法 (iframe 之外)...'); // 某些情况下,restore 按钮可能在主页面(极少见) // 或者我们可以尝试触发 google 的关闭事件 // 再次尝试清空 cookie 并刷新页面? 不,这太激进了。 // 提示用户手动刷新 GM_notification({ text: '恢复失败,请刷新页面', timeout: 2000 }); } } setupHotkeys() { document.addEventListener('keydown', (e) => { const savedKeys = this.settings.hotkey; if (!savedKeys || savedKeys.length === 0) return; // 避免在输入框中触发 if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable) return; const pressed = []; if (e.ctrlKey) pressed.push('Ctrl'); if (e.altKey) pressed.push('Alt'); if (e.shiftKey) pressed.push('Shift'); if (e.metaKey) pressed.push('Meta'); if (!['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) { pressed.push(e.key.toUpperCase()); } // 比较按键组合 if (pressed.length > 0 && pressed.join('+') === savedKeys.join('+')) { e.preventDefault(); // 阻止默认行为 // 判断当前状态 const isTranslated = document.documentElement.classList.contains('translated-ltr'); if (isTranslated) { this.doRecover(); if (this.switchBg) this.switchBg.classList.remove('geek-switch-on'); } else { this.doTranslate(); if (this.switchBg) this.switchBg.classList.add('geek-switch-on'); } } }); } showHotkeySettings() { const oldModal = document.getElementById('geek-hotkey-modal'); if (oldModal) oldModal.remove(); GM_addStyle(` #geek-hotkey-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 2147483647; display: flex; justify-content: center; align-items: center; font-family: system-ui, -apple-system, sans-serif; } .geek-hotkey-display { background: #f8f9fa; border: 2px dashed #ccc; border-radius: 8px; padding: 20px; margin: 20px 0; min-height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; color: #555; transition: all 0.2s; } .geek-hotkey-display.recording { border-color: #2196f3; background: #e3f2fd; color: #1976d2; } .geek-btn-group { display: flex; gap: 10px; justify-content: center; margin-top: 10px; } `); const modal = document.createElement('div'); modal.id = 'geek-hotkey-modal'; let currentKeys = [...this.settings.hotkey]; const displayKeys = currentKeys.length ? currentKeys.join(' + ') : '点击此处按下快捷键'; modal.innerHTML = getTrustedHTML(`
⌨️ 设置快捷键
请直接在键盘上按下您想设置的组合键
${displayKeys}
`); document.body.appendChild(modal); const display = modal.querySelector('#geek-hotkey-display'); let isRecording = false; // 激活录制状态 display.onclick = () => { isRecording = true; display.classList.add('recording'); display.textContent = '请按键...'; }; // 监听按键 const keyHandler = (e) => { if (!isRecording && document.activeElement !== display) { // 如果没点击录制,且当前不是在录制中,直接监听全局可能不太好,这里建议强制点击后录制 // 或者直接监听整个modal的keydown return; } // 只要modal存在,就拦截按键 e.preventDefault(); e.stopPropagation(); const keys = []; if (e.ctrlKey) keys.push('Ctrl'); if (e.altKey) keys.push('Alt'); if (e.shiftKey) keys.push('Shift'); if (e.metaKey) keys.push('Meta'); if (!['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) { keys.push(e.key.toUpperCase()); } if (keys.length > 0) { currentKeys = keys; display.textContent = keys.join(' + '); // 录制完成一个组合后,可以暂时保持录制状态或者结束 // 这里保持录制,直到用户点击保存 } }; // 绑定到 document,因为 div 无法直接 focus 接收 keydown (除非 tabIndex) // 为了体验,点击 display 后,我们设置一个标志位,拦截 document 的 keydown document.addEventListener('keydown', (e) => { if (document.getElementById('geek-hotkey-modal')) { keyHandler(e); } }); // 保存 modal.querySelector('#geek-hotkey-save').onclick = () => { this.settings.hotkey = currentKeys; GM_setValue('hotkey', currentKeys); GM_notification({ text: `快捷键已保存: ${currentKeys.join(' + ')}`, timeout: 2000 }); modal.remove(); this.updateMenus(); }; // 清除 modal.querySelector('#geek-hotkey-clear').onclick = () => { currentKeys = []; display.textContent = '未设置'; display.classList.remove('recording'); }; // 取消 modal.querySelector('#geek-hotkey-cancel').onclick = () => { modal.remove(); }; // 自动聚焦录制 display.click(); } applySiteFixes() { CONFIG.specialSiteFixes.forEach(fix => { if (window.location.hostname.includes(fix.domain)) { GM_addStyle(fix.style); } }); // Prevent translate on code blocks CONFIG.noTranslateSelectors.forEach(sel => { document.querySelectorAll(sel).forEach(el => el.classList.add('notranslate')); }); } } // Run new WebTranslator(); })();