// ==UserScript== // @name 微软自动翻译 // @namespace https://viayoo.com/ // @version 2.25.0 // @description 微软自动翻译小部件 | 自动翻译整个页面 | 简化界面 | 使用Ctrl+Shift+Y切换 | 支持颜色自定义 | 网站专用引擎 | 立即翻译 // @author You // @run-at document-start // @match https://*/* // @match http://*/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect api-edge.cognitive.microsofttranslator.com // @connect edge.microsoft.com // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9InVybCgjZ3JhZGllbnQpIiByeD0iMzIiLz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWRpZW50IiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDA2NmNjIi8+PHN0b3Agb2Zmc2V0PSI1MCUiIHN0b3AtY29sb3I9IiMwMDY2Y2MiLz48c3RvcCBvZmZzZXQ9IjUxJSIgc3RvcC1jb2xvcj0iI2ZmNDc1NyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmNDc1NyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0ZXh0IHg9IjUwJSIgeT0iMzglIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjgiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXdlaWdodD0iYm9sZCIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+TTwvdGV4dD48dGV4dCB4PSI1MCUiIHk9IjY4JSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjI4IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPlQ8L3RleHQ+PC9zdmc+ // ==/UserScript== (function() { 'use strict'; // 脚本配置 const CONFIG = { widgetId: 'msWidget', storageKey: 'microsoft_widget_enabled', whitelistKey: 'microsoft_whitelist', translationLanguageKey: 'translation_language', translationModeKey: 'translation_mode', translationColorKey: 'translation_color', translationBgColorKey: 'translation_bg_color', siteSpecificEnginesKey: 'site_specific_engines', showTranslationIndicatorKey: 'show_translation_indicator', defaultEnabled: true, hotkey: 'Ctrl+Shift+Y', ENGINES: { MICROSOFT: 'microsoft' }, MODES: { REPLACE: 'replace', DUAL: 'dual' }, defaultEngine: 'microsoft', defaultTargetLanguage: 'zh-Hans', defaultMode: 'replace', defaultTextColor: '#0066cc', defaultBgColor: 'rgba(0,102,204,0.1)', russianDetectionThreshold: 0.2, LANGUAGES: { 'zh-Hans': {code: 'zh-Hans', name: '简体中文', icon: '🇨🇳'}, 'en': {code: 'en', name: '英语', icon: '🇺🇸'}, 'ru': {code: 'ru', name: '俄语', icon: '🇷🇺'}, 'ja': {code: 'ja', name: '日语', icon: '🇯🇵'}, 'ko': {code: 'ko', name: '韩语', icon: '🇰🇷'}, 'fr': {code: 'fr', name: '法语', icon: '🇫🇷'}, 'de': {code: 'de', name: '德语', icon: '🇩🇪'}, 'es': {code: 'es', name: '西班牙语', icon: '🇪🇸'}, 'pt': {code: 'pt', name: '葡萄牙语', icon: '🇵🇹'}, 'ar': {code: 'ar', name: '阿拉伯语', icon: '🇸🇦'}, 'it': {code: 'it', name: '意大利语', icon: '🇮🇹'}, 'vi': {code: 'vi', name: '越南语', icon: '🇻🇳'}, 'th': {code: 'th', name: '泰语', icon: '🇹🇭'} }, COLOR_PRESETS: [ {name: '默认蓝色', value: '#0066cc', bg: 'rgba(0,102,204,0.1)'}, {name: '绿色', value: '#00b894', bg: 'rgba(0,184,148,0.1)'}, {name: '红色', value: '#ff4757', bg: 'rgba(255,71,87,0.1)'}, {name: '紫色', value: '#6c5ce7', bg: 'rgba(108,92,231,0.1)'}, {name: '橙色', value: '#ff9f43', bg: 'rgba(255,159,67,0.1)'}, {name: '深色', value: '#2d3436', bg: 'rgba(45,52,54,0.1)'}, {name: '粉色', value: '#fd79a8', bg: 'rgba(253,121,168,0.1)'}, {name: '青色', value: '#00cec9', bg: 'rgba(0,206,201,0.1)'} ], russianDomains: ['.ru', '.рф', '.su', '.by', '.kz', '.ua', '.com.ru'], russianKeywords: [ 'программы', 'андроид', 'скачать', 'русский', 'приложение', 'софт', 'игры', 'утилиты', 'бесплатно', 'android', 'программу', 'для', 'на', 'телефон', 'смартфон', 'установить', 'загрузить' ], russianSiteOptimizations: { 'programmy-dlya-android.ru': { textExtraction: 'enhanced', mergeShortTexts: true, prioritizeLongTexts: true, forceRussianSource: true, excludeSelectors: ['code', '.code', 'pre', '.pre', 'textarea', 'input'] }, 'rutracker.org': { textExtraction: 'simple', excludeSelectors: ['.post_body', '.sp-wrap', '.code'] } }, MAX_TEXT_LENGTH: 5000, BATCH_SIZE: 50, AUTO_TRANSLATE_DELAY: 100, DOM_CHANGE_DELAY: 800, SCROLL_CHECK_DELAY: 2000, MAX_RETRY_COUNT: 2, TRANSLATING_INDICATOR_DURATION: 3000, TRANSLATING_INDICATOR_FADE_OUT: 1000 }; // 脚本状态 let widgetEnabled = CONFIG.defaultEnabled; let isWhitelistedDomain = false; let mainMenuCommandId = null; let whitelistMenuCommandId = null; let settingsMenuCommandId = null; let isModalOpen = false; let cachedWhitelist = null; let currentDomain = ''; let isRussianSite = false; let isRussianContent = false; let currentRussianSiteConfig = null; let targetLanguage = CONFIG.defaultTargetLanguage; let translationMode = CONFIG.defaultMode; let translationTextColor = CONFIG.defaultTextColor; let translationBgColor = CONFIG.defaultBgColor; let siteSpecificEngines = {}; let microsoftAuthToken = null; let microsoftTranslatorActive = false; let pageTranslating = false; let translationCache = new Map(); let autoTranslateTimeout = null; let originalTextMap = new Map(); let domObserver = null; let domChangeTimer = null; let isObservingDOM = false; let scrollCheckTimer = null; let lastScrollY = 0; let scrollCheckCount = 0; let continuousTranslationTimer = null; let translationRetryCount = 0; let lastTranslationTime = 0; let isUsingSiteSpecificEngine = false; let siteSpecificEngineName = ''; let russianContentRatio = 0; let russianDetectionSamples = 0; let russianDetectionTimer = null; let isPageLoaded = false; let pageLoadedTimer = null; let immediateTranslationScheduled = false; let currentTranslatingIndicator = null; let indicatorHideTimeout = null; let isTranslatingIndicatorVisible = false; let currentIndicatorText = ''; let showTranslationIndicator = true; // 获取当前域名 function getCurrentDomain() { return location.hostname; } // 检测俄语字符 function containsRussianText(text) { const russianRegex = /[\u0400-\u04FF]/; return russianRegex.test(text); } // 计算文本中俄语字符的比例 function getRussianCharacterRatio(text) { if (!text || text.length === 0) return 0; const russianChars = text.match(/[\u0400-\u04FF]/g) || []; return russianChars.length / text.length; } // 增强的俄语内容检测 function detectRussianByContent() { if (!document.body) return false; try { // 获取页面主要内容的文本 const mainContentSelectors = [ 'main', 'article', '.content', '#content', '.main-content', '.post-content', '.entry-content', '.text', '.article', 'body' ]; let contentText = ''; for (const selector of mainContentSelectors) { const element = document.querySelector(selector); if (element && element.textContent && element.textContent.length > 100) { contentText = element.textContent; break; } } if (!contentText || contentText.length < 100) { contentText = document.body.textContent || ''; } const visibleText = contentText.replace(/\s+/g, ' ').trim(); if (visibleText.length < 50) return false; const russianRatio = getRussianCharacterRatio(visibleText); russianContentRatio = russianRatio; russianDetectionSamples++; console.log(`俄语内容检测: 字符数 ${visibleText.length}, 俄语比例 ${(russianRatio * 100).toFixed(1)}%, 样本数 ${russianDetectionSamples}`); if (russianRatio >= CONFIG.russianDetectionThreshold) { console.log(`检测到俄语内容: 俄语字符比例 ${(russianRatio * 100).toFixed(1)}%`); const textLower = visibleText.toLowerCase(); let russianKeywordCount = 0; for (const keyword of CONFIG.russianKeywords) { if (textLower.includes(keyword.toLowerCase())) { russianKeywordCount++; } } if (russianKeywordCount >= 2) { console.log(`检测到 ${russianKeywordCount} 个俄语关键词,确认为俄语网站`); return true; } if (russianRatio >= 0.25) { console.log(`俄语比例较高(${(russianRatio * 100).toFixed(1)}%),确认为俄语网站`); return true; } return false; } else { console.log(`俄语比例不足(${(russianRatio * 100).toFixed(1)}%),非俄语网站`); return false; } } catch (error) { console.error('检测俄语内容时出错:', error); return false; } } // 快速检测是否为俄语网站 function quickDetectRussianSite() { currentDomain = getCurrentDomain(); let isRussianDomain = false; for (const suffix of CONFIG.russianDomains) { if (currentDomain.includes(suffix)) { isRussianDomain = true; console.log(`检测到俄语域名后缀: ${suffix}`); break; } } const htmlLang = document.documentElement.lang; const isRussianLang = htmlLang && (htmlLang.startsWith('ru') || htmlLang.startsWith('RU')); if (isRussianLang) console.log('检测到HTML语言标签为俄语'); const metaLang = document.querySelector('meta[http-equiv="Content-Language"]'); const isRussianMeta = metaLang && (metaLang.content.startsWith('ru') || metaLang.content.startsWith('RU')); if (isRussianMeta) console.log('检测到meta语言标签为俄语'); return isRussianDomain || isRussianLang || isRussianMeta; } // 获取网站专用引擎配置 function getSiteSpecificEngines() { try { const enginesStr = GM_getValue(CONFIG.siteSpecificEnginesKey, '{}'); return JSON.parse(enginesStr); } catch (e) { console.error('读取网站专用引擎配置失败:', e); return {}; } } // 保存网站专用引擎配置 function saveSiteSpecificEngines(engines) { try { siteSpecificEngines = engines; GM_setValue(CONFIG.siteSpecificEnginesKey, JSON.stringify(engines)); } catch (e) { console.error('保存网站专用引擎配置失败:', e); } } // 检查当前网站是否有专用引擎(仅支持微软) function checkSiteSpecificEngine() { const domain = getCurrentDomain(); isUsingSiteSpecificEngine = false; siteSpecificEngineName = ''; if (siteSpecificEngines[domain]) { // 只支持微软引擎 if (siteSpecificEngines[domain] === CONFIG.ENGINES.MICROSOFT) { isUsingSiteSpecificEngine = true; siteSpecificEngineName = CONFIG.ENGINES.MICROSOFT; console.log(`当前网站有专用引擎: 微软`); return CONFIG.ENGINES.MICROSOFT; } else { // 如果保存的是其他引擎,清除掉 delete siteSpecificEngines[domain]; saveSiteSpecificEngines(siteSpecificEngines); } } return null; } // 为当前网站设置专用引擎(仅微软) function setSiteSpecificEngine(engine) { if (engine !== CONFIG.ENGINES.MICROSOFT) return; const domain = getCurrentDomain(); siteSpecificEngines[domain] = engine; saveSiteSpecificEngines(siteSpecificEngines); isUsingSiteSpecificEngine = true; siteSpecificEngineName = engine; console.log(`为网站 ${domain} 设置专用引擎: 微软`); } // 清除当前网站的专用引擎 function clearSiteSpecificEngine() { const domain = getCurrentDomain(); if (siteSpecificEngines[domain]) { delete siteSpecificEngines[domain]; saveSiteSpecificEngines(siteSpecificEngines); isUsingSiteSpecificEngine = false; siteSpecificEngineName = ''; console.log(`清除网站 ${domain} 的专用引擎`); } } // 获取当前应使用的引擎(始终微软) function getCurrentEngine() { return CONFIG.ENGINES.MICROSOFT; } // 智能引擎检测 function smartEngineDetection() { if (russianDetectionTimer) { clearTimeout(russianDetectionTimer); russianDetectionTimer = null; } russianDetectionTimer = setTimeout(() => { isRussianContent = detectRussianByContent(); console.log(`俄语内容检测结果: ${isRussianContent ? '是俄语网站' : '非俄语网站'}, 比例: ${(russianContentRatio * 100).toFixed(1)}%`); registerMenuCommands(); }, 1500); } // 获取俄语网站配置 function getRussianSiteConfig() { if (!isRussianSite) return null; const domain = getCurrentDomain(); for (const [sitePattern, config] of Object.entries(CONFIG.russianSiteOptimizations)) { if (domain.includes(sitePattern)) { console.log(`找到俄语网站优化配置: ${sitePattern}`); return config; } } return { textExtraction: 'enhanced', mergeShortTexts: true, prioritizeLongTexts: true, forceRussianSource: true, excludeSelectors: ['code', '.code', 'pre', '.pre', 'textarea', 'input', '.mce-content-body'] }; } // 获取白名单 function getWhitelist() { if (cachedWhitelist !== null) { return cachedWhitelist; } const whitelistStr = GM_getValue(CONFIG.whitelistKey, '[]'); try { cachedWhitelist = JSON.parse(whitelistStr); } catch (e) { cachedWhitelist = []; } return cachedWhitelist; } // 保存白名单 function saveWhitelist(whitelist) { cachedWhitelist = whitelist; GM_setValue(CONFIG.whitelistKey, JSON.stringify(whitelist)); } // 检查当前域名是否在白名单中 function checkWhitelist() { const currentDomain = getCurrentDomain(); const whitelist = getWhitelist(); isWhitelistedDomain = whitelist.includes(currentDomain); return isWhitelistedDomain; } // 切换当前域名的白名单状态 function toggleWhitelist() { const currentDomain = getCurrentDomain(); let whitelist = getWhitelist(); if (whitelist.includes(currentDomain)) { whitelist = whitelist.filter(domain => domain !== currentDomain); showStatusMessage(`已从白名单中移除: ${currentDomain}`, 2000); isWhitelistedDomain = false; } else { whitelist.push(currentDomain); showStatusMessage(`已添加到白名单: ${currentDomain}`, 2000); isWhitelistedDomain = true; } saveWhitelist(whitelist); if (widgetEnabled || isWhitelistedDomain) { console.log('白名单变更,立即更新翻译状态'); updateWidgetBasedOnSettings(); scheduleImmediateTranslation(); } else { deactivateAllTranslators(); } registerMenuCommands(); } // 立即翻译调度 function scheduleImmediateTranslation() { if (immediateTranslationScheduled) return; immediateTranslationScheduled = true; setTimeout(() => { immediateTranslationScheduled = false; if (microsoftTranslatorActive && !pageTranslating) { console.log('调度立即翻译'); translateWholePage(); } }, 100); } // 创建防护层,防止翻译脚本自身内容 function createTranslationProtectionLayer() { const style = document.createElement('style'); style.id = 'translation-protection'; style.setAttribute('data-no-translate', 'true'); style.setAttribute('translate', 'no'); style.className = 'no-translate script-element'; style.textContent = ` /* 强制保护脚本创建的UI元素 */ body * { transition: none !important; } /* 防护引擎选择器中的文本 */ .engine-btn, .engine-btn * { translate: no !important; -webkit-translate: no !important; font-language-override: normal !important; unicode-bidi: isolate !important; } /* 为所有脚本元素添加额外防护 */ .translation-protected { unicode-bidi: isolate !important; font-language-override: normal !important; } `; document.head.appendChild(style); console.log('已创建翻译防护层'); } // 初始化函数 function init() { console.log('微软自动翻译脚本开始初始化...'); const savedState = GM_getValue(CONFIG.storageKey, CONFIG.defaultEnabled); widgetEnabled = savedState; // 读取用户设置 targetLanguage = GM_getValue(CONFIG.translationLanguageKey, CONFIG.defaultTargetLanguage); translationMode = GM_getValue(CONFIG.translationModeKey, CONFIG.defaultMode); translationTextColor = GM_getValue(CONFIG.translationColorKey, CONFIG.defaultTextColor); translationBgColor = GM_getValue(CONFIG.translationBgColorKey, CONFIG.defaultBgColor); showTranslationIndicator = GM_getValue(CONFIG.showTranslationIndicatorKey, true); // 读取网站专用引擎配置 siteSpecificEngines = getSiteSpecificEngines(); // 检测俄语网站(基于域名) isRussianSite = quickDetectRussianSite(); currentRussianSiteConfig = getRussianSiteConfig(); checkWhitelist(); currentDomain = getCurrentDomain(); // 检查当前网站是否有专用引擎 checkSiteSpecificEngine(); // 重置检测计数器 russianDetectionSamples = 0; russianContentRatio = 0; // 添加样式 addStyles(); updateColorStyles(); // 创建翻译防护层 createTranslationProtectionLayer(); // 设置页面加载完成标记 if (document.readyState === 'complete' || document.readyState === 'interactive') { isPageLoaded = true; pageLoaded(); } else { window.addEventListener('load', function() { isPageLoaded = true; pageLoaded(); }, { once: true }); } // 设置快捷键 setupHotkey(); // 延迟检测俄语内容 setTimeout(() => { smartEngineDetection(); }, 1000); // 立即注册菜单命令 registerMenuCommands(); // 监听页面卸载事件 window.addEventListener('beforeunload', function() { console.log('页面卸载'); if (russianDetectionTimer) { clearTimeout(russianDetectionTimer); russianDetectionTimer = null; } if (pageLoadedTimer) { clearTimeout(pageLoadedTimer); pageLoadedTimer = null; } if (indicatorHideTimeout) { clearTimeout(indicatorHideTimeout); indicatorHideTimeout = null; } }); } // 页面加载完成处理 function pageLoaded() { console.log('页面加载完成,准备开始翻译'); setTimeout(() => { registerMenuCommands(); }, 500); const shouldTranslate = isWhitelistedDomain || widgetEnabled; if (shouldTranslate) { console.log('页面加载完成,立即启动翻译'); updateWidgetBasedOnSettings(); if (GM_getValue('firstRun', true)) { setTimeout(function() { const languageName = CONFIG.LANGUAGES[targetLanguage]?.name || targetLanguage; let message = `微软自动翻译已启动,目标语言: ${languageName}`; if (isUsingSiteSpecificEngine) { message += ` (网站专用引擎)`; } message += `。按Ctrl+Shift+Y关闭`; showStatusMessage(message, 2500); GM_setValue('firstRun', false); }, 1500); } } } // 添加CSS样式 function addStyles() { const css = ` /* 微软翻译文本样式 */ .microsoft-translated { position: relative; border-radius: 3px; transition: all 0.2s; } .microsoft-translated.replace-mode { color: var(--translation-text-color, #0066cc) !important; font-weight: 500; } .microsoft-translated.replace-mode:hover { background-color: var(--translation-bg-color, rgba(0,102,204,0.1)) !important; cursor: pointer; } .microsoft-translated.dual-mode { display: inline-flex; flex-direction: column; } .microsoft-translated.dual-mode .original-text { color: #666; font-size: 0.9em; text-decoration: line-through; opacity: 0.7; } .microsoft-translated.dual-mode .translated-text { color: var(--translation-text-color, #0066cc) !important; font-weight: 500; } /* 翻译状态指示器 */ .translating-indicator { position: fixed; bottom: 20px; right: 20px; background: rgba(0, 0, 0, 0.7); color: #00ffcc; padding: 4px 8px; border-radius: 8px; z-index: 2147483647; font-family: Arial, sans-serif; font-size: 12px; display: flex; align-items: center; gap: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); border: 1px solid rgba(0, 255, 204, 0.3); min-width: 100px; pointer-events: none; height: auto; box-sizing: border-box; opacity: 0.9; transition: opacity 0.3s, background 0.3s, color 0.3s; } .translating-indicator .spinner { width: 12px; height: 12px; border: 2px solid rgba(0, 255, 204, 0.5); border-top: 2px solid #00ffcc; border-radius: 50%; animation: spin 1.2s linear infinite; flex-shrink: 0; } .translating-indicator span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); font-size: 12px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 状态消息 */ #microsoft-status-msg { position: fixed; top: 20px; right: 20px; background: rgba(0, 0, 0, 0.9); color: white; padding: 12px 20px; border-radius: 10px; z-index: 2147483646; font-family: Arial, sans-serif; font-size: 14px; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(5px); border: 1px solid rgba(255, 255, 255, 0.1); opacity: 1; transform: translateY(0); max-width: 300px; word-wrap: break-word; } #microsoft-status-msg.fade-out { opacity: 0; transform: translateY(-20px); } /* 设置项样式 */ .setting-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px; background-color: #f9f9f9; border-radius: 6px; } .setting-label { flex: 1; } .setting-title { font-weight: bold; color: #333; margin-bottom: 2px; font-size: 13px; } .setting-description { color: #666; font-size: 11px; line-height: 1.3; } /* 开关样式 */ .switch { position: relative; display: inline-block; width: 45px; height: 22px; flex-shrink: 0; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 22px; } .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #0066cc; } input:checked + .slider:before { transform: translateX(23px); } input:focus + .slider { box-shadow: 0 0 1px #0066cc; } /* 网站专用引擎标签 */ .site-engine-badge { display: inline-block; background-color: #00b894; color: white; font-size: 9px; padding: 1px 4px; border-radius: 8px; margin-left: 5px; font-weight: bold; vertical-align: middle; } /* 引擎选择按钮(只保留微软) */ .engine-btn { padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 5px; transition: all 0.2s; flex: 1; justify-content: center; } .engine-btn.active { border-color: #0066cc; background-color: #f0f7ff; } .engine-btn:hover:not(.active) { border-color: #999; background-color: #f9f9f9; } /* 紧凑的设置项 */ .compact-setting { margin-bottom: 10px; padding: 8px; background-color: #f8f9fa; border-radius: 5px; border-left: 3px solid #6c5ce7; } .compact-setting-title { font-weight: bold; color: #333; margin-bottom: 5px; font-size: 13px; } .compact-setting-desc { color: #666; font-size: 11px; margin-bottom: 8px; line-height: 1.3; } /* 紧凑按钮 */ .compact-btn { padding: 5px 8px; font-size: 11px; border-radius: 4px; border: 1px solid #ddd; background: white; cursor: pointer; transition: all 0.2s; } .compact-btn:hover { border-color: #999; background-color: #f5f5f5; } .compact-btn-primary { background-color: #0066cc; color: white; border-color: #0066cc; } .compact-btn-primary:hover { background-color: #0052a3; } /* 紧凑输入框 */ .compact-input { padding: 5px 8px; font-size: 12px; border: 1px solid #ddd; border-radius: 4px; width: 100%; box-sizing: border-box; } /* 增强防护:确保所有脚本相关元素不被翻译 */ [data-no-translate], .no-translate, .microsoft-translated, .microsoft-translated *, .translating-indicator, .translating-indicator *, #microsoft-status-msg, #microsoft-status-msg *, .translation-settings-modal, .translation-settings-modal *, .tm-script-menu, .tm-script-menu *, .vm-script-menu, .vm-script-menu *, .gm-script-menu, .gm-script-menu *, [class*="userscript"], [class*="userscript"] *, [id*="userscript"], [id*="userscript"] *, .script-element, .script-element * { translate: no !important; -webkit-translate: no !important; -moz-translate: no !important; -ms-translate: no !important; -o-translate: no !important; font-language-override: normal !important; text-orientation: mixed !important; } /* 防止特定文本被翻译 */ .engine-btn, .engine-btn *, .site-engine-badge, .site-engine-badge * { translate: no !important; font-language-override: normal !important; } /* 为特定元素添加额外的防护 */ [data-protect-translation="true"], [data-protect-translation="true"] * { translate: no !important; -webkit-translate: no !important; font-language-override: normal !important; } `; GM_addStyle(css); } // 更新颜色样式 function updateColorStyles() { const oldStyle = document.querySelector('#translation-color-styles'); if (oldStyle) oldStyle.remove(); const style = document.createElement('style'); style.id = 'translation-color-styles'; style.textContent = ` :root { --translation-text-color: ${translationTextColor}; --translation-bg-color: ${translationBgColor}; } .microsoft-translated.replace-mode { color: ${translationTextColor} !important; } .microsoft-translated.replace-mode:hover { background-color: ${translationBgColor} !important; } .microsoft-translated.dual-mode .translated-text { color: ${translationTextColor} !important; } `; document.head.appendChild(style); console.log('已更新翻译颜色样式:', translationTextColor); } // 根据设置更新翻译功能 function updateWidgetBasedOnSettings() { console.log('更新翻译设置,当前状态:', { widgetEnabled, isWhitelistedDomain, isUsingSiteSpecificEngine, isRussianContent, russianContentRatio: (russianContentRatio * 100).toFixed(1) + '%' }); if (autoTranslateTimeout) { clearTimeout(autoTranslateTimeout); autoTranslateTimeout = null; } stopAllObservations(); const shouldShowWidget = isWhitelistedDomain || widgetEnabled; if (shouldShowWidget) { console.log('启用微软翻译器'); initMicrosoftTranslator(); } else { console.log('翻译已禁用'); deactivateAllTranslators(); } registerMenuCommands(); } // 初始化微软翻译器 async function initMicrosoftTranslator() { await setupMicrosoftTranslator(); if (widgetEnabled || isWhitelistedDomain) { console.log('微软翻译器就绪,立即开始翻译'); if (showTranslationIndicator) { showTranslatingIndicator('正在翻译页面...'); } if (isPageLoaded) { setTimeout(() => { translateWholePage(); }, 50); } else { autoTranslateTimeout = setTimeout(() => { translateWholePage(); }, CONFIG.AUTO_TRANSLATE_DELAY); } } startDOMObservation(); startContinuousTranslation(); } // 停用所有翻译器 function deactivateAllTranslators() { if (autoTranslateTimeout) { clearTimeout(autoTranslateTimeout); autoTranslateTimeout = null; } stopAllObservations(); deactivateMicrosoftTranslator(); hideTranslatingIndicator(); console.log('所有翻译器已停用'); } // 停止所有观察器 function stopAllObservations() { stopDOMObservation(); stopScrollCheck(); stopContinuousTranslation(); } // 注册菜单命令 function registerMenuCommands() { unregisterMenuCommands(); // 主开关命令 let mainMenuText = widgetEnabled ? '❌ 关闭自动翻译' : '✅ 开启自动翻译'; mainMenuText += ` (微软)`; if (isRussianContent) mainMenuText += ' 🇷🇺'; if (isUsingSiteSpecificEngine) mainMenuText += ' 🌐'; try { mainMenuCommandId = GM_registerMenuCommand(mainMenuText, toggleWidget, 't'); console.log('主菜单命令注册成功:', mainMenuText); } catch (e) { console.error('主菜单命令注册失败:', e); } // 白名单命令 const whitelistText = isWhitelistedDomain ? '⭐ 从白名单中移除' : '☆ 添加到白名单'; try { whitelistMenuCommandId = GM_registerMenuCommand(whitelistText, toggleWhitelist, 'a'); console.log('白名单菜单命令注册成功:', whitelistText); } catch (e) { console.error('白名单菜单命令注册失败:', e); } // 设置命令 try { settingsMenuCommandId = GM_registerMenuCommand('⚙️ 翻译设置', showSettings, 's'); console.log('设置菜单命令注册成功'); } catch (e) { console.error('设置菜单命令注册失败:', e); } console.log('菜单命令注册完成'); } // 注销菜单命令 function unregisterMenuCommands() { try { if (mainMenuCommandId !== null) { GM_unregisterMenuCommand(mainMenuCommandId); mainMenuCommandId = null; } if (whitelistMenuCommandId !== null) { GM_unregisterMenuCommand(whitelistMenuCommandId); whitelistMenuCommandId = null; } if (settingsMenuCommandId !== null) { GM_unregisterMenuCommand(settingsMenuCommandId); settingsMenuCommandId = null; } } catch (e) { console.error('注销菜单命令失败:', e); } } // 显示设置窗口 function showSettings() { isModalOpen = true; const existingContent = document.querySelector('.translation-settings-content'); let scrollTopToRestore = 0; if (existingContent) { scrollTopToRestore = existingContent.scrollTop; const existingModal = existingContent.closest('.translation-settings-modal'); if (existingModal) { existingModal.remove(); } } const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 2147483647; display: flex; justify-content: center; align-items: center; font-family: Arial, sans-serif; `; modal.setAttribute('data-no-translate', 'true'); modal.setAttribute('translate', 'no'); modal.setAttribute('data-protect-translation', 'true'); modal.className = 'no-translate translation-settings-modal script-element'; const modalContent = document.createElement('div'); modalContent.style.cssText = ` background-color: white; padding: 20px; border-radius: 10px; max-width: 480px; width: 90%; max-height: 85vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); font-size: 13px; `; modalContent.setAttribute('data-no-translate', 'true'); modalContent.setAttribute('translate', 'no'); modalContent.setAttribute('data-protect-translation', 'true'); modalContent.className = 'no-translate translation-settings-content script-element'; const title = document.createElement('h3'); title.textContent = '翻译设置'; title.style.cssText = ` margin-top: 0; margin-bottom: 15px; color: #333; text-align: center; font-size: 16px; `; title.setAttribute('data-no-translate', 'true'); title.setAttribute('translate', 'no'); title.setAttribute('data-protect-translation', 'true'); title.className = 'no-translate'; modalContent.appendChild(title); // 当前状态显示 const statusInfo = document.createElement('div'); statusInfo.style.cssText = ` margin-bottom: 15px; padding: 10px; background-color: #f0f7ff; border-radius: 6px; border-left: 3px solid #0066cc; font-size: 12px; `; statusInfo.setAttribute('data-no-translate', 'true'); statusInfo.setAttribute('translate', 'no'); statusInfo.setAttribute('data-protect-translation', 'true'); statusInfo.className = 'no-translate'; let statusHTML = `
当前状态
微软翻译`; if (isUsingSiteSpecificEngine) { statusHTML += ` 网站专用`; } statusHTML += `
`; if (isRussianSite) { statusHTML += `
${isRussianContent ? '检测到俄语内容 🇷🇺' : '俄语域名'}
`; } statusInfo.innerHTML = statusHTML; modalContent.appendChild(statusInfo); // 翻译提示开关设置 const indicatorSection = document.createElement('div'); indicatorSection.className = 'compact-setting'; indicatorSection.setAttribute('data-no-translate', 'true'); indicatorSection.setAttribute('translate', 'no'); indicatorSection.setAttribute('data-protect-translation', 'true'); indicatorSection.className += ' no-translate'; const indicatorTitle = document.createElement('div'); indicatorTitle.className = 'compact-setting-title'; indicatorTitle.textContent = '翻译状态提示'; indicatorTitle.setAttribute('data-no-translate', 'true'); indicatorTitle.setAttribute('translate', 'no'); indicatorTitle.setAttribute('data-protect-translation', 'true'); indicatorSection.appendChild(indicatorTitle); const indicatorDesc = document.createElement('div'); indicatorDesc.className = 'compact-setting-desc'; indicatorDesc.textContent = '控制是否显示"正在翻译页面..."等翻译状态提示'; indicatorDesc.setAttribute('data-no-translate', 'true'); indicatorDesc.setAttribute('translate', 'no'); indicatorDesc.setAttribute('data-protect-translation', 'true'); indicatorSection.appendChild(indicatorDesc); const indicatorSwitch = document.createElement('div'); indicatorSwitch.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 8px; background-color: #f9f9f9; border-radius: 6px; `; const indicatorLabel = document.createElement('div'); indicatorLabel.style.cssText = `flex: 1;`; const indicatorLabelTitle = document.createElement('div'); indicatorLabelTitle.className = 'setting-title'; indicatorLabelTitle.textContent = '显示翻译状态提示'; indicatorLabelTitle.setAttribute('data-no-translate', 'true'); indicatorLabelTitle.setAttribute('translate', 'no'); indicatorLabelTitle.setAttribute('data-protect-translation', 'true'); const indicatorLabelDesc = document.createElement('div'); indicatorLabelDesc.className = 'setting-description'; indicatorLabelDesc.textContent = '开启后会在翻译时显示"正在翻译页面..."和"翻译完成"等提示'; indicatorLabelDesc.setAttribute('data-no-translate', 'true'); indicatorLabelDesc.setAttribute('translate', 'no'); indicatorLabelDesc.setAttribute('data-protect-translation', 'true'); indicatorLabel.appendChild(indicatorLabelTitle); indicatorLabel.appendChild(indicatorLabelDesc); const switchContainer = document.createElement('label'); switchContainer.className = 'switch'; const switchInput = document.createElement('input'); switchInput.type = 'checkbox'; switchInput.checked = showTranslationIndicator; switchInput.setAttribute('data-no-translate', 'true'); switchInput.setAttribute('translate', 'no'); switchInput.setAttribute('data-protect-translation', 'true'); const switchSlider = document.createElement('span'); switchSlider.className = 'slider'; switchSlider.setAttribute('data-no-translate', 'true'); switchSlider.setAttribute('translate', 'no'); switchSlider.setAttribute('data-protect-translation', 'true'); switchContainer.appendChild(switchInput); switchContainer.appendChild(switchSlider); switchInput.addEventListener('change', function() { showTranslationIndicator = this.checked; GM_setValue(CONFIG.showTranslationIndicatorKey, showTranslationIndicator); if (!showTranslationIndicator) { hideTranslatingIndicator(); } showStatusMessage(`翻译状态提示 ${showTranslationIndicator ? '已开启' : '已关闭'}`, 2000); }); indicatorSwitch.appendChild(indicatorLabel); indicatorSwitch.appendChild(switchContainer); indicatorSection.appendChild(indicatorSwitch); modalContent.appendChild(indicatorSection); // 网站专用引擎设置(仅微软) const siteEngineSection = document.createElement('div'); siteEngineSection.className = 'compact-setting'; siteEngineSection.setAttribute('data-no-translate', 'true'); siteEngineSection.setAttribute('translate', 'no'); siteEngineSection.setAttribute('data-protect-translation', 'true'); siteEngineSection.className += ' no-translate'; const siteEngineTitle = document.createElement('div'); siteEngineTitle.className = 'compact-setting-title'; siteEngineTitle.textContent = '网站专用引擎'; siteEngineTitle.setAttribute('data-no-translate', 'true'); siteEngineTitle.setAttribute('translate', 'no'); siteEngineTitle.setAttribute('data-protect-translation', 'true'); siteEngineSection.appendChild(siteEngineTitle); const siteEngineDesc = document.createElement('div'); siteEngineDesc.className = 'compact-setting-desc'; siteEngineDesc.textContent = '为当前网站设置专用翻译引擎(仅微软)'; siteEngineDesc.setAttribute('data-no-translate', 'true'); siteEngineDesc.setAttribute('translate', 'no'); siteEngineDesc.setAttribute('data-protect-translation', 'true'); siteEngineSection.appendChild(siteEngineDesc); const currentDomain = getCurrentDomain(); const isSiteHasEngine = isUsingSiteSpecificEngine; const currentSiteInfo = document.createElement('div'); currentSiteInfo.style.cssText = ` margin-bottom: 8px; padding: 6px; background-color: ${isSiteHasEngine ? '#e8f5e9' : '#f5f5f5'}; border-radius: 4px; font-size: 12px; `; currentSiteInfo.setAttribute('data-no-translate', 'true'); currentSiteInfo.setAttribute('translate', 'no'); currentSiteInfo.setAttribute('data-protect-translation', 'true'); currentSiteInfo.className = 'no-translate'; currentSiteInfo.innerHTML = `
${currentDomain}
专用引擎: ${isSiteHasEngine ? '微软翻译' : '无'}
`; siteEngineSection.appendChild(currentSiteInfo); // 引擎选择按钮(仅微软) const engineSelection = document.createElement('div'); engineSelection.style.cssText = `display: flex; gap: 6px; margin-bottom: 8px;`; const microsoftBtn = document.createElement('button'); microsoftBtn.className = 'engine-btn active'; microsoftBtn.innerHTML = `🔵微软`; microsoftBtn.setAttribute('data-no-translate', 'true'); microsoftBtn.setAttribute('translate', 'no'); microsoftBtn.setAttribute('data-protect-translation', 'true'); microsoftBtn.className += ' no-translate script-element'; microsoftBtn.disabled = true; microsoftBtn.style.cursor = 'default'; microsoftBtn.style.opacity = '0.7'; engineSelection.appendChild(microsoftBtn); // 清除按钮 const clearBtn = document.createElement('button'); clearBtn.textContent = '清除专用引擎'; clearBtn.style.cssText = ` padding: 5px 8px; background-color: ${isSiteHasEngine ? '#ff4757' : '#ccc'}; color: white; border: none; border-radius: 4px; cursor: ${isSiteHasEngine ? 'pointer' : 'not-allowed'}; font-size: 11px; transition: background-color 0.2s; width: 100%; `; clearBtn.setAttribute('data-no-translate', 'true'); clearBtn.setAttribute('translate', 'no'); clearBtn.setAttribute('data-protect-translation', 'true'); clearBtn.className += ' no-translate script-element'; if (isSiteHasEngine) { clearBtn.addEventListener('click', function() { clearSiteSpecificEngine(); showStatusMessage('已清除当前网站专用引擎', 2000); showSettings(); }); } siteEngineSection.appendChild(engineSelection); siteEngineSection.appendChild(clearBtn); modalContent.appendChild(siteEngineSection); // 目标语言设置 const languageSection = document.createElement('div'); languageSection.style.cssText = `margin-bottom: 15px;`; languageSection.setAttribute('data-no-translate', 'true'); languageSection.setAttribute('translate', 'no'); languageSection.setAttribute('data-protect-translation', 'true'); languageSection.className = 'no-translate'; const languageLabel = document.createElement('div'); languageLabel.textContent = '目标语言:'; languageLabel.style.cssText = `font-weight: bold; color: #555; margin-bottom: 8px; font-size: 13px;`; languageLabel.setAttribute('data-no-translate', 'true'); languageLabel.setAttribute('translate', 'no'); languageLabel.setAttribute('data-protect-translation', 'true'); languageSection.appendChild(languageLabel); const languageGrid = document.createElement('div'); languageGrid.style.cssText = `display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 6px;`; languageGrid.setAttribute('data-no-translate', 'true'); languageGrid.setAttribute('translate', 'no'); languageGrid.setAttribute('data-protect-translation', 'true'); languageGrid.className = 'no-translate'; Object.values(CONFIG.LANGUAGES).forEach(lang => { const languageBtn = document.createElement('button'); languageBtn.style.cssText = ` padding: 8px; border: ${lang.code === targetLanguage ? '2px solid #0066cc' : '1px solid #ddd'}; border-radius: 5px; background: ${lang.code === targetLanguage ? '#f0f7ff' : 'white'}; cursor: pointer; font-size: 12px; display: flex; flex-direction: column; align-items: center; gap: 2px; transition: all 0.2s; `; languageBtn.setAttribute('data-no-translate', 'true'); languageBtn.setAttribute('translate', 'no'); languageBtn.setAttribute('data-protect-translation', 'true'); languageBtn.className = 'no-translate script-element'; languageBtn.innerHTML = `${lang.icon}${lang.name}`; languageBtn.addEventListener('click', function() { if (lang.code !== targetLanguage) { targetLanguage = lang.code; GM_setValue(CONFIG.translationLanguageKey, targetLanguage); translationCache.clear(); showSettings(); const engineToUse = getCurrentEngine(); if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive && (widgetEnabled || isWhitelistedDomain)) { showStatusMessage(`目标语言: ${lang.name},立即重新翻译...`, 2000); scheduleImmediateTranslation(); } else { showStatusMessage(`目标语言: ${lang.name}`, 2000); } } }); languageBtn.addEventListener('mouseenter', function() { if (lang.code !== targetLanguage) { this.style.borderColor = '#999'; this.style.backgroundColor = '#f9f9f9'; } }); languageBtn.addEventListener('mouseleave', function() { if (lang.code !== targetLanguage) { this.style.borderColor = '#ddd'; this.style.backgroundColor = 'white'; } }); languageGrid.appendChild(languageBtn); }); languageSection.appendChild(languageGrid); modalContent.appendChild(languageSection); // 翻译文字颜色设置 const colorSection = document.createElement('div'); colorSection.style.cssText = ` margin-bottom: 20px; padding: 12px; background-color: #f8f9fa; border-radius: 8px; border-left: 3px solid ${translationTextColor}; `; colorSection.setAttribute('data-no-translate', 'true'); colorSection.setAttribute('translate', 'no'); colorSection.setAttribute('data-protect-translation', 'true'); colorSection.className = 'no-translate'; const colorLabel = document.createElement('div'); colorLabel.textContent = '翻译文字颜色:'; colorLabel.style.cssText = `font-weight: bold; color: #555; margin-bottom: 10px; font-size: 14px;`; colorLabel.setAttribute('data-no-translate', 'true'); colorLabel.setAttribute('translate', 'no'); colorLabel.setAttribute('data-protect-translation', 'true'); colorSection.appendChild(colorLabel); const colorPreview = document.createElement('div'); colorPreview.style.cssText = `display: flex; align-items: center; margin-bottom: 10px; padding: 8px; background-color: white; border-radius: 6px; border: 1px solid #ddd;`; const previewLabel = document.createElement('span'); previewLabel.textContent = '当前颜色预览: '; previewLabel.style.cssText = `margin-right: 10px; font-size: 12px; color: #666;`; const previewColor = document.createElement('span'); previewColor.textContent = '翻译文字示例'; previewColor.style.cssText = `color: ${translationTextColor}; font-weight: bold; padding: 4px 8px; background-color: ${translationBgColor}; border-radius: 4px; border: 1px solid ${translationTextColor}20;`; colorPreview.appendChild(previewLabel); colorPreview.appendChild(previewColor); colorSection.appendChild(colorPreview); const colorGrid = document.createElement('div'); colorGrid.style.cssText = `display: grid; grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); gap: 6px; margin-bottom: 10px;`; colorGrid.setAttribute('data-no-translate', 'true'); colorGrid.setAttribute('translate', 'no'); colorGrid.setAttribute('data-protect-translation', 'true'); colorGrid.className = 'no-translate'; CONFIG.COLOR_PRESETS.forEach(preset => { const colorBtn = document.createElement('button'); colorBtn.style.cssText = ` padding: 8px; border: ${translationTextColor === preset.value ? '2px solid #333' : '1px solid #ddd'}; border-radius: 5px; background: ${preset.value}; color: white; cursor: pointer; font-size: 11px; text-shadow: 0 1px 1px rgba(0,0,0,0.3); height: 35px; position: relative; overflow: hidden; transition: all 0.2s; `; colorBtn.setAttribute('data-no-translate', 'true'); colorBtn.setAttribute('translate', 'no'); colorBtn.setAttribute('data-protect-translation', 'true'); colorBtn.className = 'no-translate script-element'; const colorName = document.createElement('div'); colorName.textContent = preset.name; colorName.style.cssText = `font-weight: bold; font-size: 10px;`; colorBtn.appendChild(colorName); if (translationTextColor === preset.value) { const checkmark = document.createElement('div'); checkmark.textContent = '✓'; checkmark.style.cssText = `position: absolute; top: 1px; right: 1px; font-size: 10px; font-weight: bold;`; colorBtn.appendChild(checkmark); } colorBtn.addEventListener('click', function() { translationTextColor = preset.value; translationBgColor = preset.bg; GM_setValue(CONFIG.translationColorKey, translationTextColor); GM_setValue(CONFIG.translationBgColorKey, translationBgColor); updateColorStyles(); updateExistingTranslationColors(); showStatusMessage(`已应用颜色: ${preset.name}`, 2000); showSettings(); }); colorGrid.appendChild(colorBtn); }); colorSection.appendChild(colorGrid); const customColorContainer = document.createElement('div'); customColorContainer.style.cssText = `display: flex; gap: 8px; margin-top: 10px; align-items: center;`; const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.value = translationTextColor; colorInput.style.cssText = `width: 40px; height: 40px; padding: 0; border: none; cursor: pointer; border-radius: 4px;`; colorInput.setAttribute('data-no-translate', 'true'); colorInput.setAttribute('translate', 'no'); colorInput.setAttribute('data-protect-translation', 'true'); colorInput.className = 'no-translate'; const colorInputText = document.createElement('input'); colorInputText.type = 'text'; colorInputText.value = translationTextColor; colorInputText.placeholder = '#RRGGBB'; colorInputText.style.cssText = `flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px;`; colorInputText.setAttribute('data-no-translate', 'true'); colorInputText.setAttribute('translate', 'no'); colorInputText.setAttribute('data-protect-translation', 'true'); colorInputText.className = 'no-translate script-element'; const applyColorBtn = document.createElement('button'); applyColorBtn.textContent = '应用'; applyColorBtn.style.cssText = `padding: 8px 12px; background-color: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold;`; applyColorBtn.setAttribute('data-no-translate', 'true'); applyColorBtn.setAttribute('translate', 'no'); applyColorBtn.setAttribute('data-protect-translation', 'true'); applyColorBtn.className = 'no-translate script-element'; applyColorBtn.addEventListener('click', function() { const colorValue = colorInputText.value.trim(); if (/^#[0-9A-Fa-f]{6}$/.test(colorValue) || /^#[0-9A-Fa-f]{3}$/.test(colorValue)) { translationTextColor = colorValue; translationBgColor = colorValue.replace('#', 'rgba(') + ',0.1)'; GM_setValue(CONFIG.translationColorKey, translationTextColor); GM_setValue(CONFIG.translationBgColorKey, translationBgColor); updateColorStyles(); updateExistingTranslationColors(); showStatusMessage('自定义颜色已应用', 2000); showSettings(); } else { showStatusMessage('颜色格式错误,请使用#RRGGBB格式', 2000); } }); colorInput.addEventListener('input', function() { colorInputText.value = this.value; }); colorInputText.addEventListener('input', function() { const colorValue = this.value.trim(); if (/^#[0-9A-Fa-f]{6}$/.test(colorValue) || /^#[0-9A-Fa-f]{3}$/.test(colorValue)) { colorInput.value = colorValue; } }); customColorContainer.appendChild(colorInput); customColorContainer.appendChild(colorInputText); customColorContainer.appendChild(applyColorBtn); colorSection.appendChild(customColorContainer); modalContent.appendChild(colorSection); // 显示模式设置 const modeSection = document.createElement('div'); modeSection.style.cssText = `margin-bottom: 15px;`; modeSection.setAttribute('data-no-translate', 'true'); modeSection.setAttribute('translate', 'no'); modeSection.setAttribute('data-protect-translation', 'true'); modeSection.className = 'no-translate'; const modeLabel = document.createElement('div'); modeLabel.textContent = '显示模式:'; modeLabel.style.cssText = `font-weight: bold; color: #555; margin-bottom: 8px; font-size: 13px;`; modeLabel.setAttribute('data-no-translate', 'true'); modeLabel.setAttribute('translate', 'no'); modeLabel.setAttribute('data-protect-translation', 'true'); modeSection.appendChild(modeLabel); const modeGrid = document.createElement('div'); modeGrid.style.cssText = `display: grid; grid-template-columns: 1fr 1fr; gap: 8px;`; modeGrid.setAttribute('data-no-translate', 'true'); modeGrid.setAttribute('translate', 'no'); modeGrid.setAttribute('data-protect-translation', 'true'); modeGrid.className = 'no-translate'; const replaceModeBtn = document.createElement('button'); replaceModeBtn.style.cssText = ` padding: 8px; border: ${translationMode === CONFIG.MODES.REPLACE ? '2px solid #0066cc' : '1px solid #ddd'}; border-radius: 5px; background: ${translationMode === CONFIG.MODES.REPLACE ? '#f0f7ff' : 'white'}; cursor: pointer; font-size: 12px; display: flex; flex-direction: column; align-items: center; gap: 3px; transition: all 0.2s; `; replaceModeBtn.setAttribute('data-no-translate', 'true'); replaceModeBtn.setAttribute('translate', 'no'); replaceModeBtn.setAttribute('data-protect-translation', 'true'); replaceModeBtn.className = 'no-translate script-element'; replaceModeBtn.innerHTML = `🔁替换原文`; replaceModeBtn.addEventListener('click', function() { if (translationMode !== CONFIG.MODES.REPLACE) { translationMode = CONFIG.MODES.REPLACE; GM_setValue(CONFIG.translationModeKey, translationMode); showSettings(); const engineToUse = getCurrentEngine(); if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive && (widgetEnabled || isWhitelistedDomain)) { showStatusMessage('替换模式,立即重新翻译...', 2000); scheduleImmediateTranslation(); } else { showStatusMessage('替换模式', 2000); } } }); const dualModeBtn = document.createElement('button'); dualModeBtn.style.cssText = ` padding: 8px; border: ${translationMode === CONFIG.MODES.DUAL ? '2px solid #00b894' : '1px solid #ddd'}; border-radius: 5px; background: ${translationMode === CONFIG.MODES.DUAL ? '#f0fff4' : 'white'}; cursor: pointer; font-size: 12px; display: flex; flex-direction: column; align-items: center; gap: 3px; transition: all 0.2s; `; dualModeBtn.setAttribute('data-no-translate', 'true'); dualModeBtn.setAttribute('translate', 'no'); dualModeBtn.setAttribute('data-protect-translation', 'true'); dualModeBtn.className = 'no-translate script-element'; dualModeBtn.innerHTML = `↩️双语显示`; dualModeBtn.addEventListener('click', function() { if (translationMode !== CONFIG.MODES.DUAL) { translationMode = CONFIG.MODES.DUAL; GM_setValue(CONFIG.translationModeKey, translationMode); showSettings(); const engineToUse = getCurrentEngine(); if (engineToUse === CONFIG.ENGINES.MICROSOFT && microsoftTranslatorActive && (widgetEnabled || isWhitelistedDomain)) { showStatusMessage('双语模式,立即重新翻译...', 2000); scheduleImmediateTranslation(); } else { showStatusMessage('双语模式', 2000); } } }); modeGrid.appendChild(replaceModeBtn); modeGrid.appendChild(dualModeBtn); modeSection.appendChild(modeGrid); modalContent.appendChild(modeSection); // 白名单管理部分 const whitelistSection = document.createElement('div'); whitelistSection.style.cssText = `margin-bottom: 15px; border-top: 1px solid #eee; padding-top: 15px;`; whitelistSection.setAttribute('data-no-translate', 'true'); whitelistSection.setAttribute('translate', 'no'); whitelistSection.setAttribute('data-protect-translation', 'true'); whitelistSection.className = 'no-translate'; const whitelistHeader = document.createElement('div'); whitelistHeader.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;`; const whitelistTitle = document.createElement('div'); whitelistTitle.textContent = '白名单管理'; whitelistTitle.style.cssText = `font-weight: bold; color: #555; font-size: 13px;`; whitelistTitle.setAttribute('data-no-translate', 'true'); whitelistTitle.setAttribute('translate', 'no'); whitelistTitle.setAttribute('data-protect-translation', 'true'); whitelistTitle.className = 'no-translate'; const whitelist = getWhitelist(); const isInWhitelist = whitelist.includes(currentDomain); const toggleWhitelistBtn = document.createElement('button'); toggleWhitelistBtn.textContent = isInWhitelist ? `⭐ 移除` : `☆ 添加`; toggleWhitelistBtn.style.cssText = ` padding: 4px 8px; background-color: ${isInWhitelist ? '#ffa502' : '#2ed573'}; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; `; toggleWhitelistBtn.setAttribute('data-no-translate', 'true'); toggleWhitelistBtn.setAttribute('translate', 'no'); toggleWhitelistBtn.setAttribute('data-protect-translation', 'true'); toggleWhitelistBtn.className = 'no-translate script-element'; toggleWhitelistBtn.addEventListener('click', function() { toggleWhitelist(); showSettings(); }); whitelistHeader.appendChild(whitelistTitle); whitelistHeader.appendChild(toggleWhitelistBtn); whitelistSection.appendChild(whitelistHeader); if (whitelist.length > 0) { const whitelistList = document.createElement('div'); whitelistList.style.cssText = ` max-height: 100px; overflow-y: auto; margin-bottom: 10px; border: 1px solid #eee; border-radius: 4px; padding: 5px; background-color: #f9f9f9; `; whitelistList.setAttribute('data-no-translate', 'true'); whitelistList.setAttribute('translate', 'no'); whitelistList.setAttribute('data-protect-translation', 'true'); whitelistList.className = 'no-translate'; whitelist.forEach((domain, index) => { const domainItem = document.createElement('div'); domainItem.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 5px 6px; background-color: ${index % 2 === 0 ? 'white' : '#f5f5f5'}; border-radius: 3px; margin-bottom: 3px; font-size: 11px; `; domainItem.setAttribute('data-no-translate', 'true'); domainItem.setAttribute('translate', 'no'); domainItem.setAttribute('data-protect-translation', 'true'); domainItem.className = 'no-translate script-element'; const domainText = document.createElement('span'); domainText.textContent = domain; domainText.style.cssText = `flex-grow: 1; word-break: break-all;`; domainText.setAttribute('data-no-translate', 'true'); domainText.setAttribute('translate', 'no'); domainText.setAttribute('data-protect-translation', 'true'); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.className = 'compact-btn'; deleteBtn.style.cssText = `font-size: 10px; padding: 3px 6px; margin-left: 5px; flex-shrink: 0;`; deleteBtn.setAttribute('data-no-translate', 'true'); deleteBtn.setAttribute('translate', 'no'); deleteBtn.setAttribute('data-protect-translation', 'true'); deleteBtn.className += ' no-translate script-element'; deleteBtn.addEventListener('click', function() { if (confirm(`确定要从白名单中删除 "${domain}" 吗?`)) { let updatedWhitelist = getWhitelist(); updatedWhitelist = updatedWhitelist.filter(d => d !== domain); saveWhitelist(updatedWhitelist); showStatusMessage(`已从白名单中删除: ${domain}`, 2000); showSettings(); } }); domainItem.appendChild(domainText); domainItem.appendChild(deleteBtn); whitelistList.appendChild(domainItem); }); whitelistSection.appendChild(whitelistList); const clearAllBtn = document.createElement('button'); clearAllBtn.textContent = '清除所有白名单'; clearAllBtn.className = 'compact-btn'; clearAllBtn.style.cssText = `width: 100%; padding: 6px; font-size: 11px;`; clearAllBtn.setAttribute('data-no-translate', 'true'); clearAllBtn.setAttribute('translate', 'no'); clearAllBtn.setAttribute('data-protect-translation', 'true'); clearAllBtn.className += ' no-translate script-element'; clearAllBtn.addEventListener('click', function() { if (confirm(`确定要清除所有白名单域名吗?`)) { cachedWhitelist = []; GM_deleteValue(CONFIG.whitelistKey); showStatusMessage('已清除所有白名单', 2000); setTimeout(function() { checkWhitelist(); showSettings(); }, 100); } }); whitelistSection.appendChild(clearAllBtn); } else { const emptyMessage = document.createElement('div'); emptyMessage.textContent = '白名单为空'; emptyMessage.style.cssText = `text-align: center; color: #999; padding: 15px; font-style: italic; font-size: 12px;`; emptyMessage.setAttribute('data-no-translate', 'true'); emptyMessage.setAttribute('translate', 'no'); emptyMessage.setAttribute('data-protect-translation', 'true'); emptyMessage.className = 'no-translate'; whitelistSection.appendChild(emptyMessage); } modalContent.appendChild(whitelistSection); const closeBtn = document.createElement('button'); closeBtn.textContent = '关闭设置'; closeBtn.style.cssText = ` background-color: #747d8c; color: white; border: none; padding: 10px 15px; border-radius: 6px; cursor: pointer; width: 100%; font-size: 13px; font-weight: 600; transition: background-color 0.2s; `; closeBtn.setAttribute('data-no-translate', 'true'); closeBtn.setAttribute('translate', 'no'); closeBtn.setAttribute('data-protect-translation', 'true'); closeBtn.className = 'no-translate script-element'; closeBtn.addEventListener('click', function() { document.body.removeChild(modal); isModalOpen = false; }); closeBtn.addEventListener('mouseenter', function() { this.style.backgroundColor = '#636e72'; }); closeBtn.addEventListener('mouseleave', function() { this.style.backgroundColor = '#747d8c'; }); modalContent.appendChild(closeBtn); modal.appendChild(modalContent); modal.addEventListener('click', function(e) { if (e.target === modal) { document.body.removeChild(modal); isModalOpen = false; } }); const escHandler = function(e) { if (e.key === 'Escape') { document.body.removeChild(modal); document.removeEventListener('keydown', escHandler); isModalOpen = false; } }; document.addEventListener('keydown', escHandler); document.body.appendChild(modal); setTimeout(() => { modalContent.scrollTop = scrollTopToRestore; }, 0); } // 更新现有翻译元素的颜色 function updateExistingTranslationColors() { console.log('更新现有翻译元素的颜色:', translationTextColor); document.querySelectorAll('.microsoft-translated.replace-mode').forEach(element => { element.style.color = translationTextColor + ' !important'; element.style.setProperty('color', translationTextColor, 'important'); element.addEventListener('mouseenter', function() { this.style.backgroundColor = translationBgColor + ' !important'; }); element.addEventListener('mouseleave', function() { this.style.backgroundColor = 'transparent'; }); }); document.querySelectorAll('.microsoft-translated.dual-mode .translated-text').forEach(element => { element.style.color = translationTextColor + ' !important'; element.style.setProperty('color', translationTextColor, 'important'); }); } // =============== 微软翻译器相关函数 =============== // 获取微软翻译认证token function getMicrosoftAuthToken() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://edge.microsoft.com/translate/auth", onload: function(res) { if (res.status === 200) { resolve(res.responseText); } else { reject(new Error('获取微软认证token失败')); } }, onerror: function(res) { reject(new Error('网络错误')); } }); }); } // 微软翻译函数(支持批量) function translateMicrosoftBatch(texts, fromLang, toLang) { return new Promise((resolve, reject) => { if (!microsoftAuthToken) { reject(new Error('未获取到微软认证token')); return; } if (!texts || texts.length === 0) { reject(new Error('翻译文本为空')); return; } const fromParam = fromLang ? `&from=${fromLang}` : ''; const translateData = texts.map(text => ({ "Text": text })); GM_xmlhttpRequest({ method: "POST", url: `https://api-edge.cognitive.microsofttranslator.com/translate?to=${toLang}${fromParam}&api-version=3.0&includeSentenceLength=true`, headers: { "Authorization": `Bearer ${microsoftAuthToken}`, "Content-Type": "application/json", }, data: JSON.stringify(translateData), onload: function(res) { if (res.status === 200) { try { const result = JSON.parse(res.responseText); const translations = result.map(item => item && item.translations && item.translations[0] ? item.translations[0].text : null ); resolve(translations); } catch (e) { reject(new Error('解析微软响应失败')); } } else { reject(new Error(`微软翻译失败: ${res.status}`)); } }, onerror: function(res) { reject(new Error('微软网络请求失败')); } }); }); } // 设置微软翻译器 async function setupMicrosoftTranslator() { if (microsoftTranslatorActive) return; try { microsoftAuthToken = await getMicrosoftAuthToken(); console.log('微软翻译认证成功'); microsoftTranslatorActive = true; } catch (error) { console.error('微软翻译初始化失败:', error); } } // 翻译整个页面 async function translateWholePage() { if (pageTranslating) { console.log('翻译正在进行中...'); return; } if (!microsoftTranslatorActive || !microsoftAuthToken) { console.log('微软翻译未初始化,跳过翻译'); return; } try { pageTranslating = true; if (showTranslationIndicator) { showTranslatingIndicator('正在翻译页面...'); } let textNodes; if (isRussianContent && currentRussianSiteConfig) { console.log('使用俄语网站优化翻译...'); textNodes = collectTextNodesForRussianSite(); } else { textNodes = collectTextNodes(); } if (textNodes.length === 0) { console.log('无可翻译文本'); pageTranslating = false; if (showTranslationIndicator) { showTranslatingIndicator('翻译完成'); setTimeout(() => { hideTranslatingIndicator(); }, CONFIG.TRANSLATING_INDICATOR_DURATION); } return; } console.log(`找到 ${textNodes.length} 个文本节点需要翻译`); const batches = createTextBatches(textNodes); let translatedCount = 0; for (let i = 0; i < batches.length; i++) { if (!pageTranslating) break; const batch = batches[i]; try { const textsToTranslate = []; const nodeIndices = []; batch.nodes.forEach((nodeInfo, index) => { const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`; if (translationCache.has(cacheKey)) { const translation = translationCache.get(cacheKey); applyTranslation(nodeInfo, translation); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(index); } }); if (textsToTranslate.length > 0) { const fromLang = isRussianContent && currentRussianSiteConfig && currentRussianSiteConfig.forceRussianSource ? 'ru' : null; const translations = await translateMicrosoftBatch(textsToTranslate, fromLang, targetLanguage); translations.forEach((translation, transIndex) => { const nodeIndex = nodeIndices[transIndex]; const nodeInfo = batch.nodes[nodeIndex]; if (translation && translation !== nodeInfo.text) { applyTranslation(nodeInfo, translation); const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`; translationCache.set(cacheKey, translation); translatedCount++; } }); } if (i < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 20)); } } catch (error) { console.error(`第 ${i + 1} 批翻译失败:`, error); if (translationRetryCount < CONFIG.MAX_RETRY_COUNT) { translationRetryCount++; console.log(`翻译失败,第${translationRetryCount}次重试...`); await new Promise(resolve => setTimeout(resolve, 200)); continue; } } } pageTranslating = false; lastTranslationTime = Date.now(); translationRetryCount = 0; console.log(`微软翻译完成: 已翻译 ${translatedCount} 个文本`); if (showTranslationIndicator) { showTranslatingIndicator('翻译完成'); setTimeout(() => { hideTranslatingIndicator(); }, CONFIG.TRANSLATING_INDICATOR_DURATION); } setTimeout(() => { checkForUntranslatedText(); }, 500); } catch (error) { console.error('页面翻译失败:', error); pageTranslating = false; if (showTranslationIndicator) { showTranslatingIndicator('翻译失败'); setTimeout(() => { hideTranslatingIndicator(); }, CONFIG.TRANSLATING_INDICATOR_DURATION); } } } // 显示翻译状态指示器 function showTranslatingIndicator(message = '正在翻译页面...') { if (!showTranslationIndicator) return; if (indicatorHideTimeout) { clearTimeout(indicatorHideTimeout); indicatorHideTimeout = null; } if (currentTranslatingIndicator && currentTranslatingIndicator.parentNode) { const spinner = currentTranslatingIndicator.querySelector('.spinner'); const text = currentTranslatingIndicator.querySelector('span:last-child'); if (text) text.textContent = message; if (message === '正在翻译页面...') { if (!spinner) { const newSpinner = document.createElement('div'); newSpinner.className = 'spinner'; currentTranslatingIndicator.insertBefore(newSpinner, text); } else { spinner.style.display = 'block'; } currentTranslatingIndicator.style.background = 'rgba(0, 0, 0, 0.7)'; currentTranslatingIndicator.style.color = '#00ffcc'; currentTranslatingIndicator.style.borderColor = 'rgba(0, 255, 204, 0.3)'; } else if (message === '翻译完成') { if (spinner) spinner.style.display = 'none'; currentTranslatingIndicator.style.background = 'rgba(0, 184, 148, 0.7)'; currentTranslatingIndicator.style.color = '#ffffff'; currentTranslatingIndicator.style.borderColor = 'rgba(0, 184, 148, 0.3)'; } else if (message === '翻译失败') { if (spinner) spinner.style.display = 'none'; currentTranslatingIndicator.style.background = 'rgba(255, 71, 87, 0.7)'; currentTranslatingIndicator.style.color = '#ffffff'; currentTranslatingIndicator.style.borderColor = 'rgba(255, 71, 87, 0.3)'; } currentTranslatingIndicator.style.opacity = '0.9'; currentIndicatorText = message; return; } currentTranslatingIndicator = document.createElement('div'); currentTranslatingIndicator.className = 'translating-indicator'; currentTranslatingIndicator.setAttribute('translate', 'no'); currentTranslatingIndicator.setAttribute('data-no-translate', 'true'); currentTranslatingIndicator.setAttribute('data-protect-translation', 'true'); currentTranslatingIndicator.classList.add('no-translate', 'script-element'); if (message === '正在翻译页面...') { const spinner = document.createElement('div'); spinner.className = 'spinner'; currentTranslatingIndicator.appendChild(spinner); } const text = document.createElement('span'); text.textContent = message; text.setAttribute('data-no-translate', 'true'); text.setAttribute('translate', 'no'); text.setAttribute('data-protect-translation', 'true'); currentTranslatingIndicator.appendChild(text); if (message === '正在翻译页面...') { currentTranslatingIndicator.style.background = 'rgba(0, 0, 0, 0.7)'; currentTranslatingIndicator.style.color = '#00ffcc'; currentTranslatingIndicator.style.borderColor = 'rgba(0, 255, 204, 0.3)'; } else if (message === '翻译完成') { currentTranslatingIndicator.style.background = 'rgba(0, 184, 148, 0.7)'; currentTranslatingIndicator.style.color = '#ffffff'; currentTranslatingIndicator.style.borderColor = 'rgba(0, 184, 148, 0.3)'; } else if (message === '翻译失败') { currentTranslatingIndicator.style.background = 'rgba(255, 71, 87, 0.7)'; currentTranslatingIndicator.style.color = '#ffffff'; currentTranslatingIndicator.style.borderColor = 'rgba(255, 71, 87, 0.3)'; } document.body.appendChild(currentTranslatingIndicator); isTranslatingIndicatorVisible = true; currentIndicatorText = message; console.log('翻译状态提示已显示:', message); } // 隐藏翻译状态指示器 function hideTranslatingIndicator() { if (indicatorHideTimeout) { clearTimeout(indicatorHideTimeout); indicatorHideTimeout = null; } if (currentTranslatingIndicator && currentTranslatingIndicator.parentNode) { currentTranslatingIndicator.style.opacity = '0'; setTimeout(() => { if (currentTranslatingIndicator && currentTranslatingIndicator.parentNode) { currentTranslatingIndicator.parentNode.removeChild(currentTranslatingIndicator); currentTranslatingIndicator = null; isTranslatingIndicatorVisible = false; currentIndicatorText = ''; } }, CONFIG.TRANSLATING_INDICATOR_FADE_OUT); } else { isTranslatingIndicatorVisible = false; currentIndicatorText = ''; } } // 智能文本收集 function collectTextNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const excludedSelectors = [ 'SCRIPT', 'STYLE', 'NOSCRIPT', '.microsoft-translated', '.translating-indicator', '.translation-settings-modal', '[data-no-translate]', '.no-translate', '.script-element', '[data-protect-translation="true"]', '.tm-script-menu', '#tm-script-menu', '.vm-script-menu', '#vm-script-menu', '.gm-script-menu', '#gm-script-menu', '[class*="userscript"]', '[id*="userscript"]', '[class*="tampermonkey"]', '[id*="tampermonkey"]', '[class*="violentmonkey"]', '[id*="violentmonkey"]', '[class*="greasemonkey"]', '[id*="greasemonkey"]', '.engine-btn', '.site-engine-badge' ]; for (const selector of excludedSelectors) { if (parent.matches(selector) || parent.closest(selector)) { return NodeFilter.FILTER_REJECT; } } if (parent.hasAttribute('translate') && parent.getAttribute('translate') === 'no') { return NodeFilter.FILTER_REJECT; } if (parent.classList && (parent.classList.contains('no-translate') || parent.classList.contains('script-element'))) { return NodeFilter.FILTER_REJECT; } const text = node.textContent.trim(); if (!text || text.length < 2) return NodeFilter.FILTER_REJECT; if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT; if (parent.classList.contains('microsoft-translated')) return NodeFilter.FILTER_REJECT; if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text && text.length > 0) { textNodes.push({ node: node, text: text, parent: node.parentElement, originalText: node.textContent }); } } return textNodes; } // 收集文本节点(俄语网站优化版) function collectTextNodesForRussianSite() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const excludedSelectors = [ 'SCRIPT', 'STYLE', 'NOSCRIPT', '.microsoft-translated', '.translating-indicator', '.translation-settings-modal', '[data-no-translate]', '.no-translate', '.script-element', '[data-protect-translation="true"]', '.tm-script-menu', '#tm-script-menu', '.vm-script-menu', '#vm-script-menu', '.gm-script-menu', '#gm-script-menu', '[class*="userscript"]', '[id*="userscript"]', '[class*="tampermonkey"]', '[id*="tampermonkey"]', '[class*="violentmonkey"]', '[id*="violentmonkey"]', '[class*="greasemonkey"]', '[id*="greasemonkey"]', '.engine-btn', '.site-engine-badge' ]; for (const selector of excludedSelectors) { if (parent.matches(selector) || parent.closest(selector)) { return NodeFilter.FILTER_REJECT; } } if (parent.hasAttribute('translate') && parent.getAttribute('translate') === 'no') { return NodeFilter.FILTER_REJECT; } if (parent.classList && (parent.classList.contains('no-translate') || parent.classList.contains('script-element'))) { return NodeFilter.FILTER_REJECT; } if (currentRussianSiteConfig && currentRussianSiteConfig.excludeSelectors) { for (const selector of currentRussianSiteConfig.excludeSelectors) { if (parent.matches(selector) || parent.closest(selector)) { return NodeFilter.FILTER_REJECT; } } } const text = node.textContent.trim(); if (!text || text.length < 1) return NodeFilter.FILTER_REJECT; if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT; if (parent.classList.contains('microsoft-translated')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text && text.length > 0) { textNodes.push({ node: node, text: text, parent: node.parentElement, originalText: node.textContent }); } } return textNodes; } // 创建文本批次 function createTextBatches(textNodes) { const batches = []; let currentBatch = { nodes: [], totalLength: 0 }; for (const nodeInfo of textNodes) { const textLength = nodeInfo.text.length; if (textLength > CONFIG.MAX_TEXT_LENGTH) { if (currentBatch.nodes.length > 0) { batches.push(currentBatch); currentBatch = { nodes: [], totalLength: 0 }; } const truncatedText = nodeInfo.text.substring(0, CONFIG.MAX_TEXT_LENGTH); batches.push({ nodes: [{...nodeInfo, text: truncatedText}], totalLength: truncatedText.length }); continue; } if (currentBatch.totalLength + textLength > CONFIG.MAX_TEXT_LENGTH || currentBatch.nodes.length >= CONFIG.BATCH_SIZE) { batches.push(currentBatch); currentBatch = { nodes: [], totalLength: 0 }; } currentBatch.nodes.push(nodeInfo); currentBatch.totalLength += textLength; } if (currentBatch.nodes.length > 0) { batches.push(currentBatch); } return batches; } // 应用翻译结果 function applyTranslation(nodeInfo, translation) { try { const originalNode = nodeInfo.node; const parent = nodeInfo.parent; const originalText = nodeInfo.originalText || originalNode.textContent; const elementId = generateElementId(); originalTextMap.set(elementId, originalText); const translatedElement = document.createElement('span'); translatedElement.className = `microsoft-translated ${translationMode}-mode`; translatedElement.dataset.originalId = elementId; translatedElement.setAttribute('translate', 'no'); translatedElement.setAttribute('data-no-translate', 'true'); translatedElement.setAttribute('data-protect-translation', 'true'); translatedElement.classList.add('no-translate', 'script-element'); if (translationMode === CONFIG.MODES.REPLACE) { translatedElement.textContent = translation; translatedElement.title = `原文: ${originalText}\n点击恢复原文`; translatedElement.style.cssText += `color: ${translationTextColor} !important;`; translatedElement.addEventListener('click', function(e) { e.stopPropagation(); const originalId = this.dataset.originalId; const original = originalTextMap.get(originalId); if (original) { const textNode = document.createTextNode(original); parent.replaceChild(textNode, this); } }); translatedElement.addEventListener('mouseenter', function() { this.style.cssText += `background-color: ${translationBgColor} !important;`; }); translatedElement.addEventListener('mouseleave', function() { this.style.cssText = `color: ${translationTextColor} !important;`; }); } else { const originalSpan = document.createElement('span'); originalSpan.className = 'original-text'; originalSpan.textContent = originalText; originalSpan.setAttribute('data-no-translate', 'true'); originalSpan.setAttribute('translate', 'no'); originalSpan.setAttribute('data-protect-translation', 'true'); originalSpan.classList.add('no-translate', 'script-element'); const translatedSpan = document.createElement('span'); translatedSpan.className = 'translated-text'; translatedSpan.textContent = translation; translatedSpan.style.cssText += `color: ${translationTextColor} !important;`; translatedSpan.setAttribute('data-no-translate', 'true'); translatedSpan.setAttribute('translate', 'no'); translatedSpan.setAttribute('data-protect-translation', 'true'); translatedSpan.classList.add('no-translate', 'script-element'); translatedElement.appendChild(originalSpan); translatedElement.appendChild(translatedSpan); } parent.replaceChild(translatedElement, originalNode); } catch (error) { console.error('应用翻译失败:', error); } } // 生成元素ID function generateElementId() { return 'translated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } // 检查是否有未翻译的文本 function checkForUntranslatedText() { if (!microsoftTranslatorActive || pageTranslating) return; const untranslatedNodes = collectUntranslatedNodes(); if (untranslatedNodes.length > 0) { console.log(`发现 ${untranslatedNodes.length} 个未翻译文本,重新翻译...`); if (untranslatedNodes.length > 10) { translateWholePage(); } else { translateSpecificNodes(untranslatedNodes); } } } // 收集未翻译的文本节点 function collectUntranslatedNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const excludedSelectors = [ 'SCRIPT', 'STYLE', 'NOSCRIPT', '.microsoft-translated', '.translating-indicator', '.translation-settings-modal', '[data-no-translate]', '.no-translate', '.script-element', '[data-protect-translation="true"]', '.tm-script-menu', '#tm-script-menu', '.vm-script-menu', '#vm-script-menu', '.gm-script-menu', '#gm-script-menu', '[class*="userscript"]', '[id*="userscript"]', '[class*="tampermonkey"]', '[id*="tampermonkey"]', '[class*="violentmonkey"]', '[id*="violentmonkey"]', '[class*="greasemonkey"]', '[id*="greasemonkey"]', '.engine-btn', '.site-engine-badge' ]; for (const selector of excludedSelectors) { if (parent.matches(selector) || parent.closest(selector)) { return NodeFilter.FILTER_REJECT; } } if (parent.hasAttribute('translate') && parent.getAttribute('translate') === 'no') { return NodeFilter.FILTER_REJECT; } if (parent.classList && (parent.classList.contains('no-translate') || parent.classList.contains('script-element'))) { return NodeFilter.FILTER_REJECT; } const text = node.textContent.trim(); if (!text || text.length < 2) return NodeFilter.FILTER_REJECT; if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT; if (parent.classList.contains('microsoft-translated')) return NodeFilter.FILTER_REJECT; if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text && text.length > 0) { textNodes.push({ node: node, text: text, parent: node.parentElement, originalText: node.textContent }); } } return textNodes; } // 翻译特定的文本节点 async function translateSpecificNodes(textNodes) { if (!microsoftTranslatorActive || !microsoftAuthToken || pageTranslating) return; try { pageTranslating = true; if (showTranslationIndicator) { showTranslatingIndicator('正在翻译页面...'); } const batches = createTextBatches(textNodes); let translatedCount = 0; for (let i = 0; i < batches.length; i++) { if (!pageTranslating) break; const batch = batches[i]; const textsToTranslate = []; const nodeIndices = []; batch.nodes.forEach((nodeInfo, index) => { const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`; if (translationCache.has(cacheKey)) { const translation = translationCache.get(cacheKey); applyTranslation(nodeInfo, translation); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(index); } }); if (textsToTranslate.length > 0) { const fromLang = isRussianContent && currentRussianSiteConfig && currentRussianSiteConfig.forceRussianSource ? 'ru' : null; const translations = await translateMicrosoftBatch(textsToTranslate, fromLang, targetLanguage); translations.forEach((translation, transIndex) => { const nodeIndex = nodeIndices[transIndex]; const nodeInfo = batch.nodes[nodeIndex]; if (translation && translation !== nodeInfo.text) { applyTranslation(nodeInfo, translation); const cacheKey = `${nodeInfo.text}|${targetLanguage}|${translationMode}`; translationCache.set(cacheKey, translation); translatedCount++; } }); } if (i < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 20)); } } pageTranslating = false; if (translatedCount > 0) { console.log(`已翻译 ${translatedCount} 个未翻译文本`); if (showTranslationIndicator) { showTranslatingIndicator('翻译完成'); setTimeout(() => hideTranslatingIndicator(), CONFIG.TRANSLATING_INDICATOR_DURATION); } } else { if (showTranslationIndicator) { showTranslatingIndicator('翻译完成'); setTimeout(() => hideTranslatingIndicator(), CONFIG.TRANSLATING_INDICATOR_DURATION); } } } catch (error) { console.error('翻译特定节点失败:', error); pageTranslating = false; if (showTranslationIndicator) { showTranslatingIndicator('翻译失败'); setTimeout(() => hideTranslatingIndicator(), CONFIG.TRANSLATING_INDICATOR_DURATION); } } } // =============== DOM变化监听相关函数 =============== function startDOMObservation() { if (isObservingDOM || !microsoftTranslatorActive) return; stopDOMObservation(); const observerConfig = { childList: true, subtree: true, characterData: true }; domObserver = new MutationObserver((mutations) => { if (pageTranslating) return; if (domChangeTimer) clearTimeout(domChangeTimer); domChangeTimer = setTimeout(() => { let hasSignificantChanges = false; mutations.forEach((mutation) => { if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) { hasSignificantChanges = true; } if (mutation.type === 'characterData' && mutation.target.nodeType === Node.TEXT_NODE) { hasSignificantChanges = true; } }); if (hasSignificantChanges) { console.log('检测到DOM变化,重新翻译...'); smartEngineDetection(); setTimeout(() => translateWholePage(), 300); } }, CONFIG.DOM_CHANGE_DELAY); }); try { domObserver.observe(document.body, observerConfig); isObservingDOM = true; } catch (error) { console.error('DOM观察器启动失败:', error); } } function stopDOMObservation() { if (domObserver) { domObserver.disconnect(); domObserver = null; } if (domChangeTimer) { clearTimeout(domChangeTimer); domChangeTimer = null; } isObservingDOM = false; } function startContinuousTranslation() { stopContinuousTranslation(); if (!microsoftTranslatorActive) return; console.log('开始持续翻译监控'); continuousTranslationTimer = setInterval(() => { if (!pageTranslating && microsoftTranslatorActive) { const now = Date.now(); if (now - lastTranslationTime > 20000) { console.log('长时间未翻译,检查未翻译文本...'); checkForUntranslatedText(); } } }, 8000); startScrollCheck(); } function stopContinuousTranslation() { if (continuousTranslationTimer) { clearInterval(continuousTranslationTimer); continuousTranslationTimer = null; } stopScrollCheck(); } function startScrollCheck() { stopScrollCheck(); if (!microsoftTranslatorActive) return; lastScrollY = window.scrollY; scrollCheckCount = 0; window.addEventListener('scroll', handleScroll); } function stopScrollCheck() { window.removeEventListener('scroll', handleScroll); if (scrollCheckTimer) { clearTimeout(scrollCheckTimer); scrollCheckTimer = null; } } function handleScroll() { const currentScrollY = window.scrollY; const scrollDelta = Math.abs(currentScrollY - lastScrollY); if (scrollDelta > 300) { scrollCheckCount++; lastScrollY = currentScrollY; if (scrollCheckTimer) clearTimeout(scrollCheckTimer); scrollCheckTimer = setTimeout(() => { if (!pageTranslating && microsoftTranslatorActive) { console.log(`检测到翻页行为,检查翻译...`); checkForUntranslatedText(); } }, CONFIG.SCROLL_CHECK_DELAY); } } // 停用微软翻译器 function deactivateMicrosoftTranslator() { stopAllObservations(); document.querySelectorAll('.microsoft-translation-popup').forEach(popup => popup.remove()); document.querySelectorAll('.microsoft-translated').forEach(translatedElement => { const originalId = translatedElement.dataset.originalId; if (originalId) { const originalText = originalTextMap.get(originalId); if (originalText) { const textNode = document.createTextNode(originalText); translatedElement.parentNode.replaceChild(textNode, translatedElement); } } }); microsoftTranslatorActive = false; microsoftAuthToken = null; pageTranslating = false; originalTextMap.clear(); translationRetryCount = 0; lastTranslationTime = 0; } // 切换主开关 function toggleWidget() { if (isWhitelistedDomain) { widgetEnabled = !widgetEnabled; GM_setValue(CONFIG.storageKey, widgetEnabled); showStatusMessage(widgetEnabled ? '自动翻译已开启' : '自动翻译已关闭', 2000); } else { widgetEnabled = !widgetEnabled; GM_setValue(CONFIG.storageKey, widgetEnabled); if (widgetEnabled) { console.log('开启翻译,立即更新设置'); updateWidgetBasedOnSettings(); let message = `微软翻译已开启`; if (isUsingSiteSpecificEngine) message += ' (网站专用引擎)'; showStatusMessage(message, 2000); } else { console.log('关闭翻译,立即停用'); deactivateAllTranslators(); showStatusMessage('自动翻译已关闭', 2000); } } registerMenuCommands(); } // 设置键盘快捷键 function setupHotkey() { document.addEventListener('keydown', function(e) { if (isModalOpen) return; if (e.ctrlKey && e.shiftKey && e.key === 'Y') { e.preventDefault(); toggleWidget(); } if (e.ctrlKey && e.shiftKey && e.key === 'W') { e.preventDefault(); toggleWhitelist(); } if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); showSettings(); } if (e.ctrlKey && e.shiftKey && e.key === 'T') { e.preventDefault(); if (microsoftTranslatorActive) { translateWholePage(); } } }, true); } // 显示状态提示 function showStatusMessage(message, duration = 2000) { const existingMsg = document.getElementById('microsoft-status-msg'); if (existingMsg) existingMsg.remove(); const msgDiv = document.createElement('div'); msgDiv.id = 'microsoft-status-msg'; msgDiv.innerHTML = message; msgDiv.setAttribute('data-no-translate', 'true'); msgDiv.setAttribute('translate', 'no'); msgDiv.setAttribute('data-protect-translation', 'true'); msgDiv.className = 'no-translate script-element'; msgDiv.style.cssText = ` position: fixed; top: 20px; right: 20px; background: rgba(0, 0, 0, 0.9); color: white; padding: 12px 20px; border-radius: 10px; z-index: 2147483646; font-family: Arial, sans-serif; font-size: 14px; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(5px); border: 1px solid rgba(255, 255, 255, 0.1); opacity: 1; transform: translateY(0); max-width: 300px; word-wrap: break-word; `; if (message.includes('白名单')) { msgDiv.style.background = 'rgba(255, 193, 7, 0.95)'; msgDiv.style.color = '#000'; } else if (message.includes('微软')) { msgDiv.style.background = 'rgba(0, 120, 215, 0.95)'; } else if (message.includes('俄语内容') || message.includes('俄语网站')) { msgDiv.style.background = 'rgba(255, 61, 0, 0.95)'; } else if (message.includes('专用引擎')) { msgDiv.style.background = 'rgba(108, 92, 231, 0.95)'; } else if (message.includes('翻译中') || message.includes('翻译完成') || message.includes('✓')) { msgDiv.style.background = 'rgba(0, 184, 148, 0.95)'; } else if (message.includes('✗') || message.includes('失败')) { msgDiv.style.background = 'rgba(255, 71, 87, 0.95)'; } else if (message.includes('颜色')) { msgDiv.style.background = 'rgba(255, 159, 67, 0.95)'; } document.body.appendChild(msgDiv); setTimeout(function() { msgDiv.classList.add('fade-out'); setTimeout(function() { if (msgDiv.parentNode) msgDiv.parentNode.removeChild(msgDiv); }, 500); }, duration); } // 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();