// ==UserScript== // @name 智能翻译助手 // @name:zh-CN 智能翻译助手 // @name:zh-TW 智能翻譯助手 // @name:zh-HK 智能翻譯助手 // @name:en Intelligent Translation Assistant // @name:es Asistente de Traducción Inteligente // @name:fr Assistant de Traduction Intelligent // @name:de Intelligenter Übersetzungsassistent // @name:it Assistente di Traduzione Intelligente // @name:pt Assistente de Tradução Inteligente // @name:pt-BR Assistente de Tradução Inteligente // @name:ru Интеллектуальный Переводчик // @name:ja インテリジェント翻訳アシスタント // @name:ko 지능형 번역 도우미 // @name:ar مساعد الترجمة الذكي // @name:tr Akıllı Çeviri Asistanı // @name:nl Intelligente Vertaalassistent // @name:pl Inteligentny Asystent Tłumaczenia // @name:sv Intelligent Översättningsassistent // @name:da Intelligent Oversættelsesassistent // @name:fi Älykäs Käännösavustaja // @name:no Intelligent Oversettelsesassistent // @name:cs Inteligentní Překladový Asistent // @name:hu Intelligens Fordító Asszisztens // @name:ro Asistent de Traducere Inteligent // @name:sk Inteligentný Prekladový Asistent // @name:uk Інтелектуальний Помічник Перекладу // @name:bg Интелигентен Преводачески Асистент // @name:el Έξυπνος Βοηθός Μετάφρασης // @name:he עוזר תרגום חכם // @name:hi इंटेलिजेंट ट्रांसलेशन असिस्टेंट // @name:th ผู้ช่วยแปลอัจฉริยะ // @name:vi Trợ lý Dịch thuật Thông minh // @name:id Asisten Terjemahan Cerdas // @name:ms Pembantu Terjemahan Pintar // @name:tl Matalinong Katulong sa Pagsasalin // @namespace https://github.com/eraycc // @version 1.3.4 // @description 功能强大的网页翻译工具,支持多语言,可自定义配置,界面精美,支持移动端 // @description:zh-CN 功能强大的网页翻译工具,支持多语言,可自定义配置,界面精美,支持移动端 // @description:zh-TW 功能強大的網頁翻譯工具,支援多語言,可自訂配置,介面精美,支援行動端 // @description:zh-HK 功能強大的網頁翻譯工具,支援多語言,可自訂配置,介面精美,支援行動端 // @description:en Powerful web translation tool, supports multiple languages, customizable configuration, beautiful interface, mobile support // @description:es Potente herramienta de traducción web, admite múltiples idiomas, configuración personalizable, interfaz elegante, compatible con dispositivos móviles // @description:fr Outil de traduction web puissant, prend en charge plusieurs langues, configuration personnalisable, interface élégante, prise en charge mobile // @description:de Leistungsstarkes Web-Übersetzungstool, unterstützt mehrere Sprachen, anpassbare Konfiguration, schöne Oberfläche, Mobilgeräte-Unterstützung // @description:it Potente strumento di traduzione web, supporta più lingue, configurazione personalizzabile, interfaccia elegante, supporto mobile // @description:pt Ferramenta de tradução web poderosa, suporta vários idiomas, configuração personalizável, interface bonita, suporte móvel // @description:pt-BR Ferramenta de tradução web poderosa, suporta vários idiomas, configuração personalizável, interface bonita, suporte móvel // @description:ru Мощный веб-инструмент перевода, поддерживает несколько языков, настраиваемая конфигурация, красивый интерфейс, поддержка мобильных устройств // @description:ja 強力なウェブ翻訳ツール、多言語対応、カスタマイズ可能な設定、美しいインターフェース、モバイルサポート // @description:ko 강력한 웹 번역 도구, 다국어 지원, 사용자 정의 구성, 아름다운 인터페이스, 모바일 지원 // @description:ar أداة ترجمة ويب قوية، تدعم لغات متعددة، تكوين قابل للتخصيص، واجهة جميلة، دعم للأجهزة المحمولة // @description:tr Güçlü web çeviri aracı, çoklu dil desteği, özelleştirilebilir yapılandırma, güzel arayüz, mobil destek // @description:nl Krachtige webvertaaltool, ondersteunt meerdere talen, aanpasbare configuratie, mooie interface, ondersteuning voor mobiele apparaten // @description:pl Potężne narzędzie do tłumaczenia stron internetowych, obsługuje wiele języków, konfigurowalne ustawienia, piękny interfejs, obsługa urządzeń mobilnych // @description:sv Kraftfull webböversättningsverktyg, stöder flera språk, anpassningsbar konfiguration, vackert gränssnitt, mobilstöd // @description:da Kraftfuldt weboversættelsesværktøj, understøtter flere sprog, tilpasningsbar konfiguration, smukt interface, mobilunderstøttelse // @description:fi Tehokas verkkokäännöstyökalu, tukee useita kieliä, mukautettava määritys, kaunis käyttöliittymä, mobiilituki // @description:no Kraftig nettoversettelsesverktøy, støtter flere språk, tilpassbar konfigurasjon, vakkert grensesnitt, mobilstøtte // @description:cs Výkonný nástroj pro webový překlad, podporuje více jazyků, přizpůsobitelná konfigurace, krásné rozhraní, podpora mobilních zařízení // @description:hu Hatékony webes fordítási eszköz, támogatja a többnyelvűséget, testreszabható konfiguráció, szép felület, mobil támogatás // @description:ro Instrument puternic de traducere web, suportă mai multe limbi, configurație personalizabilă, interfață frumoasă, suport pentru dispozitive mobile // @description:sk Výkonný nástroj pre webový preklad, podporuje viac jazykov, prispôsobiteľná konfigurácia, krásne rozhranie, podpora mobilných zariadení // @description:uk Потужний веб-інструмент перекладу, підтримує кілька мов, настроювана конфігурація, красивий інтерфейс, підтримка мобільних пристроїв // @description:bg Мощен уеб преводачески инструмент, поддържа множество езици, персонализируема конфигурация, красив интерфейс, поддръжка на мобилни устройства // @description:el Ισχυρό εργαλείο μετάφρασης ιστού, υποστηρίζει πολλές γλώσσες, προσαρμόσιμη διαμόρφωση, όμορφη διεπαφή, υποστήριξη κινητών // @description:he כלי תרגום אינטרנט חזק, תומך במספר שפות, תצורה ניתנת להתאמה אישית, ממשק יפה, תמיכה בנייד // @description:hi शक्तिशाली वेब अनुवाद उपकरण, कई भाषाओं का समर्थन करता है, अनुकूलन योग्य विन्यास, सुंदर इंटरफेस, मोबाइल समर्थन // @description:th เครื่องมือแปลเว็บที่มีประสิทธิภาพ รองรับหลายภาษา การกำหนดค่าที่ปรับแต่งได้ อินเทอร์เฟซที่สวยงาม การรองรับมือถือ // @description:vi Công cụ dịch web mạnh mẽ, hỗ trợ đa ngôn ngữ, cấu hình tùy chỉnh, giao diện đẹp, hỗ trợ di động // @description:id Alat terjemahan web yang kuat, mendukung banyak bahasa, konfigurasi yang dapat disesuaikan, antarmuka yang indah, dukungan seluler // @description:ms Alat terjemahan web yang berkuasa, menyokong pelbagai bahasa, konfigurasi boleh suai, antara muka yang cantik, sokongan mudah alih // @description:tl Makapangyarihang tool sa pagsasalin ng web, sumusuporta sa maraming wika, naaayos na pagsasaayos, magandang interface, suporta sa mobile // @author Eray // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAABvBAMAAAApjDIIAAAA82VYSWZNTQAqAAAACAAFAQAAAwAAAAEAcQAAAQEAAwAAAAEAbwAAATEAAgAAABcAAABKh2kABAAAAAEAAABhARIABAAAAAEAAAAAAAAAAEFuZHJvaWQgUEQyMTE4X0FfOS4zLjEAAASQAwACAAAAFAAAAJeSkQACAAAABDc4MgCQEQACAAAABwAAAKuSCAAEAAAAAQAAAAAAAAAAMjAyNTowODoxOSAyMjoxMjoxMAArMDg6MDAAAAMBAAADAAAAAQBxAAABMQACAAAAFwAAANwBAQADAAAAAQBvAAAAAAAAQW5kcm9pZCBQRDIxMThfQV85LjMuMQA+yDmGAAAAFVBMVEVuY8VtacxwXbxub9P///+3s+Pp6Pd/O4+7AAAGzklEQVRYw41Yy3IcNwwEqEhnTljKmTUp57zjjXyOotGeV9bj7NiK/v8TghdJkDu7MWVLpSr1NtBoYMCB/eZJyz6lNC1p6k+cECGCnC3UsoXDSGeCGAlKZwOZ+EvRE31fOjTSP2Ld5Ez8Py3MOXlUFMoI/AW4GS1hhDPxmfhL2JRSGClUPEUSbL/o6dVhjPBSnqrRGU5RaMiShUUQzniCpGqIrgTrsmxxRqvJKeciVdE0CZJ8WYRTNRqRzKecPQyNEyFWIJy3T6pQrFlKyFoUGMSxM20oW5Q5l2cqJdGAhSwqr6IjniKXvTm2GaBA2bEADTVyLnvn2JYmc6pjIbZWgb6WRjk4Vv61cmxyCmVxbBWWfUflVJHsZOikXfbbjp1UG0ZgAYJPcl9rsiRHycGarltVqcIOjhVdyT0IXtcMPadkuRTvFbNymtolrG2BtjxTETalvrNinHoH0Amdtsk7dmmORW0S6NChIltvDY6NpZ2bAwiWGdpxFmia7l5fF+GTaalTNnqFLE+zgIwPETa9fXw8mdtbY6IHwtCXOn6m6WVdnzjBWJVtnITKNVrXlVpMQVqs3nRKmIMh09JGgYWrSJs9XEr0aQbzbRuxTVeNtozY2IvTOBcGE+Ptaz2E/Lf9ths42ULAht2b57581LOua/vl4+gwQY5VZbFHHxFtn103RjRicLVMf55DHl1fZmEsjtfpM13gHAg5z6VO2ekip1WDCZlzVs5kEr19DOd9XQ/8c4e9rkIqo2BSy6ZxbFMQD/wzOliWLBnJT0uxDz841ROlOfGlBNrmFoTqBBnMfVvGyaYdIxEHbSRL42QrDGMEpcHeRdbRPnxm4ZQUhxkrRr9e10fourKlyZxcl/00kEp/3ZBAWBaKOkk4yyBIHbEVFXkcaI+8tEoOWc5BOJnUQbkU0pggaSL68ayxzp6zrQW3VH7GRU3T4UwcaZbs3FdI0/t6eGLON0uzQQP7nT5gFm1PRyyl9w+X892nGQqlxMp5qmfcHInTbxQl6XPNTYLQpal9bfXUseUfJAx5gvhW06R5chRpZzDSeWbOve0hFY4U5g+8ou/PSknhP4A8EkwhcYJbmMoTOn5hdW85WOXkZrNyMoz0Ec6NJYbDPb7UYPnXwy4UYbUuGfQB5CijdtcjB6vy3PBY+CbBiqzGWbYC/5TGO5khBwtWhsw3HQSqkFQljXssW53MsIoqcmSwPcj0Yc7ZOKfFbwW66U/6x0crpnzMQf2uJwtyXH5kGnBqB2uuTzr/LEtVVqNtewxGvdEY53OVluXaaZZSlnDKacvvrQ1oMGnvuVM5RM1zVm07VfVCE23Y/ygCHX9hvcR/YlquDEx+GdUVT4pyKKSZW+YPiljC1EhLPbsNj+Ds2PX5xUiv6Kc0AZQGs2hPt0ok266Pu1slzTIZZvqwzyG0cnqFsGwwYoNnuDLSGw6Uk/0eznLKVgCTUgJwto8QCHMPM33/yymryNSE5TGrWXKCghFpc+DRq8rOpcv6O20sWXI57ngOsbQxZzLS42yB9tGWpbJkCUp6FAfx43ZliXKu4MZpS+XUKCHfHYDDfGQH0Ad+V1F7pGWJJUsdeFev0pz3NDFnFbfFW5CxWNZR0oxdRNqDPfgfpEnGaOVCQqdlqdNVm1PPIecThcw8qBNo50bzJ7+i1CQrEttWedeylMfItUd+nk847dKOXAlPKQ5o5+tYT5Rq2p327rl77nVr2YMLt0RbOCNc7drDSwV61t2IB/8QLbZbUES35/PO8zt3mv7pJ0aGPtpyCwK3iwZ9RrNd5flDf/veSUTIWG5B/tpV9jv2Xubeoq5mF3lk1DuJvveBbvuheLktZ+mteSYf/j1oGxtnjN0+EbQ5deLdmP9atPr+pb9EZ9YnSHP+WnyuLeqQWKrS3Q90oeDmtElA4M5FHC0/DYZbRZbFGTKP2TZ9KICvHSfGliV2nIFuhE9t4NGi8XWoynDx4nVdlvWMaYq+JSF0VcFyeqjV3zYR118tWr13uVpmAfLzErYwXbR9LSun7ltz9tPSc0L/gkGvpbrHzmUT2ULy7AJ0VQliWeUEHepbERMnDO8KtL3siXeZc3jvk7Wly1awjWMk+mYuu2hZl4w2b3PW13E11qCTVra7+RInuqbUNwVSyNn2gq2SFCRi924su11fOMMZpL/IlPulLqLzhWDZxDFif4n2e+zFPCVJ9Pe1fsO7WBU/ffQa5Da8fD7afvronWK+4IGKxOEtVS63PblFnamJaev7MgRXkf/lxPEKpW0ZLqJzZx/jlPvl2TAdZwfVWs7FCxeRLtJQHRt+grN1SQj1dqodtj2ACvI/wZeKnkWuZZ8AAAAASUVORK5CYII= // @run-at document-start // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect * // @grant unsafeWindow // @license Apache-2.0 // @supportURL https://github.com/eraycc/Universal-Translation-JavaScript-Plugin/issues // @homepageURL https://scriptcat.org/zh-CN/script-show-page/5633 // @require https://unpkg.com/i18n-jsautotranslate@4.0.0/index.js // ==/UserScript== (function() { 'use strict'; function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } function isDarkMode() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function normalizeShortcut(value) { const tokens = String(value || '').split('+').map(item => item.trim()).filter(Boolean); if (tokens.length < 2) return ''; const modifierMap = { ctrl: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Meta' }; const modifiers = []; let key = ''; tokens.forEach(token => { const modifier = modifierMap[token.toLowerCase()]; if (modifier) { if (!modifiers.includes(modifier)) modifiers.push(modifier); } else { key = token; } }); if (!modifiers.length || !key) return ''; const ordered = ['Ctrl', 'Alt', 'Shift', 'Meta'].filter(item => modifiers.includes(item)); key = key === ' ' ? 'Space' : key; key = key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1); return [...ordered, key].join('+'); } function shortcutFromEvent(event) { if (!event || ['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return ''; const parts = []; if (event.ctrlKey) parts.push('Ctrl'); if (event.altKey) parts.push('Alt'); if (event.shiftKey) parts.push('Shift'); if (event.metaKey) parts.push('Meta'); let key = event.key === ' ' ? 'Space' : event.key; parts.push(key); return normalizeShortcut(parts.join('+')); } function buildDefaultConfig() { return { version: '1.3.4', enabled: true, localLanguage: 'english', targetLanguage: 'chinese_simplified', autoTranslate: false, translateService: 'client.edge', customServiceUrls: '', showFloatBall: true, floatBallSize: isMobile() ? 45 : 50, floatBallPosition: { x: 20, y: 100 }, floatBallOpacity: 0.8, allowHalfBall: true, doubleClickToggle: true, enableKeyboardShortcut: true, keyboardShortcut: 'Shift+T', siteListMode: 'blacklist', siteListRules: '', panelPosition: null, panelSize: isMobile() ? 0.9 : 1, panelOpacity: 1, ignoredClasses: '', ignoredIds: '', ignoredTags: '', ignoredTexts: '', customTerms: '', enableListener: true, enableCache: true, enableAjaxListener: true, ajaxDelayExecuteTime: 200, ajaxMinIntervalTime: 800, waitingExecute: true, translateAttributes: ['title', 'alt', 'placeholder'], customTagAttributes: '', // 实验性划词翻译:官方 setDefaultTo 默认值 + 独立请求目标 enableSelectionTranslate: false, selectionDefaultTo: 'chinese_simplified', // 高级 enableWholeAll: false, translateLocal: false, translateLanguagesRange: '', hideTextBeforeTranslate: false, enableProgressTip: false, enableSSE: false, enableUrlLanguageControl: false, urlLanguageParam: 'language', imageRules: '', enableIframe: false, iframeTranslateJsUrl: 'https://unpkg.com/i18n-jsautotranslate@4.0.0/index.js', appendParams: '', appendHeaders: '' }; } function loadCfg() { const def = buildDefaultConfig(); const saved = GM_getValue('translateConfig', null); if (!saved || typeof saved !== 'object') return def; const config = { ...def, ...saved, version: def.version }; // 版本迁移:迁移旧版划词配置,并清理已经移除的自动源语言识别开关。 if (!Object.prototype.hasOwnProperty.call(saved, 'selectionDefaultTo') && saved.selectionTargetLanguage) { config.selectionDefaultTo = saved.selectionTargetLanguage; } delete config.selectionLocalLanguage; delete config.selectionTargetLanguage; delete config.autoDetectLocalLanguage; return config; } function injectGlobalStyles() { const mobile = isMobile(); const dark = isDarkMode(); GM_addStyle(` #translate-float-ball{ position:fixed;border-radius:50%; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); box-shadow:0 4px 15px rgba(102,126,234,.4); cursor:${mobile ? 'pointer' : 'move'}; z-index:2147483647;align-items:center;justify-content:center; transition:transform .3s ease,box-shadow .3s ease; user-select:none;-webkit-user-select:none;touch-action:none; } #translate-float-ball.tt-show{display:flex} #translate-float-ball.tt-hide{display:none} #translate-float-ball svg{ width:${mobile ? '24px' : '28px'};height:${mobile ? '24px' : '28px'}; fill:#fff;pointer-events:none; } @keyframes slideInRight{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}} @keyframes slideOutRight{from{transform:translateX(0);opacity:1}to{transform:translateX(100%);opacity:0}} @keyframes ttSlideIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}} #translate-toast-container{position:fixed;top:20px;right:20px;z-index:2147483647;pointer-events:none} .tt-toast{color:#fff;padding:12px 20px;border-radius:8px;margin-bottom:10px;box-shadow:0 4px 12px rgba(0,0,0,.15);animation:slideInRight .3s ease;pointer-events:auto;font-size:14px} .tt-toast.tt-success{background:#4caf50} .tt-toast.tt-error{background:#f44336} .tt-toast.tt-info{background:#2196f3} .tt-toast.tt-out{animation:slideOutRight .3s ease} #translate-panel{ position:fixed;background:#fff;border-radius:12px; box-shadow:0 10px 40px rgba(0,0,0,.15);z-index:2147483646;display:none; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; overflow:hidden;touch-action:none;user-select:none;-webkit-user-select:none; } #translate-panel.show{display:block;animation:ttSlideIn .3s ease} #translate-panel.dragging{transition:none !important;box-shadow:0 15px 50px rgba(0,0,0,.2)} #translate-float-ball.dragging{transition:none !important;transform:scale(1.1);box-shadow:0 6px 20px rgba(102,126,234,.6)} #translate-float-ball:active{transform:scale(.95)} #translate-float-ball:hover{transform:scale(1.05);box-shadow:0 6px 20px rgba(102,126,234,.5)} .translate-panel-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:${mobile ? '15px' : '20px'};display:flex;justify-content:space-between;align-items:center;cursor:move} .translate-panel-title{font-size:${mobile ? '16px' : '18px'};font-weight:600;user-select:none} .translate-panel-close{width:30px;height:30px;border-radius:50%;background:rgba(255,255,255,.2);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .3s;font-size:18px;color:#fff} .translate-panel-close:hover{background:rgba(255,255,255,.3)} .translate-panel-body{padding:${mobile ? '15px' : '20px'};max-height:${mobile ? '60vh' : '500px'};overflow-y:auto;-webkit-overflow-scrolling:touch;cursor:default} .translate-panel-body::-webkit-scrollbar{width:6px} .translate-panel-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2);border-radius:3px} .translate-control-group{margin-bottom:${mobile ? '15px' : '20px'}} .translate-control-label{display:block;margin-bottom:8px;font-size:14px;font-weight:500;color:#333} .translate-description{font-size:12px;color:#666;margin-top:4px;font-weight:normal} .translate-description a{color:#667eea} .translate-switch{position:relative;display:inline-block;width:50px;height:24px} .translate-switch input{opacity:0;width:0;height:0} .translate-switch-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;transition:.4s;border-radius:24px} .translate-switch-slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background:#fff;transition:.4s;border-radius:50%} .translate-switch input:checked + .translate-switch-slider{background:#667eea} .translate-switch input:checked + .translate-switch-slider:before{transform:translateX(26px)} .translate-select,.translate-input{width:100%;padding:${mobile ? '12px' : '10px'};border:1px solid #ddd;border-radius:8px;font-size:${mobile ? '16px' : '14px'};background:#fff;cursor:pointer;transition:border-color .3s;color:#333;box-sizing:border-box} .translate-select:focus,.translate-input:focus{outline:none;border-color:#667eea} .translate-textarea{width:100%;min-height:80px;padding:${mobile ? '12px' : '10px'};border:1px solid #ddd;border-radius:8px;font-size:${mobile ? '14px' : '13px'};background:#fff;transition:border-color .3s;color:#333;box-sizing:border-box;resize:vertical;font-family:monospace;line-height:1.5} .translate-textarea:focus{outline:none;border-color:#667eea} .translate-slider-container{display:flex;align-items:center;gap:10px} .translate-slider{flex:1;-webkit-appearance:none;height:6px;border-radius:3px;background:#ddd;outline:none} .translate-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:18px;height:18px;border-radius:50%;background:#667eea;cursor:pointer} .translate-slider::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#667eea;cursor:pointer;border:none} .translate-slider-value{min-width:45px;text-align:center;font-size:14px;color:#666} .translate-button{width:100%;padding:${mobile ? '14px' : '12px'};background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;border-radius:8px;font-size:${mobile ? '16px' : '14px'};font-weight:500;cursor:pointer;transition:transform .3s,box-shadow .3s;-webkit-tap-highlight-color:transparent} .translate-button:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,.4)} .translate-button:active{transform:scale(.98)} .translate-button.tt-pink{background:linear-gradient(135deg,#f093fb 0%,#f5576c 100%)} .translate-button.tt-orange{background:linear-gradient(135deg,#fa709a 0%,#fee140 100%)} .translate-button.tt-red{background:linear-gradient(135deg,#ff9a9e 0%,#fecfef 100%)} .translate-button.tt-yellow{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%)} .translate-button.tt-mt{margin-top:8px} .translate-button-group{display:flex;gap:10px;margin-bottom:${mobile ? '15px' : '20px'}} .translate-button-group.tt-mt{margin-top:20px} .translate-button-group .translate-button{flex:1} .translate-section-title{font-size:${mobile ? '15px' : '16px'};font-weight:600;color:#333;margin:25px 0 15px;padding-bottom:10px;border-bottom:2px solid #f0f0f0} .translate-section-title.tt-first{margin-top:0} .translate-info{background:#f8f9fa;padding:12px;border-radius:8px;font-size:${mobile ? '12px' : '13px'};color:#666;margin-top:10px;line-height:1.5} .tt-subbox{border:1px dashed #c9c9f5;border-radius:8px;padding:12px;margin-top:10px} .tt-hidden{display:none} ${dark ? ` #translate-panel{background:#1e1e1e !important;color:#e0e0e0 !important} .translate-panel-header{background:linear-gradient(135deg,#4a5eb7 0%,#5a3d7a 100%) !important} .translate-control-label{color:#e0e0e0 !important} .translate-select,.translate-input,.translate-textarea{background:#2d2d2d !important;color:#e0e0e0 !important;border-color:#444 !important} .translate-select option{background:#2d2d2d !important;color:#e0e0e0 !important} .translate-slider{background:#444 !important} .translate-slider-value{color:#b0b0b0 !important} .translate-section-title{color:#e0e0e0 !important;border-bottom-color:#444 !important} .translate-info{background:#2d2d2d !important;color:#b0b0b0 !important} .translate-description{color:#999 !important} .tt-subbox{border-color:#444 !important} ` : ''} `); } const earlyInit = () => { injectGlobalStyles(); const cfg = loadCfg(); const ball = document.createElement('div'); ball.id = 'translate-float-ball'; ball.className = cfg.showFloatBall ? 'tt-show' : 'tt-hide'; ball.innerHTML = ` `; ball.style.setProperty('width', `${cfg.floatBallSize}px`); ball.style.setProperty('height', `${cfg.floatBallSize}px`); ball.style.setProperty('opacity', String(cfg.floatBallOpacity)); ball.style.setProperty('left', `${cfg.floatBallPosition.x}px`); ball.style.setProperty('top', `${cfg.floatBallPosition.y}px`); if (document.body) document.body.appendChild(ball); else document.addEventListener('DOMContentLoaded', () => document.body.appendChild(ball)); }; earlyInit(); (() => { const PRIVATE_DEPLOY_DOC = 'https://translate.zvo.cn/41160.html'; const PRIVATE_HOST_DOC = 'https://translate.zvo.cn/4068.html'; class ConfigManager { constructor() { this.defaultConfig = buildDefaultConfig(); this.config = loadCfg(); } saveConfig() { GM_setValue('translateConfig', this.config); } get(k) { return this.config[k]; } set(k, v) { this.config[k] = v; this.saveConfig(); } reset() { try { GM_deleteValue('translateConfig'); } catch (e) {} this.config = { ...this.defaultConfig }; } clearCache() { if (typeof translate !== 'undefined') { try { translate.storage && translate.storage.clear && translate.storage.clear(); } catch (e) {} try { translate.language.clearCacheLanguage && translate.language.clearCacheLanguage(); } catch (e) {} } let cleared = 0; try { Object.keys(localStorage).forEach(key => { if (key.indexOf('translate') > -1 || key.indexOf('hash_') > -1) { localStorage.removeItem(key); cleared++; } }); } catch (e) {} return cleared; } getRealServiceName() { const s = this.get('translateService'); if (s === 'translate.service.public' || s === 'translate.service.private') return 'translate.service'; return s; } isPrivateService() { return this.get('translateService') === 'translate.service.private'; } sanitizeImportedConfig(candidate) { if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) { throw new Error('配置内容必须是 JSON 对象'); } const clean = { ...this.defaultConfig }; Object.keys(this.defaultConfig).forEach(key => { if (!Object.prototype.hasOwnProperty.call(candidate, key) || key === 'version') return; const value = candidate[key]; const expected = this.defaultConfig[key]; if (Array.isArray(expected)) { if (Array.isArray(value)) clean[key] = value.filter(item => typeof item === 'string'); } else if (key === 'floatBallPosition') { if (value && Number.isFinite(value.x) && Number.isFinite(value.y)) clean[key] = { x: value.x, y: value.y }; } else if (key === 'panelPosition') { if (value === null) clean[key] = null; else if (value && Number.isFinite(value.x) && Number.isFinite(value.y)) clean[key] = { x: value.x, y: value.y }; } else if (expected && typeof expected === 'object') { if (value && typeof value === 'object' && !Array.isArray(value)) clean[key] = { ...expected, ...value }; } else if (typeof value === typeof expected && (typeof value !== 'number' || Number.isFinite(value))) { clean[key] = value; } }); const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); clean.floatBallSize = clamp(clean.floatBallSize, 30, 80); clean.floatBallOpacity = clamp(clean.floatBallOpacity, 0.3, 1); clean.panelSize = clamp(clean.panelSize, 0.5, 1.2); clean.panelOpacity = clamp(clean.panelOpacity, 0.5, 1); clean.ajaxDelayExecuteTime = clamp(clean.ajaxDelayExecuteTime, 200, 60000); clean.ajaxMinIntervalTime = clamp(clean.ajaxMinIntervalTime, 200, 60000); clean.siteListMode = clean.siteListMode === 'whitelist' ? 'whitelist' : 'blacklist'; clean.keyboardShortcut = normalizeShortcut(clean.keyboardShortcut) || this.defaultConfig.keyboardShortcut; clean.version = this.defaultConfig.version; return clean; } importConfigData(payload) { let candidate = payload; if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'config')) { if (!payload.config || typeof payload.config !== 'object' || Array.isArray(payload.config)) { throw new Error('配置封装中的 config 必须是 JSON 对象'); } candidate = payload.config; } this.config = this.sanitizeImportedConfig(candidate); this.saveConfig(); return this.config; } getExportPayload() { return { schemaVersion: 1, scriptVersion: this.defaultConfig.version, exportedAt: new Date().toISOString(), config: { ...this.config, version: this.defaultConfig.version } }; } parseSiteRules(raw = this.get('siteListRules')) { const rules = []; String(raw || '').split(/\r?\n/).forEach(line => { const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith('#') || trimmedLine.startsWith('//')) return; trimmedLine.split(',').forEach(item => { let value = item.trim(); if (!value || value.startsWith('#') || value.startsWith('//')) return; let type = 'fuzzy'; const match = value.match(/^(exact|fuzzy|regex):([\s\S]*)$/i); if (match) { type = match[1].toLowerCase(); value = match[2].trim(); } if (value) rules.push({ type, value }); }); }); return rules; } getSitePolicy(url = window.location.href) { const mode = this.get('siteListMode') === 'whitelist' ? 'whitelist' : 'blacklist'; let absolute = String(url || ''); let comparable = absolute.replace(/^[a-z][a-z\d+.-]*:\/\//i, '').replace(/\/$/, ''); try { const parsed = new URL(absolute, window.location.href); absolute = parsed.href.replace(/\/$/, ''); comparable = (parsed.host + parsed.pathname + parsed.search + parsed.hash).replace(/\/$/, ''); } catch (e) {} const absoluteLower = absolute.toLowerCase(); const comparableLower = comparable.toLowerCase(); let matchedRule = null; this.parseSiteRules().some(rule => { let matched = false; if (rule.type === 'regex') { try { const expression = new RegExp(rule.value, 'i'); matched = expression.test(absolute) || expression.test(comparable); } catch (e) { console.warn('已忽略无效的网站正则规则:', rule.value, e); } } else { const expected = rule.value.replace(/^[a-z][a-z\d+.-]*:\/\//i, '').replace(/\/$/, '').toLowerCase(); matched = rule.type === 'exact' ? (absoluteLower === rule.value.toLowerCase().replace(/\/$/, '') || comparableLower === expected) : (absoluteLower.includes(rule.value.toLowerCase()) || comparableLower.includes(expected)); } if (matched) matchedRule = rule; return matched; }); return { mode, matched: !!matchedRule, matchedRule, allowed: mode === 'blacklist' ? !matchedRule : !!matchedRule }; } isSiteAllowed(url = window.location.href) { return this.getSitePolicy(url).allowed; } } class TranslateManager { constructor(configManager) { this.cm = configManager; this.initialized = false; this.listenerStarted = false; this.selectionStarted = false; this.selectionBootstrapped = false; this.selectionSuspended = false; this.selectionTouchHandler = null; this.selectionTouchTimer = null; this.lastSelectionText = ''; this.lastSelectionAt = 0; this.selectionRequestSequence = 0; this.pageTranslationActive = false; this.urlControlExecuted = false; this.requestListenerStarted = false; this.requestListenerBootstrapped = false; this.requestTransportBootstrapped = false; this.executeGuardBootstrapped = false; this.executeGuardBypass = false; this.currentLanguage = null; this.isTranslating = false; } resolveRequestUrl(url) { if (typeof url !== 'string' || !url) return ''; if (/^https?:\/\//i.test(url)) return url; try { return translate.request.getUrl(url); } catch (e) { return url; } } isCrossOriginRequest(url) { try { return new URL(url, window.location.href).origin !== window.location.origin; } catch (e) { return false; } } createPrivilegedXhr(response, requestUrl, requestData) { const rawHeaders = response && typeof response.responseHeaders === 'string' ? response.responseHeaders : ''; const responseText = response && typeof response.responseText === 'string' ? response.responseText : (response && typeof response.response === 'string' ? response.response : ''); return { readyState: 4, status: response && Number.isFinite(Number(response.status)) ? Number(response.status) : 0, statusText: response && response.statusText ? response.statusText : '', response: responseText, responseText, responseURL: response && response.finalUrl ? response.finalUrl : requestUrl, requestURL: requestUrl, responseHeaders: rawHeaders, data: requestData, getAllResponseHeaders() { return rawHeaders; }, getResponseHeader(name) { if (!name) return null; const wanted = String(name).toLowerCase(); const line = rawHeaders.split(/\r?\n/).find(item => item.slice(0, item.indexOf(':')).trim().toLowerCase() === wanted ); return line ? line.slice(line.indexOf(':') + 1).trim() : null; } }; } sendWithPrivilegedRequest(url, data, appendXhrData, func, method, headers, abnormalFunc, showErrorLog) { const requestUrl = this.resolveRequestUrl(url); const requestHeaders = {}; if (headers && typeof headers === 'object') { Object.keys(headers).forEach(key => { requestHeaders[key] = headers[key]; }); } if (translate.request.appendHeaders && typeof translate.request.appendHeaders === 'object') { Object.keys(translate.request.appendHeaders).forEach(key => { requestHeaders[key] = translate.request.appendHeaders[key]; }); } if (translate.service.name !== 'client.edge') { requestHeaders.currentpage = String(window.location.href); } let params = ''; if (typeof data === 'string') { params = data; } else { if (!data || typeof data !== 'object') data = {}; try { data.browserDefaultLanguage = translate.util.browserDefaultLanguage(); } catch (e) {} if (translate.request.appendParams && typeof translate.request.appendParams === 'object') { Object.keys(translate.request.appendParams).forEach(key => { data[key] = translate.request.appendParams[key]; }); } if (typeof translate.enterprise !== 'undefined' && typeof translate.enterprise.key === 'string' && translate.enterprise.key) { data.key = translate.enterprise.key; } params = Object.keys(data).map(key => key + '=' + data[key]).join('&'); } const finish = (response, transportError) => { const xhr = this.createPrivilegedXhr(response, requestUrl, appendXhrData); try { if (translate.request && typeof translate.request.response === 'function') { translate.request.response(xhr); } } catch (e) { console.warn('translate.js 响应拦截器执行失败:', e); } if (!transportError && xhr.status === 200) { let responseData = null; if (xhr.responseText && xhr.responseText.indexOf('{') > -1 && xhr.responseText.indexOf('}') > -1) { try { responseData = JSON.parse(xhr.responseText); } catch (e) { console.warn('翻译接口返回的 JSON 无法解析:', e); } } if (typeof func === 'function') { if (responseData === null) func(xhr.responseText); else func(responseData, xhr.data, xhr); } return; } if (showErrorLog) { console.error('油猴跨域翻译请求失败:', { status: xhr.status, statusText: xhr.statusText, requestUrl, response: xhr.responseText }); } if (typeof abnormalFunc === 'function') abnormalFunc(xhr); }; try { return GM_xmlhttpRequest({ method: String(method || 'post').toUpperCase(), url: requestUrl, headers: requestHeaders, data: params, responseType: 'text', onload: response => finish(response, false), onerror: response => finish(response, true), ontimeout: response => finish(response, true), onabort: response => finish(response, true) }); } catch (e) { console.error('无法启动油猴跨域翻译请求:', e); finish({ status: 0, statusText: e.message || String(e), responseText: '' }, true); return null; } } prepareRequestTransportBridge() { if (this.requestTransportBootstrapped || typeof translate === 'undefined' || !translate.request) return; if (typeof GM_xmlhttpRequest !== 'function' || typeof translate.request.send !== 'function') return; const originalSend = translate.request.send; const manager = this; translate.request.send = function(url, data, appendXhrData, func, method, isAsynchronize, headers, abnormalFunc, showErrorLog) { const requestUrl = manager.resolveRequestUrl(url); if (isAsynchronize === false || !manager.isCrossOriginRequest(requestUrl)) { return originalSend.apply(translate.request, arguments); } return manager.sendWithPrivilegedRequest( requestUrl, data, appendXhrData, func, method, headers, abnormalFunc, showErrorLog ); }; this.requestTransportBootstrapped = true; console.info('[智能翻译] 已启用油猴跨域请求兼容层,可绕过网站 CSP 限制'); } applyServiceSettings() { if (typeof translate === 'undefined') return; const real = this.cm.getRealServiceName(); try { translate.service.use(real); } catch (e) {} if (this.cm.isPrivateService()) { const customUrls = this.cm.get('customServiceUrls'); if (customUrls) { const urls = customUrls.split(',').map(u => u.trim()).filter(u => u); if (urls.length) { try { if (translate.request.setHost) translate.request.setHost(urls.length === 1 ? urls[0] : urls); else translate.request.api.host = urls.length === 1 ? urls[0] : urls; } catch (e) {} } } } } applyAllSettings() { if (typeof translate === 'undefined') return; // 整页翻译始终使用用户明确选择的源语言,避免引擎自动识别后提前结束请求。 translate.language.setLocal(this.cm.get('localLanguage') || 'english'); this.applyServiceSettings(); translate.selectLanguageTag.show = false; this.applyIgnoreSettings(); this.applyCustomTerms(); this.applyAttributeSettings(); this.applyImageRules(); this.applyAdvancedSettings(); this.applyRequestParams(); } applyIgnoreSettings() { if (typeof translate === 'undefined') return; try { (this.cm.get('ignoredClasses') || '').split(',').map(c => c.trim()).filter(c => c) .forEach(c => { try { translate.ignore.class.push(c); } catch (e) {} }); } catch (e) {} try { (this.cm.get('ignoredIds') || '').split(',').map(i => i.trim()).filter(i => i) .forEach(i => { try { translate.ignore.id.push(i); } catch (e) {} }); } catch (e) {} try { (this.cm.get('ignoredTags') || '').split(',').map(t => t.trim().toLowerCase()).filter(t => t) .forEach(t => { try { translate.ignore.tag.push(t); } catch (e) {} }); } catch (e) {} try { (this.cm.get('ignoredTexts') || '').split('\n').map(t => t.trim()).filter(t => t) .forEach(t => { try { translate.ignore.text.push(t); } catch (e) {} }); } catch (e) {} } applyCustomTerms() { if (typeof translate === 'undefined') return; const terms = this.cm.get('customTerms'); if (!terms) return; try { translate.nomenclature.append(this.cm.get('localLanguage'), this.cm.get('targetLanguage'), terms); } catch (e) {} } applyAttributeSettings() { if (typeof translate === 'undefined') return; try { const attrs = this.cm.get('translateAttributes'); if (attrs && attrs.length) translate.translateAttributes = attrs; } catch (e) {} try { (this.cm.get('customTagAttributes') || '').split('\n').map(l => l.trim()).filter(l => l).forEach(line => { const idx = line.indexOf('='); if (idx > -1) { const tag = line.slice(0, idx).trim().toLowerCase(); const list = line.slice(idx + 1).split(',').map(a => a.trim()).filter(a => a); if (tag && list.length) translate.element.tagAttribute[tag] = list; } }); } catch (e) {} } applyImageRules() { if (typeof translate === 'undefined') return; const map = {}; (this.cm.get('imageRules') || '').split('\n').map(l => l.trim()).filter(l => l).forEach(line => { const idx = line.indexOf('='); if (idx > -1) { const src = line.slice(0, idx).trim(), target = line.slice(idx + 1).trim(); if (src && target) map[src] = target; } }); if (Object.keys(map).length) { try { translate.images.add(map); } catch (e) {} } } applyAdvancedSettings() { if (typeof translate === 'undefined') return; try { if (this.cm.get('enableWholeAll')) translate.whole.enableAll(); } catch (e) {} try { translate.language.translateLocal = !!this.cm.get('translateLocal'); } catch (e) {} try { const range = (this.cm.get('translateLanguagesRange') || '').split(',').map(r => r.trim()).filter(r => r); if (range.length) translate.language.translateLanguagesRange = range; } catch (e) {} try { if (this.cm.get('enableProgressTip')) translate.progress.api.startUITip(); } catch (e) {} try { translate.waitingExecute.use = !!this.cm.get('waitingExecute'); } catch (e) {} try { if (this.cm.get('enableSSE') && translate.request.sse) translate.request.sse.start(); } catch (e) {} try { if (this.cm.get('enableIframe')) { const url = this.cm.get('iframeTranslateJsUrl'); if (url && translate.element.iframe && translate.element.iframe.use) translate.element.iframe.use(url); } } catch (e) {} try { if (translate.storage) { if (this.cm.get('enableCache')) translate.storage.enable && translate.storage.enable(); else translate.storage.disable && translate.storage.disable(); } } catch (e) {} } applyRequestParams() { if (typeof translate === 'undefined') return; const parse = (raw) => { const obj = {}; (raw || '').split('\n').map(l => l.trim()).filter(l => l).forEach(line => { const idx = line.indexOf('='); if (idx > -1) obj[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); }); return obj; }; try { const p = parse(this.cm.get('appendParams')); if (Object.keys(p).length) translate.request.appendParams = p; const h = parse(this.cm.get('appendHeaders')); if (Object.keys(h).length) translate.request.appendHeaders = h; } catch (e) {} } init() { if (this.initialized || typeof translate === 'undefined') return; try { this.prepareRequestTransportBridge(); this.prepareExecuteGuard(); this.applyAllSettings(); this.initialized = true; console.log('翻译管理器初始化完成 (translate.js ' + (translate.version || '') + ')'); } catch (e) { console.error('翻译初始化失败:', e); } } prepareExecuteGuard() { if (this.executeGuardBootstrapped || typeof translate === 'undefined' || typeof translate.execute !== 'function') return; const originalExecute = translate.execute; const manager = this; translate.execute = function(...args) { if (!manager.executeGuardBypass && (!manager.pageTranslationActive || !manager.cm.isSiteAllowed())) return false; return originalExecute.apply(translate, args); }; this.executeGuardBootstrapped = true; } prepareRequestListenerGuard() { if (this.requestListenerBootstrapped || typeof translate === 'undefined' || !translate.request.listener) return; const requestListener = translate.request.listener; const originalTrigger = typeof requestListener.trigger === 'function' ? requestListener.trigger : () => true; const manager = this; requestListener.trigger = function(url) { if (!manager.pageTranslationActive || !manager.cm.get('enableAjaxListener') || !manager.cm.isSiteAllowed()) return false; return originalTrigger.call(requestListener, url); }; this.requestListenerBootstrapped = true; } startListener() { if (typeof translate === 'undefined' || !this.cm.isSiteAllowed()) return false; try { if (this.cm.get('enableListener') && !this.listenerStarted) { translate.listener.start(); this.listenerStarted = true; } if (this.cm.get('enableAjaxListener') && translate.request.listener && !this.requestListenerStarted) { this.prepareRequestListenerGuard(); translate.request.listener.delayExecuteTime = Math.max(200, Number(this.cm.get('ajaxDelayExecuteTime')) || 200); translate.request.listener.minIntervalTime = Math.max(200, Number(this.cm.get('ajaxMinIntervalTime')) || 800); if (!translate.request.listener.use) translate.request.listener.start(); this.requestListenerStarted = true; } } catch (e) { if (!(e.message && e.message.indexOf('已经启动') > -1)) console.warn('启动监听失败:', e); return false; } return true; } getSitePolicy() { return this.cm.getSitePolicy(window.location.href); } canTranslateCurrentSite() { return this.getSitePolicy().allowed; } beginPageTranslation() { this.pageTranslationActive = true; this.selectionSuspended = true; this.selectionRequestSequence++; clearTimeout(this.selectionTouchTimer); this.hideSelectionTooltip(); } changeLanguage(targetLang) { if (!targetLang || !this.canTranslateCurrentSite()) return false; if (!this.initialized) this.init(); if (typeof translate === 'undefined') return false; try { if (this.currentLanguage === targetLang && this.isTranslating) return true; this.beginPageTranslation(); this.currentLanguage = targetLang; this.isTranslating = true; this.applyAllSettings(); this.startListener(); translate.changeLanguage(targetLang); setTimeout(() => { this.isTranslating = false; }, 1000); return true; } catch (e) { console.error('切换语言失败:', e); this.isTranslating = false; this.pageTranslationActive = false; this.resumeSelectionTranslate(); return false; } } restoreOriginal() { if (typeof translate === 'undefined') return false; let restored = false; this.executeGuardBypass = true; try { if (translate.reset) translate.reset(); else translate.changeLanguage(this.cm.get('localLanguage')); restored = true; } catch (e) { console.error('还原原文失败:', e); } finally { this.executeGuardBypass = false; } if (!restored) return false; this.isTranslating = false; this.currentLanguage = null; if (translate.request && translate.request.listener) { // 4.0.0 没有公开 listener.stop();清空队列,并由 execute/request 门禁阻止后续动态翻译。 translate.request.listener.executetime = 0; translate.request.listener.lasttime = 0; } this.pageTranslationActive = false; this.resumeSelectionTranslate(); return true; } executeUrlLanguageControl() { if (this.urlControlExecuted || !this.cm.get('enableUrlLanguageControl') || !this.canTranslateCurrentSite()) return false; const paramName = String(this.cm.get('urlLanguageParam') || 'language').trim() || 'language'; let target = ''; try { target = new URL(window.location.href).searchParams.get(paramName) || ''; } catch (e) { return false; } if (!target.trim()) return false; if (typeof translate === 'undefined' || !translate.language || typeof translate.language.setUrlParamControl !== 'function' || typeof translate.execute !== 'function') { console.warn('当前 translate.js 不支持 URL 语言控制'); return false; } try { this.applyAllSettings(); translate.language.setUrlParamControl(paramName); this.beginPageTranslation(); this.currentLanguage = target.trim(); this.isTranslating = true; this.startListener(); translate.execute(); this.urlControlExecuted = true; setTimeout(() => { this.isTranslating = false; }, 1000); return true; } catch (e) { console.error('URL 语言控制执行失败:', e); this.isTranslating = false; this.pageTranslationActive = false; this.resumeSelectionTranslate(); return false; } } togglePageTranslation() { if (this.pageTranslationActive) { return this.restoreOriginal(); } if (!this.canTranslateCurrentSite()) return false; if (!this.cm.get('enabled')) this.cm.set('enabled', true); return this.changeLanguage(this.cm.get('targetLanguage')); } toggle(enabled) { if (enabled) { if (!this.canTranslateCurrentSite()) return false; if (!this.initialized) this.init(); if (this.executeUrlLanguageControl()) return true; if (this.cm.get('autoTranslate')) { setTimeout(() => this.changeLanguage(this.cm.get('targetLanguage')), 100); } return true; } return this.restoreOriginal(); } execute() { return this.changeLanguage(this.cm.get('targetLanguage')); } /* ===== 实验性划词翻译:官方启动能力 + 独立请求状态 ===== */ canHandleSelection() { return !!this.cm.get('enableSelectionTranslate') && this.cm.isSiteAllowed() && !this.selectionSuspended && !this.pageTranslationActive; } getSelectedText() { try { const selection = window.getSelection(); return selection && !selection.isCollapsed ? selection.toString().trim() : ''; } catch (e) { return ''; } } getSelectionPoint(event) { const touch = event && event.changedTouches && event.changedTouches[0] ? event.changedTouches[0] : (event && event.touches && event.touches[0] ? event.touches[0] : null); let x = touch ? touch.pageX : (event && Number.isFinite(event.pageX) ? event.pageX : NaN); let y = touch ? touch.pageY : (event && Number.isFinite(event.pageY) ? event.pageY : NaN); if (!Number.isFinite(x) || !Number.isFinite(y)) { try { const selection = window.getSelection(); if (selection && selection.rangeCount) { const rect = selection.getRangeAt(0).getBoundingClientRect(); x = rect.left + window.pageXOffset; y = rect.bottom + window.pageYOffset; } } catch (e) {} } return { x: Number.isFinite(x) ? x : window.pageXOffset + 20, y: Number.isFinite(y) ? y : window.pageYOffset + 20 }; } showSelectionTooltip(text, point, isError = false) { let tooltip = document.getElementById('translateTooltip'); if (!tooltip) { tooltip = document.createElement('span'); tooltip.id = 'translateTooltip'; tooltip.style.cssText = 'background-color:black;color:#fff;text-align:center;border-radius:6px;padding:5px;position:absolute;z-index:2147483647;max-width:min(80vw,600px);white-space:pre-wrap;'; document.body.appendChild(tooltip); } tooltip.textContent = text; tooltip.style.backgroundColor = isError ? '#b42318' : 'black'; tooltip.style.top = (point.y + 20) + 'px'; tooltip.style.left = Math.min(point.x + 20, window.pageXOffset + window.innerWidth - 80) + 'px'; tooltip.style.display = ''; } resolveSelectionSource(text) { try { if (translate.language && typeof translate.language.recognition === 'function') { const recognition = translate.language.recognition(text); if (recognition && typeof recognition.languageName === 'string' && recognition.languageName.trim()) { return recognition.languageName.trim(); } } } catch (e) {} try { const local = translate.language.getLocal(); if (local) return local; } catch (e) {} return this.cm.get('localLanguage') || 'english'; } requestSelectionTranslate(event) { if (!this.canHandleSelection()) return false; const text = this.getSelectedText(); if (!text) return false; const now = Date.now(); if (text === this.lastSelectionText && now - this.lastSelectionAt < 700) return false; this.lastSelectionText = text; this.lastSelectionAt = now; const target = this.cm.get('selectionDefaultTo'); const api = translate.request && translate.request.api && translate.request.api.translate; if (!target || typeof api !== 'string' || !api.trim() || !translate.request || typeof translate.request.post !== 'function') { console.warn('划词翻译请求参数不完整,已阻止无效请求:', { target, api }); this.showSelectionTooltip('⚠️ 划词翻译暂时不可用', this.getSelectionPoint(event), true); return false; } this.applySelectionTarget(); const point = this.getSelectionPoint(event); const from = this.resolveSelectionSource(text); const requestId = ++this.selectionRequestSequence; const data = { from, to: target, text: encodeURIComponent(JSON.stringify([text])) }; try { translate.request.post(api, data, (responseData) => { if (requestId !== this.selectionRequestSequence || !this.canHandleSelection()) return; if (!responseData || Number(responseData.result) !== 1 || !Array.isArray(responseData.text) || typeof responseData.text[0] !== 'string') { console.warn('划词翻译接口返回异常:', responseData); this.showSelectionTooltip('⚠️ 划词翻译失败,请稍后重试', point, true); return; } this.showSelectionTooltip(responseData.text[0], point); }, null); return true; } catch (e) { console.error('实验性划词翻译执行失败:', e); this.showSelectionTooltip('⚠️ 划词翻译失败,请稍后重试', point, true); return false; } } prepareSelectionBridge() { if (this.selectionBootstrapped || typeof translate === 'undefined') return true; const selection = translate.selectionTranslate; if (!selection || typeof selection.start !== 'function' || typeof selection.callTranslate !== 'function') { console.warn('当前 translate.js 不支持划词翻译'); return false; } const manager = this; selection.callTranslate = function(event) { return manager.requestSelectionTranslate(event); }; // translate.js 4.0.0 只注册 mouseup;移动端长按选词后补充 touchend 触发。 this.selectionTouchHandler = function(event) { if (!manager.canHandleSelection()) return; const point = manager.getSelectionPoint(event); clearTimeout(manager.selectionTouchTimer); manager.selectionTouchTimer = setTimeout(() => { if (!manager.canHandleSelection()) return; selection.callTranslate({ pageX: point.x, pageY: point.y }); }, 120); }; document.addEventListener('touchend', this.selectionTouchHandler, { passive: true }); this.selectionBootstrapped = true; return true; } applySelectionTarget() { if (typeof translate === 'undefined' || !this.cm.get('enableSelectionTranslate') || this.pageTranslationActive) return false; const target = this.cm.get('selectionDefaultTo'); if (!target || !translate.language || typeof translate.language.setDefaultTo !== 'function') return false; try { // setDefaultTo 只负责官方默认值;实际请求始终显式使用 selectionDefaultTo,避免共享 translate.to。 translate.language.setDefaultTo(target); return true; } catch (e) { console.error('设置划词翻译目标语言失败:', e); return false; } } startSelectionTranslate() { if (typeof translate === 'undefined' || !this.cm.get('enableSelectionTranslate') || !this.cm.isSiteAllowed()) return false; if (!this.initialized) this.init(); if (this.pageTranslationActive) { this.selectionSuspended = true; return true; } if (!this.prepareSelectionBridge()) return false; this.selectionSuspended = false; this.applyServiceSettings(); if (!this.applySelectionTarget()) return false; try { if (!this.selectionStarted) { translate.selectionTranslate.start(); this.selectionStarted = true; } console.info('实验性划词翻译已启用,目标语言: ' + this.cm.get('selectionDefaultTo')); return true; } catch (e) { console.error('启动实验性划词翻译失败:', e); this.selectionSuspended = true; return false; } } stopSelectionTranslate() { // translate.js 4.0.0 没有官方 stop();包装后的事件门禁会立即停止请求及过期响应。 this.selectionSuspended = true; this.selectionRequestSequence++; clearTimeout(this.selectionTouchTimer); this.hideSelectionTooltip(); } resumeSelectionTranslate() { if (!this.cm.get('enableSelectionTranslate') || this.pageTranslationActive) return false; this.selectionSuspended = false; if (!this.selectionStarted) return this.startSelectionTranslate(); return this.applySelectionTarget(); } updateSelectionTarget() { if (!this.canHandleSelection()) return false; this.selectionRequestSequence++; return this.applySelectionTarget(); } hideSelectionTooltip() { const tooltip = document.getElementById('translateTooltip'); if (tooltip) tooltip.style.display = 'none'; } } class ToastManager { constructor() { this.container = document.createElement('div'); this.container.id = 'translate-toast-container'; if (document.body) document.body.appendChild(this.container); else document.addEventListener('DOMContentLoaded', () => document.body.appendChild(this.container)); } show(message, type = 'success', duration = 2000) { const toast = document.createElement('div'); toast.className = 'tt-toast tt-' + type; toast.textContent = message; this.container.appendChild(toast); setTimeout(() => { toast.classList.add('tt-out'); setTimeout(() => { try { this.container.removeChild(toast); } catch (e) {} }, 300); }, duration); } } function getSupportedLanguages() { return [ { value: 'chinese_simplified', name: '简体中文' }, { value: 'chinese_traditional', name: '繁體中文' }, { value: 'english', name: 'English' }, { value: 'mepeti', name: 'Sepeti' }, { value: 'gongen', name: 'गोंगेन हें नांव' }, { value: 'japanese', name: '日本語' }, { value: 'korean', name: '한국어' }, { value: 'french', name: 'Français' }, { value: 'spanish', name: 'Español' }, { value: 'portuguese', name: 'Português' }, { value: 'german', name: 'Deutsch(兼容标识)' }, { value: 'deutsch', name: 'Deutsch' }, { value: 'italian', name: 'Italiano' }, { value: 'russian', name: 'Русский' }, { value: 'arabic', name: 'العربية' }, { value: 'hindi', name: 'हिन्दी' }, { value: 'bengali', name: 'বাংলা' }, { value: 'urdu', name: 'اردو' }, { value: 'persian', name: 'فارسی' }, { value: 'turkish', name: 'Türkçe' }, { value: 'vietnamese', name: 'Tiếng Việt' }, { value: 'thai', name: 'ไทย' }, { value: 'indonesian', name: 'Bahasa Indonesia' }, { value: 'malay', name: 'Bahasa Melayu' }, { value: 'filipino', name: 'Filipino' }, { value: 'dutch', name: 'Nederlands' }, { value: 'polish', name: 'Polski' }, { value: 'ukrainian', name: 'Українська' }, { value: 'romanian', name: 'Română' }, { value: 'greek', name: 'Ελληνικά' }, { value: 'czech', name: 'Čeština' }, { value: 'hungarian', name: 'Magyar' }, { value: 'swedish', name: 'Svenska' }, { value: 'danish', name: 'Dansk' }, { value: 'finnish', name: 'Suomi' }, { value: 'norwegian', name: 'Norsk' }, { value: 'slovak', name: 'Slovenčina' }, { value: 'slovene', name: 'Slovenščina' }, { value: 'croatian', name: 'Hrvatski' }, { value: 'serbian', name: 'Српски' }, { value: 'bosnian', name: 'Bosanski' }, { value: 'bulgarian', name: 'Български' }, { value: 'macedonian', name: 'Македонски' }, { value: 'albanian', name: 'Shqip' }, { value: 'lithuanian', name: 'Lietuvių' }, { value: 'latvian', name: 'Latviešu' }, { value: 'estonian', name: 'Eesti' }, { value: 'belarusian', name: 'Беларуская' }, { value: 'hebrew', name: 'עברית' }, { value: 'yiddish', name: 'ייִדיש' }, { value: 'armenian', name: 'Հայերեն' }, { value: 'georgian', name: 'ქართული' }, { value: 'azerbaijani', name: 'Azərbaycanca' }, { value: 'kazakh', name: 'Қазақ' }, { value: 'uzbek', name: 'Oʻzbek' }, { value: 'kyrgyz', name: 'Кыргыз' }, { value: 'tajik', name: 'Тоҷикӣ' }, { value: 'turkmen', name: 'Türkmençe' }, { value: 'mongolian', name: 'Монгол' }, { value: 'tatar', name: 'Татар' }, { value: 'uyghur', name: 'ئۇيغۇر' }, { value: 'tamil', name: 'தமிழ்' }, { value: 'telugu', name: 'తెలుగు' }, { value: 'kannada', name: 'ಕನ್ನಡ' }, { value: 'malayalam', name: 'മലയാളം' }, { value: 'marathi', name: 'मराठी' }, { value: 'gujarati', name: 'ગુજરાતી' }, { value: 'punjabi', name: 'ਪੰਜਾਬੀ' }, { value: 'oriya', name: 'ଓଡ଼ିଆ' }, { value: 'assamese', name: 'অসমীয়া' }, { value: 'nepali', name: 'नेपाली' }, { value: 'sindhi', name: 'سنڌي' }, { value: 'sinhala', name: 'සිංහල(兼容标识)' }, { value: 'singapore', name: 'සිංගාපුර්' }, { value: 'pashto', name: 'پښتو' }, { value: 'maithili', name: 'मैथिली' }, { value: 'bhojpuri', name: 'भोजपुरी' }, { value: 'sanskrit', name: 'संस्कृतम्' }, { value: 'dhivehi', name: 'ދިވެހި' }, { value: 'burmese', name: 'မြန်မာ' }, { value: 'khmer', name: 'ភាសាខ្មែរ' }, { value: 'lao', name: 'ລາວ' }, { value: 'meitei', name: 'ꯃꯩꯇꯩꯂꯣꯟ' }, { value: 'mizo', name: 'Mizo ṭawng' }, { value: 'dogrid', name: 'डोगरी' }, { value: 'tigri', name: 'ትግርኛ' }, { value: 'amharic', name: 'አማርኛ' }, { value: 'somali', name: 'Soomaali' }, { value: 'swahili', name: 'Kiswahili' }, { value: 'hausa', name: 'Hausa' }, { value: 'yoruba', name: 'Yorùbá' }, { value: 'igbo', name: 'Igbo' }, { value: 'zulu', name: 'isiZulu(兼容标识)' }, { value: 'south_african_zulu', name: 'IsiZulu saseNingizimu Afrika' }, { value: 'xhosa', name: 'isiXhosa(兼容标识)' }, { value: 'afrikaans_xhosa', name: 'Afrikaans isiXhosa' }, { value: 'afrikaans', name: 'Afrikaans' }, { value: 'sesotho', name: 'Sesotho' }, { value: 'shona', name: 'Shona' }, { value: 'kinyarwanda', name: 'Kinyarwanda' }, { value: 'luganda', name: 'Luganda' }, { value: 'lingala', name: 'Lingala' }, { value: 'bambara', name: 'Bamanankan' }, { value: 'ewe', name: 'Eʋegbe' }, { value: 'twi', name: 'Twi' }, { value: 'oromo', name: 'Oromoo' }, { value: 'nyanja', name: 'Chinyanja' }, { value: 'malagasy', name: 'Malagasy' }, { value: 'sundanese', name: 'Basa Sunda' }, { value: 'javanese', name: 'Basa Jawa' }, { value: 'cebuano', name: 'Cebuano' }, { value: 'ilocano', name: 'Ilokano' }, { value: 'hmong', name: 'Hmoob' }, { value: 'maori', name: 'Māori' }, { value: 'samoan', name: 'Gagana Sāmoa' }, { value: 'hawaiian', name: 'ʻŌlelo Hawaiʻi' }, { value: 'haitian_creole', name: 'Kreyòl Ayisyen' }, { value: 'creole', name: 'Krio / Creole' }, { value: 'guarani', name: 'Avañeʼẽ' }, { value: 'quechua', name: 'Runa Simi' }, { value: 'aymara', name: 'Aymar aru' }, { value: 'latin', name: 'Latina' }, { value: 'esperanto', name: 'Esperanto' }, { value: 'welsh', name: 'Cymraeg' }, { value: 'irish', name: 'Gaeilge' }, { value: 'scottish_gaelic', name: 'Gàidhlig' }, { value: 'icelandic', name: 'Íslenska' }, { value: 'luxembourgish', name: 'Lëtzebuergesch' }, { value: 'frisian', name: 'Frysk' }, { value: 'catalan', name: 'Català' }, { value: 'galician', name: 'Galego' }, { value: 'basque', name: 'Euskara' }, { value: 'corsican', name: 'Corsu' }, { value: 'zongjia', name: 'Tsonga / zongjia' }, { value: 'maltese', name: 'Malti' }, { value: 'kurdish', name: 'Kurdî' }, { value: 'kurdish_sorani', name: 'کوردیی سۆرانی' } ]; } class UIManager { constructor(configManager, translateManager) { this.cm = configManager; this.tm = translateManager; this.floatBall = document.getElementById('translate-float-ball'); this.panel = null; this.toast = new ToastManager(); this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; this.touchStartPos = { x: 0, y: 0 }; this.clickCount = 0; this.clickTimer = null; this.init(); } init() { this.setupFloatBall(); this.createPanel(); this.bindEvents(); if (this.cm.get('enabled')) setTimeout(() => this.tm.toggle(true), 1000); if (this.cm.get('enableSelectionTranslate')) { setTimeout(() => this.tm.startSelectionTranslate(), 1200); } } setupFloatBall() { if (!this.floatBall) return; const size = this.cm.get('floatBallSize'); this.floatBall.style.setProperty('width', `${size}px`); this.floatBall.style.setProperty('height', `${size}px`); this.floatBall.style.setProperty('opacity', String(this.cm.get('floatBallOpacity'))); this.ensureInViewport(); } buildLangOptions(selected) { return getSupportedLanguages().map(l => `` ).join(''); } createPanel() { const c = this.cm.config; const mobile = isMobile(); const h = escapeHtml; const shortcutControls = mobile ? '' : `
`; const services = [ { value: 'client.edge', name: 'Microsoft Edge(机器翻译·无上限·73种语言)' }, { value: 'giteeAI', name: 'Gitee AI(大模型翻译·推荐·开源中国算力)' }, { value: 'siliconflow', name: 'SiliconFlow AI(大模型翻译·Qwen3)' }, { value: 'translate.service.public', name: '公共 translate.service 服务(免费·112+语言)' }, { value: 'translate.service.private', name: '私有部署 translate.service(需填接口地址)' } ]; const panel = document.createElement('div'); panel.id = 'translate-panel'; panel.innerHTML = `
🌐 智能翻译助手 v${h(c.version)}
基础设置
实验性${mobile ? '选词' : '鼠标划词'}翻译
翻译网站黑白名单
界面设置
${shortcutControls}
${c.floatBallSize}px
${Math.round(c.floatBallOpacity * 100)}%
${Math.round(c.panelSize * 100)}%
${Math.round(c.panelOpacity * 100)}%
翻译服务
忽略规则
图片 / iframe / 属性
自定义术语
高级 / 性能
配置导入 / 导出
导出当前全部已保存设置为 JSON;导入时只接受本脚本已知配置项,并保留当前脚本版本号。
💡 ${mobile ? '长按' : '拖动'}悬浮球或面板标题栏可调整位置,设置会自动保存
🚀 基于 translate.js 4.0.0 引擎,内置 130+ 个语言标识及图片/iframe能力
🧪 划词翻译为实验功能;整页翻译时暂停,恢复原文后恢复
⚡ 多层缓存加速,二次访问极速响应
`; document.body.appendChild(panel); this.panel = panel; this.updatePanelSize(); this.panel.style.setProperty('opacity', String(c.panelOpacity)); if (c.panelPosition) { this.panel.style.setProperty('left', `${c.panelPosition.x}px`); this.panel.style.setProperty('top', `${c.panelPosition.y}px`); } } updatePanelSize() { const mobile = isMobile(); const size = this.cm.get('panelSize'); const baseWidth = mobile ? window.innerWidth * 0.9 : 440; const baseHeight = mobile ? window.innerHeight * 0.8 : 650; const width = Math.min(baseWidth * size, mobile ? window.innerWidth * 0.95 : 680); const maxHeight = baseHeight * size; this.panel.style.setProperty('width', `${width}px`); this.panel.style.setProperty('max-height', `${maxHeight}px`); const body = this.panel.querySelector('.translate-panel-body'); if (body) body.style.setProperty('max-height', `${maxHeight - 60}px`); } bindEvents() { if (isMobile()) this.bindMobileBall(); else this.bindDesktopBall(); this.bindPanelDrag(); this.bindControls(); this.bindKeyboardShortcut(); window.addEventListener('resize', () => { this.ensureInViewport(); this.ensurePanelInViewport(); }); window.addEventListener('orientationchange', () => setTimeout(() => { this.ensureInViewport(); this.ensurePanelInViewport(); this.updatePanelSize(); }, 300)); } bindPanelDrag() { const header = this.panel.querySelector('.translate-panel-header'); const mobile = isMobile(); let dragging = false, sx, sy, ix, iy, lpTimer = null; const start = (cx, cy) => { sx = cx; sy = cy; ix = this.panel.offsetLeft; iy = this.panel.offsetTop; }; const move = (cx, cy) => { let nx = ix + (cx - sx), ny = iy + (cy - sy); nx = Math.max(0, Math.min(nx, window.innerWidth - this.panel.offsetWidth)); ny = Math.max(0, Math.min(ny, window.innerHeight - this.panel.offsetHeight)); this.panel.style.setProperty('left', `${nx}px`); this.panel.style.setProperty('top', `${ny}px`); }; const end = () => { if (dragging) { dragging = false; this.panel.classList.remove('dragging'); this.cm.set('panelPosition', { x: parseInt(this.panel.style.left), y: parseInt(this.panel.style.top) }); } }; if (mobile) { header.addEventListener('touchstart', (e) => { if (e.target.id === 'tp-close') return; const t = e.touches[0]; start(t.clientX, t.clientY); lpTimer = setTimeout(() => { dragging = true; this.panel.classList.add('dragging'); navigator.vibrate && navigator.vibrate(50); }, 300); e.preventDefault(); }); header.addEventListener('touchmove', (e) => { const t = e.touches[0]; if (!dragging) { if (lpTimer && Math.hypot(t.clientX - sx, t.clientY - sy) > 10) { clearTimeout(lpTimer); lpTimer = null; } return; } move(t.clientX, t.clientY); e.preventDefault(); }); header.addEventListener('touchend', () => { if (lpTimer) { clearTimeout(lpTimer); lpTimer = null; } end(); }); } else { header.addEventListener('mousedown', (e) => { if (e.target.id === 'tp-close') return; dragging = true; start(e.clientX, e.clientY); this.panel.classList.add('dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (dragging) move(e.clientX, e.clientY); }); document.addEventListener('mouseup', end); } } bindMobileBall() { let lpTimer = null, dragging = false, moved = false; this.floatBall.addEventListener('touchstart', (e) => { e.preventDefault(); const t = e.touches[0]; this.touchStartPos = { x: t.clientX, y: t.clientY }; moved = false; lpTimer = setTimeout(() => { dragging = true; this.floatBall.classList.add('dragging'); navigator.vibrate && navigator.vibrate(50); }, 300); this.dragOffset.x = t.clientX - this.floatBall.offsetLeft; this.dragOffset.y = t.clientY - this.floatBall.offsetTop; }); this.floatBall.addEventListener('touchmove', (e) => { e.preventDefault(); const t = e.touches[0]; if (Math.hypot(t.clientX - this.touchStartPos.x, t.clientY - this.touchStartPos.y) > 10) { moved = true; if (lpTimer) { clearTimeout(lpTimer); lpTimer = null; } } if (dragging) this.moveBall(t.clientX - this.dragOffset.x, t.clientY - this.dragOffset.y); }); this.floatBall.addEventListener('touchend', (e) => { e.preventDefault(); if (lpTimer) { clearTimeout(lpTimer); lpTimer = null; } this.floatBall.classList.remove('dragging'); if (dragging) this.cm.set('floatBallPosition', { x: parseInt(this.floatBall.style.left), y: parseInt(this.floatBall.style.top) }); else if (!moved) this.handleBallClick(); dragging = false; moved = false; }); } bindDesktopBall() { this.floatBall.addEventListener('click', () => { if (!this.isDragging) this.handleBallClick(); }); this.floatBall.addEventListener('mousedown', (e) => { this.isDragging = false; this.dragOffset.x = e.clientX - this.floatBall.offsetLeft; this.dragOffset.y = e.clientY - this.floatBall.offsetTop; this.floatBall.classList.add('dragging'); const mm = (ev) => { this.isDragging = true; this.moveBall(ev.clientX - this.dragOffset.x, ev.clientY - this.dragOffset.y); }; const mu = () => { document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu); this.floatBall.classList.remove('dragging'); if (this.isDragging) this.cm.set('floatBallPosition', { x: parseInt(this.floatBall.style.left), y: parseInt(this.floatBall.style.top) }); setTimeout(() => { this.isDragging = false; }, 100); }; document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu); }); } moveBall(x, y) { if (!this.cm.get('allowHalfBall')) { x = Math.max(0, Math.min(x, window.innerWidth - this.floatBall.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - this.floatBall.offsetHeight)); } else { const h = this.floatBall.offsetWidth / 2; x = Math.max(-h, Math.min(x, window.innerWidth - h)); y = Math.max(-h, Math.min(y, window.innerHeight - h)); } this.floatBall.style.setProperty('left', `${x}px`); this.floatBall.style.setProperty('top', `${y}px`); } notifySiteBlocked() { const policy = this.tm.getSitePolicy(); const message = policy.mode === 'blacklist' ? `🚫 当前网页命中黑名单${policy.matchedRule ? ':' + policy.matchedRule.value : ''}` : '🚫 当前网页不在白名单中'; this.toast.show(message, 'error', 3500); } translateNow(closePanel = false) { if (!this.tm.canTranslateCurrentSite()) { this.notifySiteBlocked(); return false; } const started = this.tm.changeLanguage(this.cm.get('targetLanguage')); if (started) { this.updateUI(); this.toast.show('🚀 正在翻译...'); if (closePanel) this.togglePanel(); } else { this.toast.show('⚠️ 翻译未能启动,请查看控制台', 'error', 3000); } return started; } togglePageTranslation() { if (!this.tm.canTranslateCurrentSite() && !this.tm.pageTranslationActive) { this.notifySiteBlocked(); return false; } const wasActive = this.tm.pageTranslationActive; const changed = this.tm.togglePageTranslation(); this.updateUI(); if (changed) { this.toast.show(wasActive ? '↩️ 已恢复原文' : '🚀 正在翻译...'); } else { this.toast.show('⚠️ 翻译未能启动,请查看控制台', 'error', 3000); } return changed; } bindKeyboardShortcut() { if (isMobile()) return; document.addEventListener('keydown', (event) => { if (event.repeat || !this.cm.get('enableKeyboardShortcut')) return; const target = event.target; if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/i.test(target.tagName || ''))) return; const configured = normalizeShortcut(this.cm.get('keyboardShortcut')); if (!configured || shortcutFromEvent(event) !== configured) return; event.preventDefault(); event.stopPropagation(); this.togglePageTranslation(); }, true); } exportConfiguration() { const payload = this.cm.getExportPayload(); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); const date = new Date().toISOString().slice(0, 10); link.href = url; link.download = `translate-helper-${this.cm.get('version')}-${date}.json`; document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } readConfigFile(file) { if (file && typeof file.text === 'function') return file.text(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(reader.error || new Error('读取文件失败')); reader.readAsText(file, 'utf-8'); }); } async importConfiguration(file) { if (!file) return; const isJsonName = /\.json$/i.test(file.name || ''); const isJsonType = /(?:application|text)\/json/i.test(file.type || ''); if (!isJsonName && !isJsonType) throw new Error('请选择 JSON 配置文件'); if (file.size > 2 * 1024 * 1024) throw new Error('配置文件不能超过 2MB'); const text = await this.readConfigFile(file); const payload = JSON.parse(text); this.cm.importConfigData(payload); } handleBallClick() { if (this.cm.get('doubleClickToggle')) { this.clickCount++; if (this.clickCount === 1) { this.clickTimer = setTimeout(() => { this.togglePanel(); this.clickCount = 0; }, 300); } else if (this.clickCount === 2) { clearTimeout(this.clickTimer); this.clickCount = 0; this.togglePageTranslation(); } } else { this.togglePanel(); } } bindControls() { const $ = (id) => document.getElementById(id); $('tp-close').addEventListener('click', () => this.togglePanel()); $('tp-now').addEventListener('click', () => this.translateNow(true)); $('tp-enable').addEventListener('change', (e) => { this.cm.set('enabled', e.target.checked); const applied = this.tm.toggle(e.target.checked); if (!applied) { if (e.target.checked) this.notifySiteBlocked(); else this.toast.show('⚠️ 恢复原文失败,翻译仍保持活动状态', 'error', 3500); } else { this.toast.show(e.target.checked ? '✅ 翻译已启用' : '❌ 翻译已禁用'); } }); $('tp-auto').addEventListener('change', (e) => { this.cm.set('autoTranslate', e.target.checked); this.toast.show(e.target.checked ? '✅ 自动翻译已启用' : '❌ 自动翻译已禁用'); }); $('tp-local').addEventListener('change', (e) => { this.cm.set('localLanguage', e.target.value); this.toast.show('✅ 整页翻译源语言已更新'); }); $('tp-target').addEventListener('change', (e) => { this.cm.set('targetLanguage', e.target.value); this.toast.show('✅ 整页翻译目标语言已更新'); }); // 实验性划词翻译:保留单一 setDefaultTo 目标语言选择 const sel = $('tp-selection'); if (sel) { sel.addEventListener('change', (e) => { this.cm.set('enableSelectionTranslate', e.target.checked); if (e.target.checked) { const started = this.tm.startSelectionTranslate(); const paused = this.tm.pageTranslationActive; this.toast.show(started ? (paused ? '🧪 划词翻译已启用,整页翻译期间暂时暂停' : '🧪 实验性划词翻译已启用;部分网页或移动端可能无效') : '⚠️ 当前页面无法启动划词翻译', started ? 'info' : 'error', 3200); } else { this.tm.stopSelectionTranslate(); this.toast.show('❌ 实验性划词翻译已禁用', 'info', 2500); } }); } const selTarget = $('tp-sel-target'); if (selTarget) { selTarget.addEventListener('change', (e) => { this.cm.set('selectionDefaultTo', e.target.value); this.tm.updateSelectionTarget(); this.toast.show('✅ 划词翻译目标语言已更新'); }); } $('tp-save-sites').addEventListener('click', () => { this.cm.set('siteListMode', $('tp-site-mode').value === 'whitelist' ? 'whitelist' : 'blacklist'); this.cm.set('siteListRules', $('tp-site-rules').value); const policy = this.tm.getSitePolicy(); if (!policy.allowed) { if (this.tm.pageTranslationActive) this.tm.restoreOriginal(); this.tm.stopSelectionTranslate(); this.notifySiteBlocked(); } else { if (this.cm.get('enableSelectionTranslate')) this.tm.startSelectionTranslate(); this.toast.show('✅ 网站规则已保存,当前网页允许翻译'); } }); const shortcutEnable = $('tp-shortcut-enable'); const shortcutInput = $('tp-shortcut'); if (shortcutEnable) { shortcutEnable.addEventListener('change', (e) => { this.cm.set('enableKeyboardShortcut', e.target.checked); this.toast.show(e.target.checked ? '✅ 翻译快捷键已启用' : '❌ 翻译快捷键已禁用'); }); } if (shortcutInput) { shortcutInput.addEventListener('focus', () => { shortcutInput.dataset.previousValue = this.cm.get('keyboardShortcut'); shortcutInput.value = '请按下组合键…'; }); shortcutInput.addEventListener('keydown', (event) => { event.preventDefault(); event.stopPropagation(); if (event.key === 'Escape') { shortcutInput.value = shortcutInput.dataset.previousValue || this.cm.get('keyboardShortcut'); shortcutInput.blur(); return; } const shortcut = shortcutFromEvent(event); if (!shortcut) { if (!['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) { this.toast.show('⚠️ 快捷键必须包含 Ctrl、Alt、Shift 或 Meta', 'error', 2800); } return; } this.cm.set('keyboardShortcut', shortcut); shortcutInput.value = shortcut; shortcutInput.blur(); this.toast.show('✅ 翻译快捷键已设置为 ' + shortcut); }); shortcutInput.addEventListener('blur', () => { if (shortcutInput.value === '请按下组合键…') shortcutInput.value = this.cm.get('keyboardShortcut'); }); } $('tp-showball').addEventListener('change', (e) => { this.cm.set('showFloatBall', e.target.checked); this.floatBall.classList.toggle('tt-show', e.target.checked); this.floatBall.classList.toggle('tt-hide', !e.target.checked); this.toast.show(e.target.checked ? '✅ 悬浮球已显示' : '❌ 悬浮球已隐藏'); }); $('tp-half').addEventListener('change', (e) => { this.cm.set('allowHalfBall', e.target.checked); this.ensureInViewport(); }); $('tp-dblclick').addEventListener('change', (e) => { this.cm.set('doubleClickToggle', e.target.checked); }); $('tp-ballsize').addEventListener('input', (e) => { const s = parseInt(e.target.value); this.cm.set('floatBallSize', s); this.floatBall.style.setProperty('width', `${s}px`); this.floatBall.style.setProperty('height', `${s}px`); e.target.nextElementSibling.textContent = `${s}px`; this.ensureInViewport(); }); $('tp-ballopacity').addEventListener('input', (e) => { const o = parseInt(e.target.value) / 100; this.cm.set('floatBallOpacity', o); this.floatBall.style.setProperty('opacity', String(o)); e.target.nextElementSibling.textContent = `${e.target.value}%`; }); $('tp-panelsize').addEventListener('input', (e) => { this.cm.set('panelSize', parseInt(e.target.value) / 100); this.updatePanelSize(); e.target.nextElementSibling.textContent = `${e.target.value}%`; this.ensurePanelInViewport(); }); $('tp-panelopacity').addEventListener('input', (e) => { const o = parseInt(e.target.value) / 100; this.cm.set('panelOpacity', o); this.panel.style.setProperty('opacity', String(o)); e.target.nextElementSibling.textContent = `${e.target.value}%`; }); $('tp-resetball').addEventListener('click', () => { const p = { x: 20, y: 100 }; this.floatBall.style.setProperty('left', `${p.x}px`); this.floatBall.style.setProperty('top', `${p.y}px`); this.cm.set('floatBallPosition', p); this.toast.show('✅ 悬浮球位置已重置'); }); $('tp-resetpanel').addEventListener('click', () => { this.cm.set('panelPosition', null); this.positionPanel(); this.toast.show('✅ 面板位置已重置'); }); $('tp-service').addEventListener('change', (e) => { this.cm.set('translateService', e.target.value); if (typeof translate !== 'undefined') { try { translate.service.use(this.cm.getRealServiceName()); } catch (err) {} } $('tp-customsvc-group').classList.toggle('tt-hidden', e.target.value !== 'translate.service.private'); this.toast.show('✅ 翻译服务已更新,建议立即翻译以生效'); }); $('tp-save-customsvc').addEventListener('click', () => { const v = $('tp-customsvc').value.trim(); this.cm.set('customServiceUrls', v); if (v && typeof translate !== 'undefined' && this.cm.isPrivateService()) { const urls = v.split(',').map(u => u.trim()).filter(u => u); try { if (translate.request.setHost) translate.request.setHost(urls.length === 1 ? urls[0] : urls); else translate.request.api.host = urls.length === 1 ? urls[0] : urls; } catch (err) {} } this.toast.show('✅ 自定义接口地址已保存'); }); $('tp-sse').addEventListener('change', (e) => { this.cm.set('enableSSE', e.target.checked); this.toast.show(e.target.checked ? '✅ SSE 已启用,下次翻译生效' : '❌ SSE 已禁用,刷新后生效', 'info'); }); $('tp-save-ignore').addEventListener('click', () => { this.cm.set('ignoredClasses', $('tp-igclass').value.trim()); this.cm.set('ignoredIds', $('tp-igid').value.trim()); this.cm.set('ignoredTags', $('tp-igtag').value.trim()); this.cm.set('ignoredTexts', $('tp-igtext').value); this.tm.applyIgnoreSettings(); this.toast.show('✅ 忽略规则已保存,立即翻译以生效'); }); $('tp-iframe').addEventListener('change', (e) => this.cm.set('enableIframe', e.target.checked)); $('tp-save-media').addEventListener('click', () => { this.cm.set('imageRules', $('tp-images').value); this.cm.set('enableIframe', $('tp-iframe').checked); this.cm.set('iframeTranslateJsUrl', $('tp-iframeurl').value.trim()); this.cm.set('customTagAttributes', $('tp-tagattr').value); this.tm.applyImageRules(); this.tm.applyAttributeSettings(); this.toast.show('✅ 图片/iframe/属性设置已保存,立即翻译以生效'); }); $('tp-save-terms').addEventListener('click', () => { this.cm.set('customTerms', $('tp-terms').value.trim()); this.tm.applyCustomTerms(); this.toast.show('✅ 自定义术语已保存'); }); $('tp-listener').addEventListener('change', (e) => { this.cm.set('enableListener', e.target.checked); if (e.target.checked && !this.tm.listenerStarted) this.tm.startListener(); this.toast.show(e.target.checked ? '✅ 动态监听已启用' : '❌ 已禁用,刷新生效', 'info'); }); $('tp-ajax').addEventListener('change', (e) => { this.cm.set('enableAjaxListener', e.target.checked); if (e.target.checked) this.tm.startListener(); this.toast.show(e.target.checked ? '✅ Ajax 触发翻译已启用' : '❌ 已禁用,刷新生效', 'info'); }); $('tp-cache').addEventListener('change', (e) => { this.cm.set('enableCache', e.target.checked); if (typeof translate !== 'undefined' && translate.storage) { try { e.target.checked ? (translate.storage.enable && translate.storage.enable()) : (translate.storage.disable && translate.storage.disable()); } catch (err) {} } }); $('tp-whole').addEventListener('change', (e) => this.cm.set('enableWholeAll', e.target.checked)); $('tp-translocal').addEventListener('change', (e) => this.cm.set('translateLocal', e.target.checked)); $('tp-hidetext').addEventListener('change', (e) => this.cm.set('hideTextBeforeTranslate', e.target.checked)); $('tp-progress').addEventListener('change', (e) => this.cm.set('enableProgressTip', e.target.checked)); $('tp-waiting').addEventListener('change', (e) => this.cm.set('waitingExecute', e.target.checked)); $('tp-save-advanced').addEventListener('click', () => { this.cm.set('translateLanguagesRange', $('tp-langrange').value.trim()); this.cm.set('ajaxDelayExecuteTime', Math.max(200, parseInt($('tp-ajax-delay').value, 10) || 200)); this.cm.set('ajaxMinIntervalTime', Math.max(200, parseInt($('tp-ajax-interval').value, 10) || 800)); this.cm.set('enableUrlLanguageControl', $('tp-urlcontrol').checked); this.cm.set('urlLanguageParam', $('tp-urlparam').value.trim() || 'language'); this.cm.set('appendParams', $('tp-params').value); this.cm.set('appendHeaders', $('tp-headers').value); this.tm.applyAdvancedSettings(); this.tm.applyRequestParams(); this.toast.show('✅ 高级设置已保存;URL 语言控制将在刷新页面后按参数执行', 'info', 3200); }); $('tp-export-config').addEventListener('click', () => { try { this.exportConfiguration(); this.toast.show('✅ 配置已导出'); } catch (e) { console.error('导出配置失败:', e); this.toast.show('❌ 配置导出失败', 'error', 3000); } }); $('tp-import-config').addEventListener('click', () => $('tp-import-file').click()); $('tp-import-file').addEventListener('change', async (e) => { const file = e.target.files && e.target.files[0]; try { await this.importConfiguration(file); this.toast.show('✅ 配置导入成功,即将刷新...', 'success', 1800); setTimeout(() => location.reload(), 1800); } catch (err) { console.error('导入配置失败:', err); this.toast.show('❌ 配置导入失败:' + (err && err.message ? err.message : '文件无效'), 'error', 4000); } finally { e.target.value = ''; } }); $('tp-clearcache').addEventListener('click', () => { const n = this.cm.clearCache(); this.toast.show(`✅ 已清除 ${n} 个翻译缓存项`); }); $('tp-resetall').addEventListener('click', () => { if (confirm('确定要重置所有设置吗?此操作不可撤销。')) { this.cm.reset(); this.toast.show('✅ 已重置,即将刷新...', 'success', 1500); setTimeout(() => location.reload(), 1500); } }); } updateUI() { const cb = document.getElementById('tp-enable'); if (cb) cb.checked = this.cm.get('enabled'); } togglePanel() { if (this.panel.classList.contains('show')) { this.panel.classList.remove('show'); } else { this.panel.classList.add('show'); if (!this.cm.get('panelPosition')) this.positionPanel(); else this.ensurePanelInViewport(); } } positionPanel() { const mobile = isMobile(); if (mobile) { this.panel.style.setProperty('left', `${(window.innerWidth - this.panel.offsetWidth) / 2}px`); this.panel.style.setProperty('top', `${(window.innerHeight - this.panel.offsetHeight) / 2}px`); } else { const r = this.floatBall.getBoundingClientRect(); const pw = this.panel.offsetWidth, ph = this.panel.offsetHeight; let left = r.right + 10, top = r.top; if (left + pw > window.innerWidth) left = r.left - pw - 10; if (left < 0) left = (window.innerWidth - pw) / 2; if (top < 10) top = 10; if (top + ph > window.innerHeight - 10) top = window.innerHeight - ph - 10; this.panel.style.setProperty('left', `${left}px`); this.panel.style.setProperty('top', `${top}px`); } } ensureInViewport() { const p = this.cm.get('floatBallPosition'); const size = this.cm.get('floatBallSize'); let x = p.x, y = p.y; if (this.cm.get('allowHalfBall')) { const h = size / 2; x = Math.max(-h, Math.min(x, window.innerWidth - h)); y = Math.max(-h, Math.min(y, window.innerHeight - h)); } else { x = Math.max(0, Math.min(x, window.innerWidth - size)); y = Math.max(0, Math.min(y, window.innerHeight - size)); } this.floatBall.style.setProperty('left', `${x}px`); this.floatBall.style.setProperty('top', `${y}px`); if (x !== p.x || y !== p.y) this.cm.set('floatBallPosition', { x, y }); } ensurePanelInViewport() { if (!this.panel.classList.contains('show')) return; const p = this.cm.get('panelPosition'); if (!p) return; let x = Math.max(0, Math.min(p.x, window.innerWidth - this.panel.offsetWidth)); let y = Math.max(0, Math.min(p.y, window.innerHeight - this.panel.offsetHeight)); this.panel.style.setProperty('left', `${x}px`); this.panel.style.setProperty('top', `${y}px`); if (x !== p.x || y !== p.y) this.cm.set('panelPosition', { x, y }); } } (function applyHideTextEarly() { try { const cfg = loadCfg(); const policyManager = new ConfigManager(); if (cfg.enabled && cfg.hideTextBeforeTranslate && policyManager.isSiteAllowed() && typeof translate !== 'undefined') { translate.language.setLocal(cfg.localLanguage || 'english'); const real = (cfg.translateService === 'translate.service.public' || cfg.translateService === 'translate.service.private') ? 'translate.service' : cfg.translateService; try { translate.service.use(real); } catch (e) {} if (translate.visual && translate.visual.webPageLoadTranslateBeforeHiddenText) { translate.visual.webPageLoadTranslateBeforeHiddenText(); } } } catch (e) {} })(); async function init() { if (document.readyState === 'loading') { await new Promise(r => document.addEventListener('DOMContentLoaded', r)); } try { if (typeof translate === 'undefined') { console.error('translate.js 未加载,请检查 @require 配置'); return; } const cm = new ConfigManager(); const tm = new TranslateManager(cm); const ui = new UIManager(cm, tm); GM_registerMenuCommand('打开翻译设置', () => ui.togglePanel()); GM_registerMenuCommand('立即翻译', () => ui.translateNow(false)); GM_registerMenuCommand('还原为原文', () => tm.restoreOriginal()); (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window).translateHelper = { config: cm, translate: tm, ui }; console.log('智能翻译助手 v' + cm.get('version') + ' 已加载,translate.js 版本: ' + (translate.version || '未知')); } catch (e) { console.error('翻译脚本初始化失败:', e); } } setTimeout(init, 100); })(); })();