// ==UserScript== // @name Ultimate Text Selection Translator – Instantly Translate Any Selected Text // @name:fr Ultimate Text Selection Translator – Traduisez instantanément n'importe quel texte sélectionné // @name:es Ultimate Text Selection Translator – Traduce instantáneamente cualquier texto seleccionado // @name:de Ultimate Text Selection Translator – Übersetzen Sie jeden ausgewählten Text sofort // @name:ru Ultimate Text Selection Translator – Мгновенно переводите любой выделенный текст // @name:zh-CN Ultimate Text Selection Translator – 立即翻译任何选定的文本 // @name:zh-TW Ultimate Text Selection Translator – 立即翻譯任何選定的文本 // @name:ja Ultimate Text Selection Translator – 選択したテキストを即座に翻訳 // @name:pt Ultimate Text Selection Translator – Traduza instantaneamente qualquer texto selecionado // @name:it Ultimate Text Selection Translator – Traduci istantaneamente qualsiasi testo selezionato // @name:ar Ultimate Text Selection Translator – ترجمة فورية لأي نص محدد // @name:be Ultimate Text Selection Translator – Імгненна перакладайце любы выбраны тэкст // @name:bg Ultimate Text Selection Translator – Незабавен превод на всеки избран текст // @name:cs Ultimate Text Selection Translator – Okamžitě přeložte jakýkoli vybraný text // @name:da Ultimate Text Selection Translator – Oversæt øjeblikkeligt enhver valgt tekst // @name:el Ultimate Text Selection Translator – Μεταφράστε άμεσα οποιοδήποτε επιλεγμένο κείμενο // @name:eo Ultimate Text Selection Translator – Tuj Traduku Iun Elektitan Tekston // @name:fi Ultimate Text Selection Translator – Käännä välittömästi kaikki valitut tekstit // @name:he Ultimate Text Selection Translator – תרגם באופן מיידי כל טקסט שנבחר // @name:hr Ultimate Text Selection Translator – Trenutačno prevedite bilo koji odabrani tekst // @name:hu Ultimate Text Selection Translator – Azonnal lefordíthatja a kiválasztott szöveget // @name:id Ultimate Text Selection Translator – Terjemahkan Teks yang Dipilih Secara Instan // @name:ka Ultimate Text Selection Translator – მყისიერად თარგმნეთ ნებისმიერი არჩეული ტექსტი // @name:ko Ultimate Text Selection Translator – 선택한 텍스트를 즉시 번역하세요 // @name:mr Ultimate Text Selection Translator – कोणताही निवडलेला मजकूर त्वरित अनुवादित करा // @name:nl Ultimate Text Selection Translator – Vertaal onmiddellijk elke geselecteerde tekst // @name:nb Ultimate Text Selection Translator – Oversett alle valgt tekst umiddelbart // @name:pl Ultimate Text Selection Translator – Natychmiast przetłumacz dowolny zaznaczony tekst // @name:pt-BR Ultimate Text Selection Translator – Traduza instantaneamente qualquer texto selecionado // @name:ro Ultimate Text Selection Translator – Traduceți instantaneu orice text selectat // @name:sk Ultimate Text Selection Translator – Okamžite preložte akýkoľvek vybraný text // @name:sr Ultimate Text Selection Translator – Одмах преведите било који одабрани текст // @name:sv Ultimate Text Selection Translator – Översätt direkt valfri text // @name:th Ultimate Text Selection Translator – แปลข้อความที่เลือกทันที // @name:tr Ultimate Text Selection Translator – Seçilen Metni Anında Çevir // @name:ug Ultimate Text Selection Translator – تاللانغان تېكىستنى دەرھال تەرجىمە قىلىڭ // @name:uk Ultimate Text Selection Translator – Миттєво перекладіть будь-який виділений текст // @name:vi Ultimate Text Selection Translator – Dịch ngay lập tức mọi văn bản đã chọn // @name:fr-CA Ultimate Text Selection Translator – Traduisez instantanément n'importe quel texte sélectionné // @name:ckb Ultimate Text Selection Translator – Her Nivîsarek Hilbijartî tavilê Wergerîne // @name:es-419 Ultimate Text Selection Translator – Traduce instantáneamente cualquier texto seleccionado // @description Instantly translate selected text using the smart button or the Ctrl+L shortcut. Automatically detects the language and translates it into the language of your choice. // @description:fr Traduisez instantanément le texte sélectionné grâce au bouton intelligent ou au raccourci Ctrl+L. Détection automatique de la langue et traduction immédiate dans la langue de votre choix. // @description:es Traduce instantáneamente el texto seleccionado mediante el botón inteligente o el atajo Ctrl+L. Detecta automáticamente el idioma y lo traduce al idioma de tu elección. // @description:de Übersetzen Sie ausgewählten Text sofort über die intelligente Schaltfläche oder die Tastenkombination Strg+L. Erkennt die Sprache automatisch und übersetzt sie in die Sprache Ihrer Wahl. // @description:ru Мгновенно переводите выделенный текст с помощью умной кнопки или сочетания Ctrl+L. Автоматически определяет язык и переводит его на выбранный вами язык. // @description:zh-CN 使用智能按钮或 Ctrl+L 快捷键立即翻译所选文本。自动检测语言并将其翻译为您选择的语言。 // @description:zh-TW 使用智慧按鈕或 Ctrl+L 快捷鍵立即翻譯所選文字。自動偵測語言並翻譯為您選擇的語言。 // @description:ja スマートボタンまたは Ctrl+L ショートカットで選択したテキストを即座に翻訳します。言語を自動検出し、選択した言語に翻訳します。 // @description:pt Traduza instantaneamente o texto selecionado usando o botão inteligente ou o atalho Ctrl+L. Detecta automaticamente o idioma e o traduz para o idioma de sua escolha. // @description:it Traduci istantaneamente il testo selezionato tramite il pulsante intelligente o la scorciatoia Ctrl+L. Rileva automaticamente la lingua e la traduce nella lingua scelta. // @description:ar ترجم النص المحدد فورًا باستخدام الزر الذكي أو اختصار Ctrl+L. يكتشف اللغة تلقائيًا ويترجمها إلى اللغة التي تختارها. // @description:be Імгненна перакладайце вылучаны тэкст з дапамогай разумнай кнопкі або спалучэння Ctrl+L. Аўтаматычна вызначае мову і перакладае яе на мову па вашым выбары. // @description:bg Превеждайте избрания текст незабавно чрез интелигентния бутон или клавишната комбинация Ctrl+L. Автоматично разпознава езика и го превежда на езика по ваш избор. // @description:cs Okamžitě přeložte vybraný text pomocí chytrého tlačítka nebo zkratky Ctrl+L. Automaticky rozpozná jazyk a přeloží jej do vámi zvoleného jazyka. // @description:da Oversæt valgt tekst med det samme ved hjælp af den smarte knap eller genvejen Ctrl+L. Registrerer automatisk sproget og oversætter det til det sprog, du vælger. // @description:el Μεταφράστε άμεσα το επιλεγμένο κείμενο χρησιμοποιώντας το έξυπνο κουμπί ή τη συντόμευση Ctrl+L. Ανιχνεύει αυτόματα τη γλώσσα και τη μεταφράζει στη γλώσσα της επιλογής σας. // @description:eo Traduku elektitan tekston tuj per la inteligenta butono aŭ la klavkombino Ctrl+L. Aŭtomate detektas la lingvon kaj tradukas ĝin al la lingvo laŭ via elekto. // @description:fi Käännä valittu teksti välittömästi älypainikkeella tai Ctrl+L-pikanäppäimellä. Tunnistaa kielen automaattisesti ja kääntää sen valitsemaasi kieleen. // @description:he תרגם טקסט נבחר באופן מיידי באמצעות הכפתור החכם או קיצור הדרך Ctrl+L. מזהה אוטומטית את השפה ומתרגם אותה לשפה שתבחר. // @description:hr Odmah prevedite odabrani tekst pomoću pametnog gumba ili prečaca Ctrl+L. Automatski prepoznaje jezik i prevodi ga na jezik po vašem izboru. // @description:hu Fordítsa le azonnal a kijelölt szöveget az intelligens gombbal vagy a Ctrl+L billentyűkombinációval. Automatikusan felismeri a nyelvet, és az Ön által választott nyelvre fordítja. // @description:id Terjemahkan teks yang dipilih secara instan menggunakan tombol pintar atau pintasan Ctrl+L. Secara otomatis mendeteksi bahasa dan menerjemahkannya ke bahasa pilihan Anda. // @description:ka შერჩეული ტექსტი მყისიერად თარგმნეთ ჭკვიანი ღილაკის ან Ctrl+L მალსახმობის გამოყენებით. ავტომატურად ამოიცნობს ენას და თარგმნის თქვენს მიერ არჩეულ ენაზე. // @description:ko 스마트 버튼 또는 Ctrl+L 단축키를 사용하여 선택한 텍스트를 즉시 번역하세요. 언어를 자동으로 감지하고 원하는 언어로 번역합니다. // @description:mr स्मार्ट बटण किंवा Ctrl+L शॉर्टकट वापरून निवडलेला मजकूर त्वरित भाषांतरित करा. भाषा आपोआप ओळखते आणि तुमच्या निवडीच्या भाषेत अनुवादित करते. // @description:nl Vertaal geselecteerde tekst direct met de slimme knop of de sneltoets Ctrl+L. Detecteert automatisch de taal en vertaalt deze naar de taal van jouw keuze. // @description:nb Oversett valgt tekst umiddelbart med den smarte knappen eller Ctrl+L-snarveien. Oppdager språket automatisk og oversetter det til språket du velger. // @description:pl Natychmiast przetłumacz zaznaczony tekst za pomocą inteligentnego przycisku lub skrótu Ctrl+L. Automatycznie wykrywa język i tłumaczy go na wybrany przez Ciebie język. // @description:pt-BR Traduza instantaneamente o texto selecionado usando o botão inteligente ou o atalho Ctrl+L. Detecta automaticamente o idioma e o traduz para o idioma de sua escolha. // @description:ro Traduceți instantaneu textul selectat folosind butonul inteligent sau combinația Ctrl+L. Detectează automat limba și o traduce în limba aleasă de dvs. // @description:sk Okamžite preložte vybraný text pomocou inteligentného tlačidla alebo skratky Ctrl+L. Automaticky rozpozná jazyk a preloží ho do jazyka podľa vášho výberu. // @description:sr Одмах преведите изабрани текст помоћу паметног дугмета или пречице Ctrl+L. Аутоматски препознаје језик и преводи га на језик по вашем избору. // @description:sv Översätt markerad text direkt med den smarta knappen eller genvägen Ctrl+L. Identifierar automatiskt språket och översätter det till det språk du väljer. // @description:th แปลข้อความที่เลือกทันทีด้วยปุ่มอัจฉริยะหรือคีย์ลัด Ctrl+L ระบบจะตรวจจับภาษาอัตโนมัติและแปลเป็นภาษาที่คุณเลือก // @description:tr Seçilen metni akıllı düğme veya Ctrl+L kısayolu ile anında çevirin. Dili otomatik olarak algılar ve seçtiğiniz dile çevirir. // @description:ug تاللانغان تېكىستنى ئەقلىي كۇنۇپكا ياكى Ctrl+L قىسقا يولى ئارقىلىق دەرھال تەرجىمە قىلىڭ. تىلنى ئاپتوماتىك بايقىپ، سىز تاللىغان تىلغا تەرجىمە قىلىدۇ. // @description:uk Миттєво перекладіть виділений текст за допомогою розумної кнопки або поєднання Ctrl+L. Автоматично визначає мову та перекладає її на обрану вами мову. // @description:vi Dịch ngay văn bản đã chọn bằng nút thông minh hoặc phím tắt Ctrl+L. Tự động phát hiện ngôn ngữ và dịch sang ngôn ngữ bạn chọn. // @description:fr-CA Traduisez instantanément le texte sélectionné grâce au bouton intelligent ou au raccourci Ctrl+L. Détection automatique de la langue et traduction immédiate dans la langue de votre choix. // @description:ckb دەقە هەڵبژێردراوەکانت بە شێوەیەکی خێرا بە دوگمەی زیرەک یان Ctrl+L وەرگێڕە. زمان بە ئۆتۆماتیکی دەدۆزێتەوە و دەیگۆڕێتە سەر زمانی هەڵبژێردراوی تۆ. // @description:es-419 Traduce instantáneamente el texto seleccionado mediante el botón inteligente o el atajo Ctrl+L. Detecta automáticamente el idioma y lo traduce al idioma que elijas. // @namespace https://github.com/DREwX-code // @author Dℝ∃wX // @copyright 2025-2026 Dℝ∃wX // @license Apache-2.0 // @require https://update.greasyfork.icu/scripts/556911/1754127/UTST%20Translation%20Library.js // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @connect translate.googleapis.com // @match *://*/* // @run-at document-start // @version 1.4.1 // @icon https://raw.githubusercontent.com/DREwX-code/Ultimate-Text-Selection-Translator/refs/heads/main/assets/icons/Icon_Translate_Script.png // @tag translation // @tag text selection // @tag translate // @tag google translate // @tag shortcut // @tag productivity // @tag accessibility // @tag language // @tag multilingual // @downloadURL none // ==/UserScript== /* Copyright 2025-2026 Dℝ∃wX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ (function () { 'use strict'; function bootstrap() { GM_addStyle(` @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'); #closeButton:hover svg { stroke: #ff4d4d !important; filter: drop-shadow(0 0 4px rgba(255, 77, 77, 0.5)); transform: scale(1.1); transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } @keyframes utst-shimmer { 0% { background-position: -468px 0; } 100% { background-position: 468px 0; } } .utst-loading { position: relative !important; overflow: hidden !important; pointer-events: none !important; } .utst-loading::after { content: "" !important; position: absolute !important; inset: 0 !important; background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0) 100%) !important; background-size: 468px 100% !important; animation: utst-shimmer 1.5s infinite linear !important; z-index: 5 !important; } .utst-panel-light .utst-loading::after { background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(0,0,0,0.05) 50%, rgba(255,255,255,0) 100%) !important; } .utst-loading-overlay { position: absolute !important; inset: 0 !important; background: rgba(0, 0, 0, 0.2) !important; backdrop-filter: blur(2px) !important; display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 10px !important; z-index: 10 !important; pointer-events: none !important; opacity: 0 !important; transition: opacity 0.3s ease !important; } .utst-loading-active .utst-loading-overlay { opacity: 1 !important; } .utst-loading-shimmer { width: 100% !important; height: 100% !important; background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.1) 50%, rgba(255,255,255,0) 100%) !important; background-size: 468px 100% !important; animation: utst-shimmer 1.5s infinite linear !important; } .utst-panel-light .utst-loading-shimmer { background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(0,0,0,0.06) 50%, rgba(255,255,255,0) 100%) !important; } .utst-scroll { scrollbar-width: thin !important; scrollbar-color: rgba(100, 149, 237, 0.5) rgba(0, 0, 0, 0.1) !important; } .utst-scroll::-webkit-scrollbar { width: 6px !important; height: 6px !important; } .utst-scroll::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05) !important; border-radius: 3px !important; } .utst-scroll::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15) !important; border-radius: 3px !important; border: 1px solid rgba(255, 255, 255, 0.05) !important; } .utst-scroll::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3) !important; } #utstSelectionBubble { position: absolute; z-index: 2147483647; display: flex; align-items: center; gap: 0; height: 40px; padding: 0 6px; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.15); background: rgba(25, 25, 35, 0.85); /* Dark semi-transparent */ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: #fff; opacity: 0; transform: translateY(-8px) scale(0.95); pointer-events: none; transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); font-family: 'Roboto', sans-serif; box-sizing: border-box !important; } #utstTranslationBox, #utstTranslationBox * { box-sizing: border-box !important; } #utstTranslationBox, #fullscreenOverlay, #utstSelectionBubble, .utst-inline-lang-panel { font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; font-size: 14px !important; line-height: 1.35 !important; letter-spacing: normal !important; text-transform: none !important; text-size-adjust: 100% !important; -webkit-text-size-adjust: 100% !important; direction: ltr !important; writing-mode: horizontal-tb !important; zoom: 1 !important; isolation: isolate !important; } #fullscreenOverlay, #fullscreenOverlay *, #utstSelectionBubble, #utstSelectionBubble *, .utst-inline-lang-panel, .utst-inline-lang-panel * { box-sizing: border-box !important; text-transform: none !important; letter-spacing: normal !important; } #fullscreenOverlay { overflow: auto !important; } #fullscreenPanel { width: min(1100px, 95vw) !important; max-width: 95vw !important; max-height: 92vh !important; overflow: auto !important; box-sizing: border-box !important; } #fullscreenColumns { min-width: 0 !important; } #fullscreenColumns > div { min-width: 0 !important; } #fullscreenSource, #fullscreenTarget, #fullscreenSourceWrap, #fullscreenTargetWrap { width: 100% !important; max-width: 100% !important; min-width: 0 !important; } #fullscreenSource, #fullscreenTarget { min-height: 200px !important; max-height: min(62vh, 560px) !important; resize: vertical !important; box-sizing: border-box !important; } #fullscreenSourceWrap, #fullscreenTargetWrap { box-sizing: border-box !important; } #utstTranslationBox { width: min(420px, calc(100vw - 20px)) !important; min-width: min(420px, calc(100vw - 20px)) !important; max-width: min(420px, calc(100vw - 20px)) !important; } #utstTranslationBox select, #utstTranslationBox option { -webkit-appearance: menulist !important; -moz-appearance: menulist !important; appearance: auto !important; background-image: none !important; font-family: inherit !important; font-size: 13px !important; line-height: 1.2 !important; color: #fff !important; } #utstTranslationBox select { padding-right: 24px !important; min-height: 30px !important; } #utstSelectionBubble.utst-visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } #utstSelectionBubbleClose { width: 30px; height: 30px; border: 0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.7); background: transparent; font-size: 16px; font-weight: 500; line-height: 1; transition: all 0.2s ease; cursor: pointer; user-select: none; margin-right: 2px; } #utstSelectionBubbleClose:hover { background: rgba(255, 255, 255, 0.1); color: #fff; transform: rotate(90deg); } #utstSelectionBubbleDivider { width: 1px; height: 20px; margin: 0 6px; background: rgba(255, 255, 255, 0.2); } #utstSelectionBubbleAction { width: 30px; height: 30px; border: 0; border-radius: 50%; padding: 0; background: transparent; color: #fff; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } #utstSelectionBubbleAction svg { width: 18px; height: 18px; color: #d8e8ff; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); } #utstSelectionBubbleAction:hover { background: rgba(255, 255, 255, 0.15); transform: scale(1.1); } #speakTooltip .utst-speak-option:hover { background: rgba(255,255,255,0.12); } #fullscreenSwap { background: transparent !important; border: none !important; box-shadow: none !important; } #fullscreenSwap:hover, #fullscreenSwap:active { background: transparent !important; box-shadow: none !important; } #utstBubbleCloseMenu { position: absolute; left: 0; top: calc(100% + 10px); display: none; flex-direction: column; min-width: 180px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(30, 30, 40, 0.95); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); overflow: hidden; animation: utstFadeIn 0.2s ease; } @keyframes utstFadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } #utstBubbleCloseMenu.utst-open { display: flex; } .utst-bubble-menu-btn { border: 0; background: transparent; color: rgba(255, 255, 255, 0.9); text-align: left; font-size: 13px; padding: 10px 14px; cursor: pointer; transition: background 0.15s ease; font-family: inherit; } .utst-bubble-menu-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } .utst-bubble-settings { margin-top: 14px; padding-top: 14px; border-top: 1px solid rgba(255, 255, 255, 0.1); } #utstTranslationBox #settingsHeader { padding: 4px 8px; border-radius: 10px; background: #222b3f; border: 1px solid rgba(255, 255, 255, 0.08); right: 8px; z-index: 14; } #utstTranslationBox #settingsPanel { position: absolute; top: 62px; left: 8px; right: 8px; bottom: 10px; z-index: 13; margin: 0; min-width: 0 !important; max-width: none !important; min-height: 0 !important; max-height: none !important; overflow-y: auto; border-radius: 10px; background: transparent; } #utstTranslationBox #translatorPanel { transition: filter 0.18s ease, opacity 0.18s ease; } #utstTranslationBox #translationTextWrap, #fullscreenPanel #fullscreenTargetWrap { position: relative; } .utst-modern-loader { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; border-radius: 10px; background: linear-gradient(135deg, rgba(12, 20, 36, 0.7) 0%, rgba(16, 28, 50, 0.62) 100%); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); opacity: 0; pointer-events: none; transform: scale(0.985); transition: opacity 0.2s ease, transform 0.2s ease; z-index: 9; } .utst-modern-loader.is-active { opacity: 1; pointer-events: auto; transform: scale(1); } .utst-modern-loader__card { display: flex; align-items: center; gap: 10px; min-width: 170px; max-width: calc(100% - 20px); padding: 10px 12px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(8, 14, 28, 0.64); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.28); } .utst-modern-loader__ring { width: 20px; height: 20px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.2); border-top-color: #7bb1ff; animation: utstLoaderSpin 0.8s linear infinite; flex: none; } .utst-modern-loader[data-mode="language"] .utst-modern-loader__ring { border-top-color: #4fd0a9; } .utst-modern-loader__body { display: flex; flex-direction: column; gap: 6px; min-width: 105px; } .utst-modern-loader__title { font-size: 12px; font-weight: 700; letter-spacing: 0.2px; color: rgba(245, 248, 255, 0.95); line-height: 1.2; white-space: nowrap; } .utst-modern-loader__line { width: 100%; height: 6px; border-radius: 999px; background: linear-gradient(90deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.4) 48%, rgba(255, 255, 255, 0.14) 100%); background-size: 180% 100%; animation: utstLoaderShimmer 1.1s linear infinite; } html.utst-theme-dark .utst-modern-loader { background: linear-gradient(135deg, rgba(10, 10, 10, 0.78) 0%, rgba(20, 20, 20, 0.78) 100%) !important; } html.utst-theme-dark .utst-modern-loader__card { background: rgba(16, 16, 16, 0.74) !important; border-color: rgba(255, 255, 255, 0.14) !important; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45) !important; } html.utst-theme-dark .utst-modern-loader__ring { border-color: rgba(255, 255, 255, 0.16) !important; border-top-color: #d0d0d0 !important; } html.utst-theme-dark .utst-modern-loader[data-mode="language"] .utst-modern-loader__ring { border-top-color: #55c89a !important; } html.utst-theme-dark .utst-modern-loader__title { color: rgba(245, 245, 245, 0.94) !important; } html.utst-theme-dark .utst-modern-loader__line { background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.32) 50%, rgba(255, 255, 255, 0.1) 100%) !important; } @keyframes utstLoaderSpin { to { transform: rotate(360deg); } } @keyframes utstLoaderShimmer { from { background-position: 180% 0; } to { background-position: -80% 0; } } #utstTranslationBox.utst-settings-open #translatorPanel { filter: blur(4px) saturate(0.9); opacity: 0.34; pointer-events: none; user-select: none; } .utst-toggle-row { display: flex; align-items: center; gap: 10px; color: rgba(255, 255, 255, 0.9); font-size: 13px; margin-bottom: 10px; user-select: none; cursor: pointer; } .utst-toggle-row input[type="checkbox"] { appearance: none; width: 36px; height: 20px; background: rgba(255, 255, 255, 0.1); border-radius: 20px; position: relative; cursor: pointer; transition: background 0.2s; border: 1px solid rgba(255, 255, 255, 0.1); } .utst-toggle-row input[type="checkbox"]::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; background: #fff; border-radius: 50%; transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.3); } .utst-toggle-row input[type="checkbox"]:checked { background: #4a90e2; border-color: #4a90e2; } .utst-toggle-row input[type="checkbox"]:checked::after { transform: translateX(16px); } .utst-blacklist-controls { display: flex; gap: 8px; margin-top: 8px; } .utst-blacklist-input { flex: 1; min-width: 0; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.15); background: rgba(0, 0, 0, 0.2); color: #fff; font-size: 12px; font-family: inherit; transition: border-color 0.2s; } .utst-blacklist-input:focus { outline: none; border-color: #4a90e2; } .utst-blacklist-add { border: none; border-radius: 8px; background: #4a90e2; color: #fff; font-size: 12px; font-weight: 600; padding: 0 12px; cursor: pointer; transition: background 0.2s; } .utst-blacklist-add:hover { background: #357abd; } .utst-blacklist-list { margin-top: 10px; max-height: 120px; overflow-y: auto; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 8px; background: rgba(0, 0, 0, 0.15); } .utst-blacklist-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; color: rgba(255, 255, 255, 0.9); padding: 6px 8px; border-radius: 6px; background: rgba(255, 255, 255, 0.03); transition: background 0.1s; } .utst-blacklist-item:hover { background: rgba(255, 255, 255, 0.08); } .utst-blacklist-item + .utst-blacklist-item { margin-top: 4px; } .utst-blacklist-remove { border: none; border-radius: 4px; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.7); width: 20px; height: 20px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; font-size: 14px; transition: all 0.2s; } .utst-blacklist-remove:hover { background: rgba(255, 77, 77, 0.2); color: #ff4d4d; } .utst-blacklist-empty { font-size: 12px; color: rgba(255, 255, 255, 0.5); padding: 4px; text-align: center; } html.utst-theme-blue #utstSelectionBubble { /* Muted deep blue, inspired by the panel but less saturated/flashy */ background: linear-gradient(135deg, rgba(30, 30, 47, 0.96) 0%, rgba(35, 35, 52, 0.96) 100%); border-color: rgba(255, 255, 255, 0.15); box-shadow: 0 8px 25px rgba(10, 14, 28, 0.5); } html.utst-theme-blue #utstSelectionBubbleDivider { background: rgba(255, 255, 255, 0.2); } html.utst-theme-blue #utstSelectionBubbleAction svg, html.utst-theme-blue #utstSelectionBubbleClose { color: #e0e6ff; } html.utst-theme-dark #utstSelectionBubble { background: linear-gradient(135deg, rgba(18, 18, 18, 0.96) 0%, rgba(28, 28, 28, 0.96) 100%) !important; border-color: rgba(255, 255, 255, 0.08) !important; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.6) !important; } html.utst-theme-dark #utstSelectionBubbleDivider { background: rgba(255, 255, 255, 0.15) !important; } html.utst-theme-dark #utstSelectionBubbleAction svg, html.utst-theme-dark #utstSelectionBubbleClose { color: #d0d0d0 !important; } html.utst-theme-dark #utstTranslationBox { /* True neutral dark, removing blue tint */ background: linear-gradient(135deg, #121212 0%, #1e1e1e 100%) !important; border-color: rgba(255,255,255,0.08) !important; } html.utst-theme-dark #utstTranslationBox #dragHandle { background: linear-gradient(120deg, #1a1a1a, #252525) !important; } html.utst-theme-dark #fullscreenPanel { background: linear-gradient(135deg, #121212 0%, #1e1e1e 100%) !important; border-color: rgba(255,255,255,0.08) !important; } html.utst-theme-blue #utstTranslationBox #settingsHeader { background: #222b3f !important; border-color: rgba(139, 177, 255, 0.28) !important; } html.utst-theme-blue #utstTranslationBox #settingsPanel { background: transparent !important; } html.utst-theme-dark #utstTranslationBox #settingsHeader { background: #1a1a1a !important; border-color: rgba(255, 255, 255, 0.14) !important; } html.utst-theme-dark #utstTranslationBox #settingsPanel { background: transparent !important; } html.utst-theme-light #utstSelectionBubble { /* Softer, less blinding white - slightly grey/blue tinted off-white */ background: linear-gradient(135deg, #f0f2f5 0%, #e1e4e8 100%) !important; border-color: rgba(0, 0, 0, 0.1) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important; } html.utst-theme-light #utstSelectionBubbleDivider { background: rgba(0, 0, 0, 0.1) !important; } html.utst-theme-light #utstSelectionBubbleAction svg, html.utst-theme-light #utstSelectionBubbleClose { color: #4a5568 !important; /* Dark grey-blue */ } html.utst-theme-light #utstBubbleCloseMenu { background: rgba(255, 255, 255, 0.98) !important; border-color: rgba(0, 0, 0, 0.1) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; } html.utst-theme-light .utst-bubble-menu-btn { color: #2d3748 !important; } html.utst-theme-light .utst-bubble-menu-btn:hover { background: rgba(0, 0, 0, 0.05) !important; } html.utst-theme-light #utstTranslationBox { /* Softer light theme background */ background: linear-gradient(135deg, #ffffff 0%, #f7f9fc 100%) !important; border-color: rgba(0, 0, 0, 0.08) !important; color: #1a202c !important; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12) !important; } html.utst-theme-light #utstTranslationBox #dragHandle { background: linear-gradient(120deg, #edf2f7, #e2e8f0) !important; color: #4a5568 !important; box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05) !important; } html.utst-theme-light #utstTranslationBox #dragHandle > div { background: rgba(74, 85, 104, 0.45) !important; } /* Ensure ALL icons in the box are dark in light theme */ html.utst-theme-light #utstTranslationBox svg { stroke: #4a5568; } /* Keep specific icon colors if needed, e.g. close button might be red */ html.utst-theme-light #utstTranslationBox #closeButton svg { stroke: #ef4444 !important; } html.utst-theme-light #utstTranslationBox #settingsButton svg path { stroke: #4a5568 !important; } html.utst-theme-light #utstTranslationBox #translatorPanel *, html.utst-theme-light #utstTranslationBox #settingsPanel *, html.utst-theme-light #utstTranslationBox #settingsHeader *, html.utst-theme-light #fullscreenPanel * { color: #2d3748 !important; } html.utst-theme-light #utstTranslationBox #translationText { background: #f7fafc !important; border: 1px solid #e2e8f0 !important; color: #1a202c !important; } html.utst-theme-light .utst-modern-loader { background: linear-gradient(135deg, rgba(241, 245, 249, 0.78) 0%, rgba(226, 232, 240, 0.78) 100%) !important; } html.utst-theme-light .utst-modern-loader__card { background: rgba(255, 255, 255, 0.9) !important; border-color: rgba(148, 163, 184, 0.45) !important; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12) !important; } html.utst-theme-light .utst-modern-loader__ring { border-color: rgba(71, 85, 105, 0.2) !important; border-top-color: #2563eb !important; } html.utst-theme-light .utst-modern-loader[data-mode="language"] .utst-modern-loader__ring { border-top-color: #0f9f6e !important; } html.utst-theme-light .utst-modern-loader__title { color: #1e293b !important; } html.utst-theme-light .utst-modern-loader__line { background: linear-gradient(90deg, rgba(30, 41, 59, 0.08) 0%, rgba(37, 99, 235, 0.28) 50%, rgba(30, 41, 59, 0.08) 100%) !important; } html.utst-theme-light #utstTranslationBox select, html.utst-theme-light #utstTranslationBox input { background: #ffffff !important; border: 1px solid #cbd5e0 !important; color: #2d3748 !important; } html.utst-theme-light #utstTranslationBox .utst-toggle-row input[type="checkbox"] { background: #d9e1ec !important; border: 1px solid #b8c4d6 !important; } html.utst-theme-light #utstTranslationBox .utst-toggle-row input[type="checkbox"]::after { background: #ffffff !important; } html.utst-theme-light #utstTranslationBox .utst-toggle-row input[type="checkbox"]:checked { background: #4a90e2 !important; border-color: #4a90e2 !important; } html.utst-theme-light #utstTranslationBox #bubbleBlacklistList { background: #ffffff !important; border-color: #e2e8f0 !important; } html.utst-theme-light #utstTranslationBox .utst-blacklist-item { background: #f7fafc !important; color: #2d3748 !important; } html.utst-theme-light #utstTranslationBox .utst-blacklist-empty { color: #a0aec0 !important; } html.utst-theme-light #utstTranslationBox .utst-blacklist-remove { background: #edf2f7 !important; color: #718096 !important; } html.utst-theme-light #utstTranslationBox #settingsPanel #bubbleBlacklistAdd { color: #ffffff !important; } html.utst-theme-light #utstTranslationBox #settingsPanel #bubbleBlacklistAdd:hover { color: #ffffff !important; } html.utst-theme-light #utstTranslationBox #settingsHeader { background: #ffffff !important; border-color: rgba(148, 163, 184, 0.45) !important; } html.utst-theme-light #utstTranslationBox #settingsPanel { background: transparent !important; } html.utst-theme-light #utstTranslationBox #panelThemeTrigger { background: #ffffff !important; border: 1px solid #94a3b8 !important; color: #2d3748 !important; } html.utst-theme-light #utstTranslationBox #panelThemePanel { background: #ffffff !important; border: 1px solid #cbd5e0 !important; } html.utst-theme-light #utstTranslationBox .utst-bubble-settings { border-top-color: rgba(74, 85, 104, 0.28) !important; } html.utst-theme-light #utstTranslationBox #speakTooltip { background: #ffffff !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 6px rgba(0,0,0,0.05) !important; } html.utst-theme-light #utstTranslationBox #speakTooltip .utst-speak-option:hover { background: rgba(45, 92, 190, 0.14) !important; color: #1f3f73 !important; } html.utst-theme-blue #utstTranslationBox #speakTooltip { background: rgba(20, 36, 64, 0.98) !important; border: 1px solid rgba(139, 177, 255, 0.34) !important; box-shadow: 0 10px 24px rgba(6, 15, 35, 0.48) !important; } html.utst-theme-blue #utstTranslationBox #speakTooltip .utst-speak-option:hover { background: rgba(120, 165, 255, 0.22) !important; color: #e9f1ff !important; } html.utst-theme-light #fullscreenOverlay { background: rgba(0, 0, 0, 0.65) !important; backdrop-filter: blur(8px) !important; } html.utst-theme-light #fullscreenPanel { background: linear-gradient(135deg, #ffffff 0%, #f7f9fc 100%) !important; border-color: rgba(0, 0, 0, 0.08) !important; box-shadow: 0 20px 50px rgba(0,0,0,0.1) !important; } html.utst-theme-light #fullscreenPanel svg { stroke: #4a5568; } html.utst-theme-light #fullscreenPanel #fullscreenClose svg { stroke: #ef4444 !important; } html.utst-theme-light #fullscreenPanel #fullscreenSourceCopy, html.utst-theme-light #fullscreenPanel #fullscreenSourceSpeak, html.utst-theme-light #fullscreenPanel #fullscreenTargetCopy, html.utst-theme-light #fullscreenPanel #fullscreenTargetSpeak { background: #ffffff !important; border: 1px solid #cbd5e0 !important; } html.utst-theme-light #fullscreenPanel #fullscreenSourceCopy:hover, html.utst-theme-light #fullscreenPanel #fullscreenSourceSpeak:hover, html.utst-theme-light #fullscreenPanel #fullscreenTargetCopy:hover, html.utst-theme-light #fullscreenPanel #fullscreenTargetSpeak:hover { background: #f8fafc !important; border-color: #94a3b8 !important; } html.utst-theme-light #fullscreenPanel textarea, html.utst-theme-light #fullscreenPanel input, html.utst-theme-light #fullscreenPanel button[id$="LangTrigger"] { background: #ffffff !important; border: 1px solid #cbd5e0 !important; color: #2d3748 !important; } html.utst-theme-light #fullscreenPanel [id$="LangPanel"] { background: #ffffff !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 10px 15px rgba(0,0,0,0.05) !important; } `); const translationLibrary = (typeof window !== 'undefined' ? window.TraductionOutilTranslator : null) || (typeof globalThis !== 'undefined' ? globalThis.TraductionOutilTranslator : null); if (!translationLibrary || !translationLibrary.languageNames) { console.error('[Ultimate Translator] Missing TraductionOutilTranslator language library.'); return; } const browserLang = navigator.language.split('-')[0]; const languageNames = translationLibrary.languageNames; const englishLangNames = languageNames.en || {}; const supportedUiLanguages = Array.isArray(translationLibrary.supportedUiLanguages) && translationLibrary.supportedUiLanguages.length ? translationLibrary.supportedUiLanguages : Object.keys(languageNames); const storedToolLangPref = GM_getValue('defaultToolLang', 'browser'); const normalizedToolLangPref = (storedToolLangPref === 'browser' || supportedUiLanguages.includes(storedToolLangPref)) ? storedToolLangPref : 'browser'; if (normalizedToolLangPref !== storedToolLangPref) { GM_setValue('defaultToolLang', normalizedToolLangPref); } function resolveUiLang(preference) { if (preference === 'browser') { return languageNames[browserLang] ? browserLang : 'en'; } return languageNames[preference] ? preference : (languageNames[browserLang] ? browserLang : 'en'); } let toolLanguagePreference = normalizedToolLangPref; const uiLang = resolveUiLang(toolLanguagePreference); let langNames = languageNames[uiLang]; let errors = langNames.errors; let tooltips = langNames.tooltips; let dragHandleLabel = langNames.dragHandleLabel || languageNames.en.dragHandleLabel; let overlayLabels = langNames.overlay || languageNames.en.overlay; let settingsTitle = langNames.settingsTitle || languageNames.en.settingsTitle; let settingsDefaultLabel = langNames.settingsDefaultLabel || languageNames.en.settingsDefaultLabel; let settingsToolLabel = langNames.settingsToolLabel || languageNames.en.settingsToolLabel; const languages = [ { code: 'auto', name: englishLangNames.auto || langNames.auto }, { code: 'en', name: englishLangNames.en || 'English' }, { code: 'fr', name: englishLangNames.fr || 'French' }, { code: 'es', name: englishLangNames.es || 'Spanish' }, { code: 'de', name: englishLangNames.de || 'German' }, { code: 'it', name: englishLangNames.it || 'Italian' }, { code: 'pt', name: englishLangNames.pt || 'Portuguese' }, { code: 'ru', name: englishLangNames.ru || 'Russian' }, { code: 'zh-CN', name: englishLangNames['zh-CN'] || 'Chinese (Simplified)' }, { code: 'ja', name: englishLangNames.ja || 'Japanese' }, { code: 'navigator', name: englishLangNames.navigator || 'Browser language' } ]; const googleTranslateLanguages = { 'af': 'Afrikaans', 'sq': 'Albanian', 'am': 'Amharic', 'ar': 'Arabic', 'hy': 'Armenian', 'az': 'Azerbaijani', 'eu': 'Basque', 'be': 'Belarusian', 'bn': 'Bengali', 'bs': 'Bosnian', 'bg': 'Bulgarian', 'ca': 'Catalan', 'ceb': 'Cebuano', 'ny': 'Chichewa', 'zh-CN': 'Chinese (Simplified)', 'zh-TW': 'Chinese (Traditional)', 'co': 'Corsican', 'hr': 'Croatian', 'cs': 'Czech', 'da': 'Danish', 'nl': 'Dutch', 'en': 'English', 'eo': 'Esperanto', 'et': 'Estonian', 'tl': 'Filipino', 'fi': 'Finnish', 'fr': 'French', 'gl': 'Galician', 'ka': 'Georgian', 'de': 'German', 'el': 'Greek', 'gu': 'Gujarati', 'ht': 'Haitian Creole', 'ha': 'Hausa', 'haw': 'Hawaiian', 'he': 'Hebrew', 'hi': 'Hindi', 'hmn': 'Hmong', 'hu': 'Hungarian', 'is': 'Icelandic', 'ig': 'Igbo', 'id': 'Indonesian', 'ga': 'Irish', 'it': 'Italian', 'ja': 'Japanese', 'jw': 'Javanese', 'kn': 'Kannada', 'kk': 'Kazakh', 'km': 'Khmer', 'rw': 'Kinyarwanda', 'ko': 'Korean', 'ku': 'Kurdish', 'ky': 'Kyrgyz', 'lo': 'Lao', 'la': 'Latin', 'lv': 'Latvian', 'lt': 'Lithuanian', 'lb': 'Luxembourgish', 'mk': 'Macedonian', 'mg': 'Malagasy', 'ms': 'Malay', 'ml': 'Malayalam', 'mt': 'Maltese', 'mi': 'Maori', 'mr': 'Marathi', 'mn': 'Mongolian', 'my': 'Myanmar', 'ne': 'Nepali', 'no': 'Norwegian', 'or': 'Odia', 'ps': 'Pashto', 'fa': 'Persian', 'pl': 'Polish', 'pt': 'Portuguese', 'pa': 'Punjabi', 'ro': 'Romanian', 'ru': 'Russian', 'sm': 'Samoan', 'gd': 'Scots Gaelic', 'sr': 'Serbian', 'st': 'Sesotho', 'sn': 'Shona', 'sd': 'Sindhi', 'si': 'Sinhala', 'sk': 'Slovak', 'sl': 'Slovenian', 'so': 'Somali', 'es': 'Spanish', 'su': 'Sundanese', 'sw': 'Swahili', 'sv': 'Swedish', 'tg': 'Tajik', 'ta': 'Tamil', 'tt': 'Tatar', 'te': 'Telugu', 'th': 'Thai', 'tr': 'Turkish', 'tk': 'Turkmen', 'uk': 'Ukrainian', 'ur': 'Urdu', 'ug': 'Uyghur', 'uz': 'Uzbek', 'vi': 'Vietnamese', 'cy': 'Welsh', 'xh': 'Xhosa', 'yi': 'Yiddish', 'yo': 'Yoruba', 'zu': 'Zulu' }; const defaultTargetLang = languages.some(lang => lang.code === browserLang && lang.code !== 'auto') ? browserLang : 'en'; const commonFavoriteTargetLangs = ['en', 'fr', 'es', 'de', 'it', 'pt', 'ru', 'zh-CN', 'ja']; const favoriteTargetLangs = ['navigator']; if (googleTranslateLanguages[browserLang] && !favoriteTargetLangs.includes(browserLang)) { favoriteTargetLangs.push(browserLang); } commonFavoriteTargetLangs.forEach(code => { if (!favoriteTargetLangs.includes(code)) { favoriteTargetLangs.push(code); } }); const sortedGoogleLanguageEntries = Object.entries(googleTranslateLanguages) .sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB)); function getLanguageLabel(code) { if (code === 'auto') { return langNames.auto || englishLangNames.auto || 'Detect language'; } if (code === 'navigator') { return englishLangNames.navigator || 'Browser language'; } return englishLangNames[code] || googleTranslateLanguages[code] || code; } function buildTargetLanguageOptions(includeNavigator = false) { const favorites = favoriteTargetLangs .filter(code => code === 'navigator' ? includeNavigator : googleTranslateLanguages[code]) .map(code => { const optionValue = code === 'navigator' ? 'navigator' : code; return ``; }) .join(''); const favoriteCodes = new Set(favoriteTargetLangs.filter(code => code !== 'navigator')); const others = sortedGoogleLanguageEntries .filter(([code]) => !favoriteCodes.has(code)) .map(([code, name]) => ``) .join(''); const parts = []; if (favorites) { parts.push(favorites); } if (others) { if (favorites) { parts.push(''); } parts.push(others); } return parts.join(''); } function getToolLanguageLabel(code) { if (code === 'browser') { return englishLangNames.navigator || 'Browser language'; } return englishLangNames[code] || languageNames.en[code] || code; } function buildToolLanguageOptionsHtml() { return ['browser', ...supportedUiLanguages] .map(code => ``) .join(''); } function buildSourceLanguageOptionsHtml() { const entries = Object.entries(googleTranslateLanguages) .sort(([, a], [, b]) => a.localeCompare(b)); const options = entries .map(([code, name]) => ``) .join(''); return `${options}`; } const toolLanguageOptionsHtml = buildToolLanguageOptionsHtml(); let sourceLanguageOptionsHtml = buildSourceLanguageOptionsHtml(); const targetLanguageOptionsHtml = buildTargetLanguageOptions(true); const translationBox = document.createElement('div'); translationBox.id = 'utstTranslationBox'; translationBox.style.cssText = ` all: initial; position: absolute; background: linear-gradient(135deg, #1e1e2f 0%, #2a2a4a 100%); color: #ffffff; padding: 20px; padding-top: 40px; border-radius: 12px; z-index: 9999; display: none; min-width: 370px; max-width: 420px; min-height: 200px; max-height: 260px; overflow-y: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.3s ease; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); box-sizing: border-box; line-height: 1.35; direction: ltr; text-align: left; `; document.documentElement.appendChild(translationBox); translationBox.innerHTML = `
${dragHandleLabel}
`; translationBox.classList.add("utst-scroll"); const fullscreenOverlay = document.createElement('div'); fullscreenOverlay.id = 'fullscreenOverlay'; fullscreenOverlay.style.cssText = ` all: initial; position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(8px); z-index: 10001; padding: 18px; box-sizing: border-box; `; fullscreenOverlay.innerHTML = `
${overlayLabels.title}
`; fullscreenOverlay.classList.add("utst-scroll"); document.documentElement.appendChild(fullscreenOverlay); const selectionBubble = document.createElement('div'); selectionBubble.id = 'utstSelectionBubble'; selectionBubble.innerHTML = `
`; document.documentElement.appendChild(selectionBubble); const selectionBubbleClose = selectionBubble.querySelector('#utstSelectionBubbleClose'); const selectionBubbleAction = selectionBubble.querySelector('#utstSelectionBubbleAction'); const bubbleCloseMenu = selectionBubble.querySelector('#utstBubbleCloseMenu'); const bubbleHideSiteButton = selectionBubble.querySelector('#utstBubbleHideSite'); const bubbleHideGlobalButton = selectionBubble.querySelector('#utstBubbleHideGlobal'); const BOX_W = 420; const BOX_H = 260; const MARGIN = 10; function placeBoxAtSelection(fallbackPosition) { const sel = window.getSelection(); if (!sel || !sel.rangeCount) { if (fallbackPosition && Number.isFinite(fallbackPosition.x) && Number.isFinite(fallbackPosition.y)) { const { left, top } = clampBoxPosition(fallbackPosition.x, fallbackPosition.y + MARGIN); translationBox.style.left = `${left}px`; translationBox.style.top = `${top}px`; } return; } const rect = sel.getRangeAt(0).getBoundingClientRect(); const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || 0; let left = rect.left + scrollX; const topBelow = rect.bottom + scrollY + MARGIN; const topAbove = rect.top + scrollY - BOX_H - MARGIN; const vpLeft = scrollX + MARGIN; const vpRight = scrollX + window.innerWidth - MARGIN; const vpBottom = scrollY + window.innerHeight - MARGIN; if (left + BOX_W > vpRight) left = vpRight - BOX_W; if (left < vpLeft) left = vpLeft; let top; if (topBelow + BOX_H <= vpBottom) { top = topBelow; } else { top = Math.max(topAbove, scrollY + MARGIN); } translationBox.style.left = `${left}px`; translationBox.style.top = `${top}px`; } const dragHandle = translationBox.querySelector('#dragHandle'); let isDragging = false; let dragStartMouseX = 0; let dragStartMouseY = 0; let dragStartLeft = 0; let dragStartTop = 0; let previousUserSelect = ''; function clampBoxPosition(left, top) { const width = translationBox.offsetWidth || BOX_W; const height = translationBox.offsetHeight || BOX_H; const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || 0; const minLeft = scrollX + MARGIN; const maxLeft = scrollX + window.innerWidth - width - MARGIN; const minTop = scrollY + MARGIN; const maxTop = scrollY + window.innerHeight - height - MARGIN; return { left: Math.min(Math.max(minLeft, left), maxLeft), top: Math.min(Math.max(minTop, top), maxTop) }; } window.addEventListener('resize', () => { if (translationBox.style.display === 'block') placeBoxAtSelection(); }); if (dragHandle) { dragHandle.addEventListener('mousedown', (e) => { isDragging = true; const rect = translationBox.getBoundingClientRect(); const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || 0; dragStartMouseX = e.clientX; dragStartMouseY = e.clientY; dragStartLeft = parseFloat(translationBox.style.left) || rect.left + scrollX; dragStartTop = parseFloat(translationBox.style.top) || rect.top + scrollY; previousUserSelect = document.body.style.userSelect; document.body.style.userSelect = 'none'; }); } document.addEventListener('mousemove', (e) => { if (fullscreenTextareaResizePending && fullscreenOverlay.style.display === 'flex' && fullscreenTextareaResizeActive) { if (!fullscreenTextareaResizeRaf) { fullscreenTextareaResizeRaf = requestAnimationFrame(() => { fullscreenTextareaResizeRaf = 0; if (!fullscreenTextareaResizePending || !fullscreenTextareaResizeActive) return; const liveHeight = Math.round(fullscreenTextareaResizeActive.getBoundingClientRect().height || 0); if (liveHeight > 0 && Math.abs(liveHeight - fullscreenTextareaLastSyncedHeight) >= 1) { syncFullscreenTextareaHeights(liveHeight); fullscreenTextareaLastSyncedHeight = liveHeight; } }); } } if (!isDragging) return; const newLeft = dragStartLeft + (e.clientX - dragStartMouseX); const newTop = dragStartTop + (e.clientY - dragStartMouseY); const { left, top } = clampBoxPosition(newLeft, newTop); translationBox.style.left = `${left}px`; translationBox.style.top = `${top}px`; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; document.body.style.userSelect = previousUserSelect; }); const sourceLangSelect = translationBox.querySelector('#sourceLang'); const targetLangSelect = translationBox.querySelector('#targetLang'); const translationText = translationBox.querySelector('#translationText'); const panelLoadingOverlay = translationBox.querySelector('#utstPanelLoading'); const panelLoadingTitle = translationBox.querySelector('#utstPanelLoadingTitle'); const speakButton = translationBox.querySelector('#speakButton'); const speakTooltip = translationBox.querySelector('#speakTooltip'); const speakTranslated = translationBox.querySelector('#speakTranslated'); const speakOriginal = translationBox.querySelector('#speakOriginal'); const copyButton = translationBox.querySelector('#copyButton'); const settingsButton = translationBox.querySelector('#settingsButton'); const backButton = translationBox.querySelector('#backButton'); const defaultTranslateLangSelect = translationBox.querySelector('#defaultTranslateLang'); const toolLanguageSelect = translationBox.querySelector('#toolLanguage'); const panelThemeSelect = translationBox.querySelector('#panelTheme'); const panelThemeTrigger = translationBox.querySelector('#panelThemeTrigger'); const panelThemeCurrent = translationBox.querySelector('#panelThemeCurrent'); const panelThemePanel = translationBox.querySelector('#panelThemePanel'); const panelThemeGrid = translationBox.querySelector('#panelThemeGrid'); const selectionBubbleEnabledCheckbox = translationBox.querySelector('#selectionBubbleEnabled'); const bubbleBlacklistInput = translationBox.querySelector('#bubbleBlacklistInput'); const bubbleBlacklistAddButton = translationBox.querySelector('#bubbleBlacklistAdd'); const bubbleBlacklistList = translationBox.querySelector('#bubbleBlacklistList'); const defaultTranslateLangLabel = translationBox.querySelector('label[for="defaultTranslateLang"]'); const toolLanguageLabel = translationBox.querySelector('label[for="toolLanguage"]'); const panelThemeLabel = translationBox.querySelector('label[for="panelTheme"]'); const bubbleToggleLabel = translationBox.querySelector('label[for="selectionBubbleEnabled"] span'); const bubbleBlacklistLabel = translationBox.querySelector('label[for="bubbleBlacklistInput"]'); const sourceAutoOption = sourceLangSelect.querySelector('option[value="auto"]'); const settingsHeader = translationBox.querySelector('#settingsHeader'); const settingsHeaderTitle = translationBox.querySelector('#settingsHeaderTitle'); const fullscreenTitleEl = fullscreenOverlay.querySelector('#fullscreenTitle'); const fullscreenClose = fullscreenOverlay.querySelector('#fullscreenClose'); const fullscreenSourceLangSelect = fullscreenOverlay.querySelector('#fullscreenSourceLang'); const fullscreenTargetLangSelect = fullscreenOverlay.querySelector('#fullscreenTargetLang'); const fullscreenSourceLangCurrent = fullscreenOverlay.querySelector('#fullscreenSourceLangCurrent'); const fullscreenTargetLangCurrent = fullscreenOverlay.querySelector('#fullscreenTargetLangCurrent'); const fullscreenSourceLangSearch = fullscreenOverlay.querySelector('#fullscreenSourceLangSearch'); const fullscreenTargetLangSearch = fullscreenOverlay.querySelector('#fullscreenTargetLangSearch'); const fullscreenSourceLangGrid = fullscreenOverlay.querySelector('#fullscreenSourceLangGrid'); const fullscreenTargetLangGrid = fullscreenOverlay.querySelector('#fullscreenTargetLangGrid'); const fullscreenSourceLangPanel = fullscreenOverlay.querySelector('#fullscreenSourceLangPanel'); const fullscreenTargetLangPanel = fullscreenOverlay.querySelector('#fullscreenTargetLangPanel'); const fullscreenSourceLangTrigger = fullscreenOverlay.querySelector('#fullscreenSourceLangTrigger'); const fullscreenTargetLangTrigger = fullscreenOverlay.querySelector('#fullscreenTargetLangTrigger'); const fullscreenSourceLabel = fullscreenOverlay.querySelector('#fullscreenSourceLabel'); const fullscreenTargetLabel = fullscreenOverlay.querySelector('#fullscreenTargetLabel'); const fullscreenSwap = fullscreenOverlay.querySelector('#fullscreenSwap'); const fullscreenSource = fullscreenOverlay.querySelector('#fullscreenSource'); const fullscreenTarget = fullscreenOverlay.querySelector('#fullscreenTarget'); const fullscreenSourceWrap = fullscreenOverlay.querySelector('#fullscreenSourceWrap'); const fullscreenTargetWrap = fullscreenOverlay.querySelector('#fullscreenTargetWrap'); const fullscreenLoadingOverlay = fullscreenOverlay.querySelector('#utstFullscreenLoading'); const fullscreenLoadingTitle = fullscreenOverlay.querySelector('#utstFullscreenLoadingTitle'); const fullscreenSourceCopy = fullscreenOverlay.querySelector('#fullscreenSourceCopy'); const fullscreenSourceSpeak = fullscreenOverlay.querySelector('#fullscreenSourceSpeak'); const fullscreenTargetCopy = fullscreenOverlay.querySelector('#fullscreenTargetCopy'); const fullscreenTargetSpeak = fullscreenOverlay.querySelector('#fullscreenTargetSpeak'); const fullscreenToggle = translationBox.querySelector('#fullscreenToggle'); let fullscreenTextareaResizePending = false; let fullscreenTextareaResizeStartHeight = 0; let fullscreenTextareaResizeActive = null; let fullscreenTextareaResizeRaf = 0; let fullscreenTextareaLastSyncedHeight = 0; function getFullscreenTextareaBounds() { const minHeight = 200; const maxByViewport = Math.floor(window.innerHeight * 0.62); const maxHeight = Math.max(minHeight, Math.min(560, maxByViewport)); return { minHeight, maxHeight }; } function syncFullscreenTextareaHeights(preferredHeight = null) { if (!fullscreenSource || !fullscreenTarget) return; const { minHeight, maxHeight } = getFullscreenTextareaBounds(); const sourceHeight = Math.round(fullscreenSource.getBoundingClientRect().height || minHeight); const targetHeight = Math.round(fullscreenTarget.getBoundingClientRect().height || minHeight); const rawHeight = Number.isFinite(preferredHeight) && preferredHeight > 0 ? preferredHeight : Math.max(sourceHeight, targetHeight, minHeight); const clampedHeight = Math.max(minHeight, Math.min(maxHeight, Math.round(rawHeight))); fullscreenSource.style.minHeight = `${minHeight}px`; fullscreenTarget.style.minHeight = `${minHeight}px`; fullscreenSource.style.maxHeight = `${maxHeight}px`; fullscreenTarget.style.maxHeight = `${maxHeight}px`; fullscreenSource.style.height = `${clampedHeight}px`; fullscreenTarget.style.height = `${clampedHeight}px`; if (fullscreenSourceWrap) { fullscreenSourceWrap.style.height = `${clampedHeight}px`; fullscreenSourceWrap.style.minHeight = `${minHeight}px`; fullscreenSourceWrap.style.maxHeight = `${maxHeight}px`; } if (fullscreenTargetWrap) { fullscreenTargetWrap.style.height = `${clampedHeight}px`; fullscreenTargetWrap.style.minHeight = `${minHeight}px`; fullscreenTargetWrap.style.maxHeight = `${maxHeight}px`; } } sourceLangSelect.value = 'auto'; const inlineLanguagePanels = []; let fullscreenSwapRotation = 0; let currentSelectedText = ''; let currentTranslatedText = ''; let detectedSourceLang = 'auto'; let currentResolvedTargetLang = browserLang; let fullscreenTranslateTimer = null; let fullscreenTranslateReason = 'translate'; let selectionBubbleUpdateTimer = null; let bubbleSelectedText = ''; let bubbleSelectionPosition = null; let isSelectingPointer = false; let panelTranslateRequestId = 0; let fullscreenTranslateRequestId = 0; let fullscreenScrollLocked = false; let fullscreenScrollTop = 0; let prevHtmlOverflow = ''; let prevHtmlOverscrollBehavior = ''; let prevBodyOverflow = ''; let prevBodyPosition = ''; let prevBodyTop = ''; let prevBodyLeft = ''; let prevBodyWidth = ''; let prevBodyOverscrollBehavior = ''; let prevBodyTouchAction = ''; const BUBBLE_ENABLED_KEY = 'selectionBubbleEnabled'; const BUBBLE_BLACKLIST_KEY = 'selectionBubbleBlacklist'; const PANEL_THEME_KEY = 'panelTheme'; const currentSiteHost = normalizeHostname(window.location.hostname || window.location.host || ''); let selectionBubbleEnabled = GM_getValue(BUBBLE_ENABLED_KEY, true) !== false; let selectionBubbleBlacklist = loadBubbleBlacklist(); let currentPanelTheme = normalizePanelTheme(GM_getValue(PANEL_THEME_KEY, 'blue')); function normalizePanelTheme(value) { return value === 'dark' || value === 'light' ? value : 'blue'; } function getIconDefaultStrokeColor() { return currentPanelTheme === 'light' ? '#4a5568' : '#ffffff'; } const COPY_FEEDBACK_STROKE = 'rgb(64 130 243)'; function lockPageScrollForFullscreen() { if (fullscreenScrollLocked) return; const scrollY = window.scrollY || window.pageYOffset || 0; fullscreenScrollTop = scrollY; prevHtmlOverflow = document.documentElement.style.overflow; prevHtmlOverscrollBehavior = document.documentElement.style.overscrollBehavior; prevBodyOverflow = document.body.style.overflow; prevBodyPosition = document.body.style.position; prevBodyTop = document.body.style.top; prevBodyLeft = document.body.style.left; prevBodyWidth = document.body.style.width; prevBodyOverscrollBehavior = document.body.style.overscrollBehavior; prevBodyTouchAction = document.body.style.touchAction; document.documentElement.style.overflow = 'hidden'; document.documentElement.style.overscrollBehavior = 'none'; document.body.style.overflow = 'hidden'; document.body.style.position = 'fixed'; document.body.style.top = `-${scrollY}px`; document.body.style.left = '0'; document.body.style.width = '100%'; document.body.style.overscrollBehavior = 'none'; document.body.style.touchAction = 'none'; fullscreenScrollLocked = true; } function unlockPageScrollForFullscreen() { if (!fullscreenScrollLocked) return; document.documentElement.style.overflow = prevHtmlOverflow; document.documentElement.style.overscrollBehavior = prevHtmlOverscrollBehavior; document.body.style.overflow = prevBodyOverflow; document.body.style.position = prevBodyPosition; document.body.style.top = prevBodyTop; document.body.style.left = prevBodyLeft; document.body.style.width = prevBodyWidth; document.body.style.overscrollBehavior = prevBodyOverscrollBehavior; document.body.style.touchAction = prevBodyTouchAction; window.scrollTo(0, fullscreenScrollTop); fullscreenScrollLocked = false; } function resolveTargetLanguageValue(value, fallback = defaultTargetLang) { let lang = value || fallback; if (lang === 'navigator') return browserLang; if (!lang || lang === 'auto') lang = fallback; if (lang === 'navigator') return browserLang; return lang || browserLang; } function resolveSourceSpeechLanguage(sourceValue) { if (sourceValue && sourceValue !== 'auto') return sourceValue; if (detectedSourceLang && detectedSourceLang !== 'auto') return detectedSourceLang; if (sourceLangSelect && sourceLangSelect.value && sourceLangSelect.value !== 'auto') return sourceLangSelect.value; return browserLang; } function resolveTargetSpeechLanguage(targetValue, fallback = currentResolvedTargetLang) { return resolveTargetLanguageValue(targetValue, fallback || browserLang); } function getDetectedSourceLanguageLabel() { if (!detectedSourceLang || detectedSourceLang === 'auto') return ''; return getLanguageLabel(detectedSourceLang); } function getFullscreenSourceCurrentLabel(code) { if (code === 'auto') { const detectedLabel = getDetectedSourceLanguageLabel(); return detectedLabel ? `${langNames.auto} (${detectedLabel})` : (langNames.auto || 'Detect language'); } return getLanguageLabel(code); } function updateFullscreenSourceCurrentLabel() { if (!fullscreenSourceLangCurrent || !fullscreenSourceLangSelect) return; const sourceCode = fullscreenSourceLangSelect.value || 'auto'; fullscreenSourceLangCurrent.textContent = getFullscreenSourceCurrentLabel(sourceCode); } function updateFullscreenTargetCurrentLabel() { if (!fullscreenTargetLangCurrent || !fullscreenTargetLangSelect) return; const targetCode = fullscreenTargetLangSelect.value || defaultTargetLang; fullscreenTargetLangCurrent.textContent = getLanguageLabel(targetCode); } function ensureFullscreenTargetLanguageValid(preferred) { if (!fullscreenTargetLangSelect) return resolveTargetLanguageValue(preferred, defaultTargetLang); const candidate = resolveTargetLanguageValue(preferred, defaultTargetLang); return ensureSelectValue(fullscreenTargetLangSelect, candidate); } function getLoaderTitleByMode(mode = 'translate') { const translateLabel = (overlayLabels && overlayLabels.translate) || (langNames.overlay && langNames.overlay.translate) || 'Translate'; if (mode === 'language') { return `${translateLabel}...`; } return `${translateLabel}...`; } function syncLoadingTitles() { if (panelLoadingTitle) panelLoadingTitle.textContent = getLoaderTitleByMode(panelLoadingOverlay && panelLoadingOverlay.dataset.mode ? panelLoadingOverlay.dataset.mode : 'translate'); if (fullscreenLoadingTitle) fullscreenLoadingTitle.textContent = getLoaderTitleByMode(fullscreenLoadingOverlay && fullscreenLoadingOverlay.dataset.mode ? fullscreenLoadingOverlay.dataset.mode : 'translate'); } function setLoaderState(loaderEl, titleEl, active, mode = 'translate') { if (!loaderEl) return; loaderEl.dataset.mode = mode === 'language' ? 'language' : 'translate'; loaderEl.classList.toggle('is-active', !!active); loaderEl.setAttribute('aria-hidden', active ? 'false' : 'true'); if (titleEl) { titleEl.textContent = getLoaderTitleByMode(loaderEl.dataset.mode); } } function setPanelLoading(active, mode = 'translate') { setLoaderState(panelLoadingOverlay, panelLoadingTitle, active, mode); } function setFullscreenLoading(active, mode = 'translate') { setLoaderState(fullscreenLoadingOverlay, fullscreenLoadingTitle, active, mode); } function runPanelTranslation(text, sourceLang, targetLang, callback, position, loadingMode = 'translate') { const requestId = ++panelTranslateRequestId; setPanelLoading(true, loadingMode); translateText(text, sourceLang, targetLang, (translation, pos, resolvedTargetLang) => { if (requestId !== panelTranslateRequestId) return; setPanelLoading(false, loadingMode); callback(translation, pos, resolvedTargetLang); }, position); } function applyPanelTheme(theme, { persist = false } = {}) { const normalizedTheme = normalizePanelTheme(theme); currentPanelTheme = normalizedTheme; if (persist) { GM_setValue(PANEL_THEME_KEY, normalizedTheme); } document.documentElement.classList.remove('utst-theme-blue', 'utst-theme-dark', 'utst-theme-light'); document.documentElement.classList.add(`utst-theme-${normalizedTheme}`); if (panelThemeSelect) { panelThemeSelect.value = normalizedTheme; } updateThemePickerCurrentLabel(); refreshLanguagePanelTheme(); } function getThemeDisplayLabel(themeValue) { const normalized = normalizePanelTheme(themeValue); const localizedThemes = langNames.themes || languageNames.en.themes || {}; return localizedThemes[normalized] || normalized; } function updateThemePickerCurrentLabel() { if (!panelThemeCurrent) return; const selected = panelThemeSelect ? normalizePanelTheme(panelThemeSelect.value || currentPanelTheme) : currentPanelTheme; panelThemeCurrent.textContent = getThemeDisplayLabel(selected); } function renderThemePickerOptions() { if (!panelThemeGrid || !panelThemeSelect || !panelThemePanel) return; const style = getLanguagePanelThemeStyles(); applyLanguagePanelContainerTheme(panelThemePanel, null); const selected = normalizePanelTheme(panelThemeSelect.value || currentPanelTheme); const isLightTheme = currentPanelTheme === 'light'; const options = ['blue', 'dark', 'light']; panelThemeGrid.innerHTML = options.map((value) => { const active = value === selected; const activeBorder = isLightTheme ? '#2d5cbe' : style.buttonActiveBorder; const idleBorder = isLightTheme ? '#94a3b8' : style.buttonBorder; const activeShadow = isLightTheme ? 'inset 0 0 0 1px rgba(38,61,104,0.28), 0 0 0 1px rgba(38,61,104,0.18)' : (style.buttonActiveShadow || 'none'); return ``; }).join(''); panelThemeGrid.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => { const theme = btn.getAttribute('data-theme') || 'blue'; panelThemeSelect.value = normalizePanelTheme(theme); applyPanelTheme(panelThemeSelect.value, { persist: true }); if (panelThemePanel) panelThemePanel.style.display = 'none'; }); }); } function getLanguagePanelThemeStyles() { if (currentPanelTheme === 'light') { return { panelBg: 'rgba(255,255,255,0.98)', panelBorder: 'rgba(36,58,99,0.18)', panelShadow: '0 10px 24px rgba(18,27,44,0.18)', searchBg: 'rgba(255,255,255,0.96)', searchBorder: 'rgba(38,61,104,0.2)', searchColor: '#203150', buttonBg: 'rgba(45,92,190,0.06)', buttonBorder: 'rgba(38,61,104,0.2)', buttonColor: '#203150', buttonActiveBg: 'rgba(45,92,190,0.22)', buttonActiveBorder: 'rgba(38,61,104,0.5)', buttonActiveColor: '#16386c', buttonWeight: 500, buttonActiveWeight: 650, buttonActiveShadow: 'inset 0 0 0 1px rgba(38,61,104,0.12)' }; } if (currentPanelTheme === 'dark') { return { panelBg: 'rgba(18,18,18,0.98)', panelBorder: 'rgba(255,255,255,0.08)', panelShadow: '0 10px 24px rgba(0,0,0,0.45)', searchBg: 'rgba(255,255,255,0.06)', searchBorder: 'rgba(255,255,255,0.12)', searchColor: '#f0f0f0', buttonBg: 'rgba(255,255,255,0.03)', buttonBorder: 'rgba(255,255,255,0.1)', buttonColor: '#f0f0f0', buttonActiveBg: 'rgba(255,255,255,0.14)', buttonActiveBorder: 'rgba(255,255,255,0.32)', buttonWeight: 500, buttonActiveWeight: 600 }; } return { panelBg: 'rgba(20,36,64,0.98)', panelBorder: 'rgba(139,177,255,0.34)', panelShadow: '0 10px 24px rgba(6,15,35,0.48)', searchBg: 'rgba(120,165,255,0.12)', searchBorder: 'rgba(139,177,255,0.34)', searchColor: '#e9f1ff', buttonBg: 'rgba(120,165,255,0.11)', buttonBorder: 'rgba(139,177,255,0.28)', buttonColor: '#e9f1ff', buttonActiveBg: 'rgba(120,165,255,0.28)', buttonActiveBorder: 'rgba(173,201,255,0.62)', buttonWeight: 500, buttonActiveWeight: 600 }; } function applyLanguagePanelContainerTheme(panelEl, searchEl) { if (!panelEl) return; const style = getLanguagePanelThemeStyles(); panelEl.style.background = style.panelBg; panelEl.style.border = `1px solid ${style.panelBorder}`; panelEl.style.boxShadow = style.panelShadow; if (searchEl) { searchEl.style.background = style.searchBg; searchEl.style.border = `1px solid ${style.searchBorder}`; searchEl.style.color = style.searchColor; } } function renderInlineLanguageGridForTheme(panel, selectEl) { if (!panel || !selectEl) return; const searchEl = panel.querySelector('.inlineLangSearch'); const gridEl = panel.querySelector('.inlineLangGrid'); if (!gridEl) return; const shouldRender = panel.style.display === 'block' || gridEl.childElementCount > 0; if (!shouldRender) return; const opts = Array.from(selectEl.options) .filter(o => !o.disabled) .map(o => ({ value: o.value, label: o.textContent || o.value })); renderLanguageGrid(gridEl, searchEl, selectEl, null, panel, opts); } function refreshLanguagePanelTheme() { applyLanguagePanelContainerTheme(fullscreenSourceLangPanel, fullscreenSourceLangSearch); applyLanguagePanelContainerTheme(fullscreenTargetLangPanel, fullscreenTargetLangSearch); applyLanguagePanelContainerTheme(panelThemePanel, null); if (fullscreenSourceLangGrid && fullscreenSourceLangSelect && (fullscreenSourceLangPanel.style.display === 'block' || fullscreenSourceLangGrid.childElementCount > 0)) { renderLanguageGrid(fullscreenSourceLangGrid, fullscreenSourceLangSearch, fullscreenSourceLangSelect, fullscreenSourceLangCurrent, fullscreenSourceLangPanel); } if (fullscreenTargetLangGrid && fullscreenTargetLangSelect && (fullscreenTargetLangPanel.style.display === 'block' || fullscreenTargetLangGrid.childElementCount > 0)) { renderLanguageGrid(fullscreenTargetLangGrid, fullscreenTargetLangSearch, fullscreenTargetLangSelect, fullscreenTargetLangCurrent, fullscreenTargetLangPanel); } if (panelThemePanel && panelThemeGrid && (panelThemePanel.style.display === 'block' || panelThemeGrid.childElementCount > 0)) { renderThemePickerOptions(); } inlineLanguagePanels.forEach(({ panel }) => { const searchEl = panel.querySelector('.inlineLangSearch'); applyLanguagePanelContainerTheme(panel, searchEl); }); inlineLanguagePanels.forEach(({ panel, selectEl }) => { renderInlineLanguageGridForTheme(panel, selectEl); }); } function normalizeHostname(value) { if (value == null) return ''; let host = String(value).trim().toLowerCase(); if (!host) return ''; host = host.replace(/^\*\./, ''); if (host.includes('://')) { try { host = new URL(host).hostname.toLowerCase(); } catch (e) { host = host.split('://').pop(); } } host = host.split('/')[0].split('?')[0].split('#')[0].split(':')[0]; host = host.replace(/^www\./, ''); return host; } function loadBubbleBlacklist() { const stored = GM_getValue(BUBBLE_BLACKLIST_KEY, []); const list = Array.isArray(stored) ? stored : typeof stored === 'string' ? stored.split(',').map(v => v.trim()) : []; const normalized = [...new Set(list.map(normalizeHostname).filter(Boolean))]; GM_setValue(BUBBLE_BLACKLIST_KEY, normalized); return normalized; } function persistBubbleBlacklist() { GM_setValue(BUBBLE_BLACKLIST_KEY, selectionBubbleBlacklist); } function persistSelectionBubbleEnabled() { GM_setValue(BUBBLE_ENABLED_KEY, selectionBubbleEnabled); } function isCurrentSiteBlacklisted() { if (!currentSiteHost) return false; return selectionBubbleBlacklist.some(site => currentSiteHost === site || currentSiteHost.endsWith(`.${site}`)); } function canShowSelectionBubble() { return selectionBubbleEnabled && !isCurrentSiteBlacklisted(); } function getSelectionContext() { const sel = window.getSelection(); if (!sel || !sel.rangeCount || sel.isCollapsed) return null; const text = sel.toString().trim(); if (!text) return null; const range = sel.getRangeAt(0); let rect = null; try { if (sel.focusNode) { const focusRange = document.createRange(); focusRange.setStart(sel.focusNode, sel.focusOffset); focusRange.setEnd(sel.focusNode, sel.focusOffset); const focusRect = focusRange.getBoundingClientRect(); if (focusRect && (focusRect.width || focusRect.height)) { rect = focusRect; } } } catch (e) { rect = null; } if (!rect) { const clientRects = range.getClientRects(); if (clientRects && clientRects.length) { rect = clientRects[clientRects.length - 1]; } } if (!rect) { rect = range.getBoundingClientRect(); } if (!rect || (!rect.width && !rect.height)) return null; return { text, rect, position: { x: rect.right + window.scrollX, y: rect.bottom + window.scrollY } }; } function hideBubbleCloseMenu() { if (!bubbleCloseMenu) return; bubbleCloseMenu.classList.remove('utst-open'); } function hideSelectionBubble() { selectionBubble.classList.remove('utst-visible'); bubbleSelectedText = ''; bubbleSelectionPosition = null; hideBubbleCloseMenu(); } function positionSelectionBubble(rect) { if (!rect) return; const bubbleWidth = selectionBubble.offsetWidth || 120; const bubbleHeight = selectionBubble.offsetHeight || 38; const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || 0; const minLeft = scrollX + MARGIN; const maxLeft = scrollX + window.innerWidth - bubbleWidth - MARGIN; const belowTop = rect.bottom + scrollY + 8; const aboveTop = rect.top + scrollY - bubbleHeight - 8; const maxTop = scrollY + window.innerHeight - bubbleHeight - MARGIN; const minTop = scrollY + MARGIN; const anchorLeft = rect.right + scrollX - (bubbleWidth / 2); let top = belowTop; if (top > maxTop) { top = Math.max(minTop, aboveTop); } const left = Math.min(Math.max(anchorLeft, minLeft), maxLeft); selectionBubble.style.left = `${left}px`; selectionBubble.style.top = `${Math.min(Math.max(top, minTop), maxTop)}px`; } function isSelectionInsideTool() { const sel = window.getSelection(); if (!sel) return false; const anchor = sel.anchorNode; const focus = sel.focusNode; const nodes = [anchor, focus].filter(Boolean); return nodes.some(node => { const el = node.nodeType === 1 ? node : node.parentElement; return el && (translationBox.contains(el) || fullscreenOverlay.contains(el) || selectionBubble.contains(el)); }); } function updateSelectionBubble() { if (isSelectingPointer) { hideSelectionBubble(); return; } if (translationBox.style.display === 'block' || fullscreenOverlay.style.display === 'flex') { hideSelectionBubble(); return; } if (!canShowSelectionBubble() || isSelectionInsideTool()) { hideSelectionBubble(); return; } const context = getSelectionContext(); if (!context) { hideSelectionBubble(); return; } bubbleSelectedText = context.text; bubbleSelectionPosition = context.position; positionSelectionBubble(context.rect); selectionBubble.classList.add('utst-visible'); } function scheduleSelectionBubbleUpdate(delay = 20) { if (selectionBubbleUpdateTimer) clearTimeout(selectionBubbleUpdateTimer); selectionBubbleUpdateTimer = setTimeout(() => { selectionBubbleUpdateTimer = null; updateSelectionBubble(); }, delay); } function renderBubbleBlacklist() { if (!bubbleBlacklistList) return; if (!selectionBubbleBlacklist.length) { bubbleBlacklistList.innerHTML = `
${langNames.settingsBlacklistEmpty}
`; return; } bubbleBlacklistList.innerHTML = selectionBubbleBlacklist .map(site => `
${site}
`) .join(''); bubbleBlacklistList.querySelectorAll('.utst-blacklist-remove').forEach(btn => { btn.addEventListener('click', () => { const site = normalizeHostname(btn.getAttribute('data-site') || ''); if (!site) return; selectionBubbleBlacklist = selectionBubbleBlacklist.filter(entry => entry !== site); persistBubbleBlacklist(); renderBubbleBlacklist(); scheduleSelectionBubbleUpdate(0); }); }); } function syncSelectionBubbleSettingsUi() { const bubbleLabels = (langNames && langNames.bubble) || (languageNames.en && languageNames.en.bubble) || {}; const hideOnLabel = bubbleLabels.hideOn || 'Hide on'; const hideSiteLabel = bubbleLabels.hideSite || 'Hide on this site'; const hideGlobalLabel = bubbleLabels.hideGlobal || 'Hide globally'; const closeTitleLabel = bubbleLabels.closeTitle || 'Hide selection bubble'; const translateTitleLabel = bubbleLabels.translateTitle || 'Translate selected text'; if (selectionBubbleEnabledCheckbox) { selectionBubbleEnabledCheckbox.checked = !!selectionBubbleEnabled; } if (selectionBubbleClose) { selectionBubbleClose.title = closeTitleLabel; selectionBubbleClose.setAttribute('aria-label', closeTitleLabel); } if (selectionBubbleAction) { selectionBubbleAction.title = translateTitleLabel; selectionBubbleAction.setAttribute('aria-label', translateTitleLabel); } if (bubbleHideSiteButton) { bubbleHideSiteButton.textContent = currentSiteHost ? `${hideOnLabel} ${currentSiteHost}` : hideSiteLabel; } if (bubbleHideGlobalButton) { bubbleHideGlobalButton.textContent = hideGlobalLabel; } renderBubbleBlacklist(); } function getSelectedText() { return window.getSelection().toString().trim(); } function ensureSelectValue(selectEl, lang) { if (selectEl.querySelector(`option[value="${lang}"]`)) { selectEl.value = lang; return lang; } selectEl.value = defaultTargetLang; return defaultTargetLang; } function getSavedTargetLanguage() { const saved = GM_getValue('defaultTranslateLang', defaultTargetLang); if (!targetLangSelect.querySelector(`option[value="${saved}"]`)) { GM_setValue('defaultTranslateLang', defaultTargetLang); return defaultTargetLang; } return saved; } function persistDefaultTargetLanguage(lang) { const valueToPersist = defaultTranslateLangSelect.querySelector(`option[value="${lang}"]`) ? lang : defaultTargetLang; GM_setValue('defaultTranslateLang', valueToPersist); return valueToPersist; } function applyToolLanguage(preference, { persist = false } = {}) { const normalizedSelection = (preference === 'browser' || supportedUiLanguages.includes(preference)) ? preference : 'browser'; if (persist) { GM_setValue('defaultToolLang', normalizedSelection); } const previousErrors = errors; toolLanguagePreference = normalizedSelection; const newUiLang = resolveUiLang(normalizedSelection); langNames = languageNames[newUiLang]; errors = langNames.errors; tooltips = langNames.tooltips; dragHandleLabel = langNames.dragHandleLabel || languageNames.en.dragHandleLabel; overlayLabels = langNames.overlay || languageNames.en.overlay; settingsTitle = langNames.settingsTitle || languageNames.en.settingsTitle; settingsDefaultLabel = langNames.settingsDefaultLabel || languageNames.en.settingsDefaultLabel; settingsToolLabel = langNames.settingsToolLabel || languageNames.en.settingsToolLabel; const settingsThemeLabel = langNames.settingsThemeLabel || languageNames.en.settingsThemeLabel || 'Theme:'; const settingsBubbleLabel = langNames.settingsBubbleLabel || languageNames.en.settingsBubbleLabel || 'Selection Bubble'; const settingsBlacklistLabel = langNames.settingsBlacklistLabel || languageNames.en.settingsBlacklistLabel || 'Blacklist'; const settingsBlacklistAdd = langNames.settingsBlacklistAdd || languageNames.en.settingsBlacklistAdd || 'Add'; if (settingsHeaderTitle) settingsHeaderTitle.textContent = settingsTitle; if (defaultTranslateLangLabel) defaultTranslateLangLabel.textContent = settingsDefaultLabel; if (toolLanguageLabel) toolLanguageLabel.textContent = settingsToolLabel; if (panelThemeLabel) panelThemeLabel.textContent = settingsThemeLabel; if (bubbleToggleLabel) bubbleToggleLabel.textContent = settingsBubbleLabel; if (bubbleBlacklistLabel) bubbleBlacklistLabel.textContent = settingsBlacklistLabel; if (bubbleBlacklistAddButton) bubbleBlacklistAddButton.textContent = settingsBlacklistAdd; if (settingsButton) settingsButton.title = settingsTitle; if (sourceAutoOption) sourceAutoOption.textContent = langNames.auto; if (speakTranslated) speakTranslated.textContent = tooltips.listenTranslated; if (speakOriginal) speakOriginal.textContent = tooltips.listenOriginal; const dragLabelEl = translationBox.querySelector('#dragHandle span'); if (dragLabelEl) dragLabelEl.textContent = dragHandleLabel; if (fullscreenTitleEl) fullscreenTitleEl.textContent = overlayLabels.title; if (fullscreenSourceLabel) fullscreenSourceLabel.textContent = overlayLabels.source; if (fullscreenTargetLabel) fullscreenTargetLabel.textContent = overlayLabels.target; if (fullscreenToggle) fullscreenToggle.title = overlayLabels.open; if (fullscreenSourceLangSearch) fullscreenSourceLangSearch.placeholder = langNames.navigator; if (fullscreenTargetLangSearch) fullscreenTargetLangSearch.placeholder = langNames.navigator; syncLoadingTitles(); if (fullscreenSourceLangSelect) { const prev = fullscreenSourceLangSelect.value || 'auto'; sourceLanguageOptionsHtml = buildSourceLanguageOptionsHtml(); fullscreenSourceLangSelect.innerHTML = sourceLanguageOptionsHtml; fullscreenSourceLangSelect.value = fullscreenSourceLangSelect.querySelector(`option[value="${prev}"]`) ? prev : 'auto'; } if (fullscreenTargetLangSelect) { const prev = fullscreenTargetLangSelect.value || defaultTargetLang; const refreshedTargetOptionsOverlay = buildTargetLanguageOptions(true); fullscreenTargetLangSelect.innerHTML = refreshedTargetOptionsOverlay; ensureFullscreenTargetLanguageValid(prev); } updateFullscreenSourceCurrentLabel(); updateFullscreenTargetCurrentLabel(); if (toolLanguageSelect) { toolLanguageSelect.innerHTML = buildToolLanguageOptionsHtml(); toolLanguageSelect.value = normalizedSelection; } if (panelThemeSelect) { const blueOption = panelThemeSelect.querySelector('option[value="blue"]'); const darkOption = panelThemeSelect.querySelector('option[value="dark"]'); const lightOption = panelThemeSelect.querySelector('option[value="light"]'); const localizedThemes = langNames.themes || languageNames.en.themes || {}; if (blueOption) blueOption.textContent = localizedThemes.blue || 'Blue'; if (darkOption) darkOption.textContent = localizedThemes.dark || 'Dark'; if (lightOption) lightOption.textContent = localizedThemes.light || 'Light'; } updateThemePickerCurrentLabel(); renderThemePickerOptions(); inlineLanguagePanels.forEach(({ panel }) => { const searchEl = panel.querySelector('.inlineLangSearch'); if (searchEl) searchEl.placeholder = langNames.navigator; }); if (translationText && previousErrors && translationText.textContent === previousErrors.noText) { translationText.textContent = errors.noText; } syncSelectionBubbleSettingsUi(); scheduleSelectionBubbleUpdate(0); const currentTargetValue = targetLangSelect.value; const savedDefaultValue = GM_getValue('defaultTranslateLang', defaultTargetLang); const refreshedTargetOptions = buildTargetLanguageOptions(true); targetLangSelect.innerHTML = refreshedTargetOptions; ensureSelectValue(targetLangSelect, currentTargetValue); defaultTranslateLangSelect.innerHTML = refreshedTargetOptions; ensureSelectValue(defaultTranslateLangSelect, savedDefaultValue); } const initialTargetLang = getSavedTargetLanguage(); ensureSelectValue(targetLangSelect, initialTargetLang); ensureSelectValue(defaultTranslateLangSelect, initialTargetLang); currentResolvedTargetLang = initialTargetLang === 'navigator' ? browserLang : initialTargetLang; if (toolLanguageSelect) { toolLanguageSelect.value = toolLanguagePreference; } defaultTranslateLangSelect.addEventListener('change', () => { stopSpeaking(); const persisted = persistDefaultTargetLanguage(defaultTranslateLangSelect.value); ensureSelectValue(targetLangSelect, persisted); currentResolvedTargetLang = persisted === 'navigator' ? browserLang : persisted; handleLanguageChange(); }); if (toolLanguageSelect) { toolLanguageSelect.addEventListener('change', () => { const selected = toolLanguageSelect.value || 'browser'; const normalizedSelection = (selected === 'browser' || supportedUiLanguages.includes(selected)) ? selected : 'browser'; applyToolLanguage(normalizedSelection, { persist: true }); }); } if (panelThemeSelect) { panelThemeSelect.addEventListener('change', () => { applyPanelTheme(panelThemeSelect.value || 'blue', { persist: true }); }); } if (panelThemeTrigger) { panelThemeTrigger.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const isOpen = panelThemePanel && panelThemePanel.style.display === 'block'; hideInlinePanels(); hideLanguagePanels(); if (!panelThemePanel) return; if (isOpen) { panelThemePanel.style.display = 'none'; return; } renderThemePickerOptions(); panelThemePanel.style.display = 'block'; }); } if (selectionBubbleEnabledCheckbox) { selectionBubbleEnabledCheckbox.addEventListener('change', () => { selectionBubbleEnabled = !!selectionBubbleEnabledCheckbox.checked; persistSelectionBubbleEnabled(); hideSelectionBubble(); scheduleSelectionBubbleUpdate(0); }); } if (bubbleBlacklistAddButton) { const addBlacklistSite = () => { const normalized = normalizeHostname(bubbleBlacklistInput ? bubbleBlacklistInput.value : ''); if (!normalized) return; if (!selectionBubbleBlacklist.includes(normalized)) { selectionBubbleBlacklist.push(normalized); selectionBubbleBlacklist.sort((a, b) => a.localeCompare(b)); persistBubbleBlacklist(); renderBubbleBlacklist(); } if (bubbleBlacklistInput) bubbleBlacklistInput.value = ''; hideSelectionBubble(); scheduleSelectionBubbleUpdate(0); }; bubbleBlacklistAddButton.addEventListener('click', addBlacklistSite); if (bubbleBlacklistInput) { bubbleBlacklistInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addBlacklistSite(); } }); } } selectionBubble.addEventListener('mousedown', (e) => { e.preventDefault(); }); document.addEventListener('mousedown', (e) => { if (e.button !== 0) return; if (selectionBubble.contains(e.target) || translationBox.contains(e.target) || fullscreenOverlay.contains(e.target)) return; isSelectingPointer = true; hideSelectionBubble(); }, true); if (selectionBubbleClose) { selectionBubbleClose.addEventListener('click', (e) => { e.stopPropagation(); if (!bubbleCloseMenu) return; const isOpen = bubbleCloseMenu.classList.contains('utst-open'); bubbleCloseMenu.classList.toggle('utst-open', !isOpen); }); } if (bubbleHideSiteButton) { bubbleHideSiteButton.addEventListener('click', (e) => { e.stopPropagation(); if (currentSiteHost && !selectionBubbleBlacklist.includes(currentSiteHost)) { selectionBubbleBlacklist.push(currentSiteHost); selectionBubbleBlacklist.sort((a, b) => a.localeCompare(b)); persistBubbleBlacklist(); } hideSelectionBubble(); syncSelectionBubbleSettingsUi(); scheduleSelectionBubbleUpdate(0); }); } if (bubbleHideGlobalButton) { bubbleHideGlobalButton.addEventListener('click', (e) => { e.stopPropagation(); selectionBubbleEnabled = false; persistSelectionBubbleEnabled(); syncSelectionBubbleSettingsUi(); hideSelectionBubble(); }); } if (selectionBubbleAction) { selectionBubbleAction.addEventListener('click', (e) => { e.stopPropagation(); const text = (bubbleSelectedText || getSelectedText() || '').trim(); const pos = bubbleSelectionPosition ? { x: bubbleSelectionPosition.x, y: bubbleSelectionPosition.y } : null; openTranslationPanelForText(text, pos); }); } applyPanelTheme(currentPanelTheme); applyToolLanguage(toolLanguagePreference); syncSelectionBubbleSettingsUi(); scheduleSelectionBubbleUpdate(0); function splitSentences(text) { const regex = /(\.\s+|\.\n|\.)/; let parts = text.split(regex); let sentences = []; let currentSentence = ''; for (let i = 0; i < parts.length; i++) { currentSentence += parts[i]; if (parts[i].match(regex) || i === parts.length - 1) { if (currentSentence.trim()) { sentences.push(currentSentence.trim()); } currentSentence = ''; } } return sentences.length ? sentences : [text]; } function translateSentence(text, sourceLang, targetLang, callback) { if (!text.trim()) { callback(text, null); return; } const match = text.match(/([\s\S]*?)(?:(\.\s+|\.\n|\.)|$)/); const textToTranslate = match ? (match[1] || text) : text; const delimiter = match && match[2] ? match[2] : ''; function chunkBySize(s, size = 1000) { const out = []; for (let i = 0; i < s.length; i += size) out.push(s.slice(i, i + size)); return out; } let sentences = splitSentences(text).flatMap(seg => seg.length > 1000 ? chunkBySize(seg) : [seg] ); GM_xmlhttpRequest({ method: 'GET', url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(textToTranslate.trim())}`, onload: function (response) { try { const data = JSON.parse(response.responseText); let detected = sourceLang; if (sourceLang === 'auto') { if (data[2]) { detected = data[2]; } else if (data[8] && data[8][0] && data[8][0][0]) { detected = data[8][0][0]; } else { detected = ''; } } const translation = (data && data[0] && data[0][0] && data[0][0][0]) ? data[0][0][0] + delimiter : '' + delimiter; callback(translation, detected || null); } catch (e) { callback(errors.translation + delimiter, null); } }, onerror: function () { callback(errors.connection + delimiter, null); } }); } function translateText(text, sourceLang, targetLang, callback, position) { if (!text) { callback(errors.noText, position, null); return; } if (!sourceLang || sourceLang === '') sourceLang = 'auto'; let resolvedTargetLang = targetLang; if (resolvedTargetLang === 'navigator') { resolvedTargetLang = browserLang; } if (!resolvedTargetLang || resolvedTargetLang === '') { let fallback = getSavedTargetLanguage(); if (fallback === 'navigator') fallback = browserLang; resolvedTargetLang = fallback || defaultTargetLang; } const sentences = splitSentences(text); let translatedSentences = []; let completed = 0; let runDetectedLang = null; sentences.forEach((sentence, index) => { translateSentence(sentence, sourceLang, resolvedTargetLang, (translation, detected) => { translatedSentences[index] = translation; if (!runDetectedLang && detected && googleTranslateLanguages[detected]) { runDetectedLang = detected; } completed++; if (completed === sentences.length) { if (runDetectedLang && sourceLangSelect.querySelector(`option[value="${runDetectedLang}"]`)) { sourceLangSelect.value = runDetectedLang; detectedSourceLang = runDetectedLang; } else { sourceLangSelect.value = 'auto'; detectedSourceLang = 'auto'; } updateFullscreenSourceCurrentLabel(); const fullTranslation = translatedSentences.join(''); callback(fullTranslation, position, resolvedTargetLang); } }); }); } let currentSpeakerId = null; let speechPlaying = false; let activeSpeechAudio = null; let activeSpeechAudioUrl = null; let speechQueue = []; let speechFetchRequest = null; let speechRequestToken = 0; const SPEECH_ACTIVE_STROKE = COPY_FEEDBACK_STROKE; function setSpeakButtonVisualState(buttonEl, active) { if (!buttonEl) return; const svg = buttonEl.querySelector('svg'); if (svg) { svg.style.stroke = active ? SPEECH_ACTIVE_STROKE : ''; } } function updateSpeechIconState() { const panelActive = speechPlaying && currentSpeakerId && currentSpeakerId.startsWith('panel-'); const sourceActive = speechPlaying && currentSpeakerId === 'fs-source'; const targetActive = speechPlaying && currentSpeakerId === 'fs-target'; setSpeakButtonVisualState(speakButton, panelActive); setSpeakButtonVisualState(fullscreenSourceSpeak, sourceActive); setSpeakButtonVisualState(fullscreenTargetSpeak, targetActive); } function normalizeSpeechLangTag(langTag) { return (langTag || '').toLowerCase().replace(/_/g, '-').trim(); } function clearActiveSpeechAudio() { if (activeSpeechAudio) { activeSpeechAudio.onended = null; activeSpeechAudio.onerror = null; activeSpeechAudio.pause(); activeSpeechAudio.src = ''; activeSpeechAudio = null; } if (activeSpeechAudioUrl) { URL.revokeObjectURL(activeSpeechAudioUrl); activeSpeechAudioUrl = null; } } function stopSpeaking() { speechRequestToken += 1; if (speechFetchRequest && typeof speechFetchRequest.abort === 'function') { speechFetchRequest.abort(); } speechFetchRequest = null; speechQueue = []; clearActiveSpeechAudio(); speechPlaying = false; currentSpeakerId = null; updateSpeechIconState(); } function normalizeGoogleTtsLang(langCode) { let normalized = normalizeSpeechLangTag(langCode); if (!normalized || normalized === 'auto' || normalized === 'navigator') { normalized = normalizeSpeechLangTag(browserLang || 'en'); } if (normalized === 'zh-cn' || normalized === 'zh-sg') return 'zh-CN'; if (normalized === 'zh-tw' || normalized === 'zh-hk') return 'zh-TW'; if (normalized === 'pt-br') return 'pt-BR'; return normalized; } function getGoogleTtsLanguageCandidates(langCode) { const normalized = normalizeGoogleTtsLang(langCode); const candidates = [normalized]; const base = normalized.split('-')[0]; if (base && !candidates.includes(base)) candidates.push(base); return candidates.filter(Boolean); } function splitTextForGoogleTts(text, maxChunkLength = 180) { const normalized = (text || '').replace(/\s+/g, ' ').trim(); if (!normalized) return []; if (normalized.length <= maxChunkLength) return [normalized]; const chunks = []; const sentences = normalized.match(/[^.!?]+[.!?]*/g) || [normalized]; sentences.forEach((sentenceRaw) => { const sentence = sentenceRaw.trim(); if (!sentence) return; if (sentence.length <= maxChunkLength) { chunks.push(sentence); return; } const words = sentence.split(' '); let current = ''; words.forEach((word) => { if (!word) return; if (word.length > maxChunkLength) { if (current) { chunks.push(current); current = ''; } for (let i = 0; i < word.length; i += maxChunkLength) { chunks.push(word.slice(i, i + maxChunkLength)); } return; } const next = current ? `${current} ${word}` : word; if (next.length > maxChunkLength) { if (current) chunks.push(current); current = word; } else { current = next; } }); if (current) chunks.push(current); }); return chunks.filter(Boolean); } function finishSpeechPlayback(requestToken) { if (requestToken !== speechRequestToken) return; speechFetchRequest = null; speechQueue = []; clearActiveSpeechAudio(); speechPlaying = false; currentSpeakerId = null; updateSpeechIconState(); } function fetchGoogleTtsChunk(chunkText, langCandidates, requestToken, done) { if (!chunkText || !langCandidates.length || requestToken !== speechRequestToken) { done(null); return; } const tryCandidate = (index) => { if (requestToken !== speechRequestToken) { done(null); return; } if (index >= langCandidates.length) { done(null); return; } const candidateLang = langCandidates[index]; const url = `https://translate.googleapis.com/translate_tts?client=gtx&ie=UTF-8&tl=${encodeURIComponent(candidateLang)}&q=${encodeURIComponent(chunkText)}`; speechFetchRequest = GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', onload: (response) => { speechFetchRequest = null; if (requestToken !== speechRequestToken) { done(null); return; } const status = Number(response.status) || 0; const hasAudio = response.response && response.response.byteLength > 0; if (status >= 200 && status < 300 && hasAudio) { done(new Blob([response.response], { type: 'audio/mpeg' })); return; } tryCandidate(index + 1); }, onerror: () => { speechFetchRequest = null; if (requestToken !== speechRequestToken) { done(null); return; } tryCandidate(index + 1); } }); }; tryCandidate(0); } function playSpeechChunkAt(index, requestToken, langCandidates) { if (requestToken !== speechRequestToken) return; if (!speechQueue.length || index >= speechQueue.length) { finishSpeechPlayback(requestToken); return; } fetchGoogleTtsChunk(speechQueue[index], langCandidates, requestToken, (audioBlob) => { if (requestToken !== speechRequestToken) return; if (!audioBlob) { finishSpeechPlayback(requestToken); return; } clearActiveSpeechAudio(); activeSpeechAudioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(activeSpeechAudioUrl); activeSpeechAudio = audio; audio.onended = () => { playSpeechChunkAt(index + 1, requestToken, langCandidates); }; audio.onerror = () => { playSpeechChunkAt(index + 1, requestToken, langCandidates); }; const playPromise = audio.play(); if (playPromise && typeof playPromise.catch === 'function') { playPromise.catch(() => { playSpeechChunkAt(index + 1, requestToken, langCandidates); }); } }); } function speak(text, lang, speakerId = null) { const value = (text || '').trim(); if (!value) return; const sameSpeaker = speechPlaying && speakerId && speakerId === currentSpeakerId; if (sameSpeaker) { stopSpeaking(); return; } stopSpeaking(); speechQueue = splitTextForGoogleTts(value); if (!speechQueue.length) return; const requestToken = speechRequestToken; const langCandidates = getGoogleTtsLanguageCandidates(lang); currentSpeakerId = speakerId; speechPlaying = true; updateSpeechIconState(); playSpeechChunkAt(0, requestToken, langCandidates); } function openTranslationPanelForText(selectedText, selectionPosition) { stopSpeaking(); sourceLangSelect.value = 'auto'; detectedSourceLang = 'auto'; const translatorPanel = document.getElementById('translatorPanel'); const settingsPanel = document.getElementById('settingsPanel'); if (translatorPanel) translatorPanel.style.display = 'block'; if (settingsPanel) settingsPanel.style.display = 'none'; if (settingsHeader) settingsHeader.style.display = 'none'; translationBox.classList.remove('utst-settings-open'); hideSelectionBubble(); hideBubbleCloseMenu(); const text = (selectedText || '').trim(); if (!text) { const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || 0; panelTranslateRequestId++; setPanelLoading(false); translationText.textContent = errors.noText; translationBox.style.display = 'block'; translationBox.style.left = `${scrollX + window.innerWidth / 2 - 150}px`; translationBox.style.top = `${scrollY + window.innerHeight / 2 - 50}px`; translationBox.style.opacity = '1'; translationBox.style.transform = 'translateY(0)'; return; } currentSelectedText = text; const savedTargetLang = getSavedTargetLanguage(); const targetLangForSession = ensureSelectValue(targetLangSelect, savedTargetLang); ensureSelectValue(defaultTranslateLangSelect, savedTargetLang); const fallbackPosition = selectionPosition && Number.isFinite(selectionPosition.x) && Number.isFinite(selectionPosition.y) ? selectionPosition : { x: 0, y: 0 }; translationText.textContent = ''; placeBoxAtSelection(fallbackPosition); translationBox.style.display = 'block'; translationBox.style.opacity = '1'; translationBox.style.transform = 'translateY(0)'; runPanelTranslation(text, 'auto', targetLangForSession, (translation, pos, resolvedTargetLang) => { currentTranslatedText = translation; translationText.textContent = translation; currentResolvedTargetLang = resolvedTargetLang || currentResolvedTargetLang; placeBoxAtSelection(pos || fallbackPosition); hideSelectionBubble(); }, fallbackPosition, 'translate'); } document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key.toLowerCase() === 'l' && !e.altKey && !e.metaKey && !e.shiftKey) { e.preventDefault(); const context = getSelectionContext(); openTranslationPanelForText(context ? context.text : '', context ? context.position : null); } }); function handleLanguageChange() { stopSpeaking(); const targetVal = targetLangSelect.value; currentResolvedTargetLang = targetVal === 'navigator' ? browserLang : targetVal; const sourceVal = sourceLangSelect.value; if (sourceVal !== 'auto') { detectedSourceLang = sourceVal; } if (currentSelectedText) { runPanelTranslation(currentSelectedText, sourceVal, targetVal, (translation, pos, resolvedTargetLang) => { currentTranslatedText = translation; translationText.textContent = translation; currentResolvedTargetLang = resolvedTargetLang || currentResolvedTargetLang; }, { x: parseFloat(translationBox.style.left), y: parseFloat(translationBox.style.top) }, 'language'); } } sourceLangSelect.addEventListener('change', handleLanguageChange); targetLangSelect.addEventListener('change', () => { ensureSelectValue(targetLangSelect, targetLangSelect.value); handleLanguageChange(); }); speakButton.addEventListener('mouseenter', () => { speakTooltip.style.display = 'block'; }); speakButton.addEventListener('mouseleave', () => { speakTooltip.style.display = 'none'; }); speakButton.addEventListener('click', (e) => { if (speakTooltip && speakTooltip.contains(e.target)) return; if (speechPlaying && currentSpeakerId && currentSpeakerId.startsWith('panel-')) { e.preventDefault(); e.stopPropagation(); stopSpeaking(); speakTooltip.style.display = 'none'; } }); speakTranslated.addEventListener('click', (e) => { e.stopPropagation(); if (currentTranslatedText) { const langForSpeech = resolveTargetSpeechLanguage(targetLangSelect ? targetLangSelect.value : currentResolvedTargetLang, currentResolvedTargetLang); speak(currentTranslatedText, langForSpeech, 'panel-translated'); } }); speakOriginal.addEventListener('click', (e) => { e.stopPropagation(); if (currentSelectedText) { speak(currentSelectedText, resolveSourceSpeechLanguage(sourceLangSelect ? sourceLangSelect.value : 'auto'), 'panel-original'); } }); copyButton.addEventListener('click', () => { if (currentTranslatedText) { navigator.clipboard.writeText(currentTranslatedText); copyButton.querySelector('svg').style.stroke = COPY_FEEDBACK_STROKE; setTimeout(() => { copyButton.querySelector('svg').style.stroke = getIconDefaultStrokeColor(); }, 1000); } }); function openFullscreenOverlay() { hideSelectionBubble(); hideBubbleCloseMenu(); lockPageScrollForFullscreen(); fullscreenOverlay.style.display = 'flex'; fullscreenSource.value = currentSelectedText || ''; fullscreenTarget.value = currentTranslatedText || ''; if (fullscreenSourceLangSelect) { const srcVal = sourceLangSelect ? sourceLangSelect.value : 'auto'; fullscreenSourceLangSelect.value = fullscreenSourceLangSelect.querySelector(`option[value="${srcVal}"]`) ? srcVal : 'auto'; } if (fullscreenTargetLangSelect) { const tgtVal = targetLangSelect ? targetLangSelect.value : defaultTargetLang; ensureFullscreenTargetLanguageValid(tgtVal); } updateFullscreenSourceCurrentLabel(); updateFullscreenTargetCurrentLabel(); hideLanguagePanels(); refreshLanguagePanelTheme(); renderLanguageGrid(fullscreenSourceLangGrid, fullscreenSourceLangSearch, fullscreenSourceLangSelect, fullscreenSourceLangCurrent, fullscreenSourceLangPanel); renderLanguageGrid(fullscreenTargetLangGrid, fullscreenTargetLangSearch, fullscreenTargetLangSelect, fullscreenTargetLangCurrent, fullscreenTargetLangPanel); syncFullscreenTextareaHeights(); scheduleFullscreenTranslate(0, 'translate'); } function closeFullscreenOverlay() { fullscreenTranslateRequestId++; setFullscreenLoading(false); fullscreenTextareaResizePending = false; fullscreenTextareaResizeActive = null; fullscreenTextareaResizeStartHeight = 0; fullscreenTextareaLastSyncedHeight = 0; if (fullscreenTextareaResizeRaf) { cancelAnimationFrame(fullscreenTextareaResizeRaf); fullscreenTextareaResizeRaf = 0; } fullscreenOverlay.style.display = 'none'; unlockPageScrollForFullscreen(); stopSpeaking(); scheduleSelectionBubbleUpdate(0); } function translateInFullscreen(reason = 'translate') { const text = fullscreenSource.value.trim(); const target = fullscreenTargetLangSelect ? ensureFullscreenTargetLanguageValid(fullscreenTargetLangSelect.value) : (targetLangSelect ? targetLangSelect.value : defaultTargetLang); const srcLang = fullscreenSourceLangSelect ? fullscreenSourceLangSelect.value || 'auto' : 'auto'; const requestId = ++fullscreenTranslateRequestId; setFullscreenLoading(true, reason === 'language' ? 'language' : 'translate'); translateText(text, srcLang, target, (translation, pos, resolvedTargetLang) => { if (requestId !== fullscreenTranslateRequestId) return; setFullscreenLoading(false, reason === 'language' ? 'language' : 'translate'); fullscreenTarget.value = translation; currentResolvedTargetLang = resolvedTargetLang || currentResolvedTargetLang; updateFullscreenSourceCurrentLabel(); updateFullscreenTargetCurrentLabel(); }, { x: 0, y: 0 }); } function scheduleFullscreenTranslate(delay = 250, reason = 'translate') { if (fullscreenTranslateTimer) clearTimeout(fullscreenTranslateTimer); fullscreenTranslateReason = reason === 'language' ? 'language' : 'translate'; fullscreenTranslateTimer = setTimeout(() => { fullscreenTranslateTimer = null; translateInFullscreen(fullscreenTranslateReason); }, delay); } function hideLanguagePanels() { if (fullscreenSourceLangPanel) fullscreenSourceLangPanel.style.display = 'none'; if (fullscreenTargetLangPanel) fullscreenTargetLangPanel.style.display = 'none'; } function renderLanguageGrid(gridEl, searchEl, selectEl, currentLabelEl, panelEl, customOptions) { if (!gridEl || !selectEl) return; const style = getLanguagePanelThemeStyles(); applyLanguagePanelContainerTheme(panelEl, searchEl); const query = (searchEl && searchEl.value || '').toLowerCase(); const btns = []; const firstUsableOption = Array.from(selectEl.options || []).find(opt => !opt.disabled && opt.value); const hasAutoOption = !!selectEl.querySelector('option[value="auto"]'); const current = selectEl.value || (hasAutoOption ? 'auto' : (firstUsableOption ? firstUsableOption.value : defaultTargetLang)); const pushBtn = (code, name) => { const active = code === current; btns.push(``); }; if (customOptions && customOptions.length) { customOptions.forEach(({ value, label }) => { const code = value; const name = label; if (query && !name.toLowerCase().includes(query) && !code.toLowerCase().includes(query)) return; pushBtn(code, name); }); } else { const entries = Array.from(selectEl.options) .filter(option => !option.disabled && option.value) .map(option => [option.value, option.textContent || getLanguageLabel(option.value)]); entries.forEach(([code, name]) => { if (query && !name.toLowerCase().includes(query) && !code.toLowerCase().includes(query)) return; pushBtn(code, name); }); } gridEl.innerHTML = btns.join(''); gridEl.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => { const code = btn.getAttribute('data-code'); if (!code || !selectEl.querySelector(`option[value="${code}"]`)) return; stopSpeaking(); selectEl.value = code; if (selectEl === fullscreenTargetLangSelect) { const validTarget = ensureFullscreenTargetLanguageValid(code); currentResolvedTargetLang = resolveTargetLanguageValue(validTarget, currentResolvedTargetLang || defaultTargetLang); } else if (selectEl === fullscreenSourceLangSelect && code !== 'auto') { detectedSourceLang = code; } if (currentLabelEl === fullscreenSourceLangCurrent) { updateFullscreenSourceCurrentLabel(); } else if (currentLabelEl === fullscreenTargetLangCurrent) { updateFullscreenTargetCurrentLabel(); } else if (currentLabelEl) { currentLabelEl.textContent = getLanguageLabel(code); } renderLanguageGrid(gridEl, searchEl, selectEl, currentLabelEl, panelEl); if (panelEl) panelEl.style.display = 'none'; if (selectEl === fullscreenTargetLangSelect || selectEl === fullscreenSourceLangSelect) { scheduleFullscreenTranslate(0, 'language'); } }); }); if (currentLabelEl === fullscreenSourceLangCurrent) { updateFullscreenSourceCurrentLabel(); } else if (currentLabelEl === fullscreenTargetLangCurrent) { updateFullscreenTargetCurrentLabel(); } else if (currentLabelEl) { currentLabelEl.textContent = getLanguageLabel(current); } } if (fullscreenSourceCopy) fullscreenSourceCopy.addEventListener('click', () => { const text = fullscreenSource.value || ''; if (!text) return; navigator.clipboard.writeText(text); const svg = fullscreenSourceCopy.querySelector('svg'); if (svg) { svg.style.stroke = COPY_FEEDBACK_STROKE; setTimeout(() => { svg.style.stroke = getIconDefaultStrokeColor(); }, 900); } }); if (fullscreenTargetCopy) fullscreenTargetCopy.addEventListener('click', () => { const text = fullscreenTarget.value || ''; if (!text) return; navigator.clipboard.writeText(text); const svg = fullscreenTargetCopy.querySelector('svg'); if (svg) { svg.style.stroke = COPY_FEEDBACK_STROKE; setTimeout(() => { svg.style.stroke = getIconDefaultStrokeColor(); }, 900); } }); if (fullscreenSourceSpeak) fullscreenSourceSpeak.addEventListener('click', () => { const text = fullscreenSource.value.trim(); if (!text) return; if (speechPlaying && currentSpeakerId === 'fs-source') { stopSpeaking(); return; } const selectedSrc = fullscreenSourceLangSelect ? fullscreenSourceLangSelect.value : 'auto'; const langForSpeech = resolveSourceSpeechLanguage(selectedSrc); speak(text, langForSpeech, 'fs-source'); }); if (fullscreenTargetSpeak) fullscreenTargetSpeak.addEventListener('click', () => { const text = fullscreenTarget.value.trim(); if (!text) return; if (speechPlaying && currentSpeakerId === 'fs-target') { stopSpeaking(); return; } const selectedTarget = fullscreenTargetLangSelect ? fullscreenTargetLangSelect.value : (targetLangSelect ? targetLangSelect.value : defaultTargetLang); const tgtLang = resolveTargetSpeechLanguage(selectedTarget, currentResolvedTargetLang); speak(text, tgtLang || browserLang, 'fs-target'); }); if (fullscreenSourceLangSearch) fullscreenSourceLangSearch.addEventListener('input', () => { renderLanguageGrid(fullscreenSourceLangGrid, fullscreenSourceLangSearch, fullscreenSourceLangSelect, fullscreenSourceLangCurrent, fullscreenSourceLangPanel); }); if (fullscreenTargetLangSearch) fullscreenTargetLangSearch.addEventListener('input', () => { renderLanguageGrid(fullscreenTargetLangGrid, fullscreenTargetLangSearch, fullscreenTargetLangSelect, fullscreenTargetLangCurrent, fullscreenTargetLangPanel); }); function markFullscreenResizeStart(e) { if (!e || !e.currentTarget) return; const rect = e.currentTarget.getBoundingClientRect(); if (!rect || !rect.height) return; const resizeZone = 18; const isNearBottom = (rect.bottom - e.clientY) <= resizeZone; if (!isNearBottom) return; fullscreenTextareaResizePending = true; fullscreenTextareaResizeActive = e.currentTarget; fullscreenTextareaResizeStartHeight = Math.round(rect.height); fullscreenTextareaLastSyncedHeight = fullscreenTextareaResizeStartHeight; } if (fullscreenSource) fullscreenSource.addEventListener('pointerdown', markFullscreenResizeStart); if (fullscreenTarget) fullscreenTarget.addEventListener('pointerdown', markFullscreenResizeStart); if (fullscreenSource) fullscreenSource.addEventListener('input', () => scheduleFullscreenTranslate(250, 'translate')); if (fullscreenSourceLangSelect) fullscreenSourceLangSelect.addEventListener('change', () => { if (fullscreenSourceLangSelect.value !== 'auto') { detectedSourceLang = fullscreenSourceLangSelect.value; } updateFullscreenSourceCurrentLabel(); scheduleFullscreenTranslate(0, 'language'); }); if (fullscreenTargetLangSelect) fullscreenTargetLangSelect.addEventListener('change', () => { const validTarget = ensureFullscreenTargetLanguageValid(fullscreenTargetLangSelect.value); currentResolvedTargetLang = resolveTargetLanguageValue(validTarget, currentResolvedTargetLang || defaultTargetLang); updateFullscreenTargetCurrentLabel(); scheduleFullscreenTranslate(0, 'language'); }); function swapFullscreenContent() { if (!fullscreenSource || !fullscreenTarget || !fullscreenSourceLangSelect || !fullscreenTargetLangSelect) return; stopSpeaking(); const srcText = fullscreenSource.value; fullscreenSource.value = fullscreenTarget.value; fullscreenTarget.value = srcText; const srcLang = fullscreenSourceLangSelect.value || 'auto'; const tgtLang = fullscreenTargetLangSelect.value || defaultTargetLang; fullscreenSourceLangSelect.value = fullscreenSourceLangSelect.querySelector(`option[value="${tgtLang}"]`) ? tgtLang : 'auto'; let swappedTargetLang = srcLang; if (swappedTargetLang === 'auto') { swappedTargetLang = resolveTargetLanguageValue( (detectedSourceLang && detectedSourceLang !== 'auto') ? detectedSourceLang : currentResolvedTargetLang, defaultTargetLang ); } const validTarget = ensureFullscreenTargetLanguageValid(swappedTargetLang); currentResolvedTargetLang = resolveTargetLanguageValue(validTarget, defaultTargetLang); if (fullscreenSourceLangSelect.value !== 'auto') { detectedSourceLang = fullscreenSourceLangSelect.value; } updateFullscreenSourceCurrentLabel(); updateFullscreenTargetCurrentLabel(); scheduleFullscreenTranslate(0, 'language'); } if (fullscreenSwap) { fullscreenSwap.addEventListener('click', () => { swapFullscreenContent(); fullscreenSwapRotation += 360; fullscreenSwap.style.transform = `rotate(${fullscreenSwapRotation}deg)`; }); } function togglePanel(panelEl, otherPanel) { if (!panelEl) return; const isOpen = panelEl.style.display === 'block'; hideLanguagePanels(); panelEl.style.display = isOpen ? 'none' : 'block'; } if (fullscreenSourceLangTrigger) fullscreenSourceLangTrigger.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(fullscreenSourceLangPanel, fullscreenTargetLangPanel); renderLanguageGrid(fullscreenSourceLangGrid, fullscreenSourceLangSearch, fullscreenSourceLangSelect, fullscreenSourceLangCurrent, fullscreenSourceLangPanel); }); if (fullscreenTargetLangTrigger) fullscreenTargetLangTrigger.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(fullscreenTargetLangPanel, fullscreenSourceLangPanel); renderLanguageGrid(fullscreenTargetLangGrid, fullscreenTargetLangSearch, fullscreenTargetLangSelect, fullscreenTargetLangCurrent, fullscreenTargetLangPanel); }); document.addEventListener('mousedown', (e) => { if (selectionBubble && !selectionBubble.contains(e.target)) { hideBubbleCloseMenu(); } if (fullscreenSourceLangPanel && !fullscreenSourceLangPanel.contains(e.target) && fullscreenSourceLangTrigger && !fullscreenSourceLangTrigger.contains(e.target)) { fullscreenSourceLangPanel.style.display = 'none'; } if (fullscreenTargetLangPanel && !fullscreenTargetLangPanel.contains(e.target) && fullscreenTargetLangTrigger && !fullscreenTargetLangTrigger.contains(e.target)) { fullscreenTargetLangPanel.style.display = 'none'; } if (panelThemePanel && !panelThemePanel.contains(e.target) && panelThemeTrigger && !panelThemeTrigger.contains(e.target)) { panelThemePanel.style.display = 'none'; } inlineLanguagePanels.forEach(({ panel, selectEl }) => { if (!panel.contains(e.target) && !selectEl.contains(e.target)) { panel.style.display = 'none'; } }); }); function hideInlinePanels(except) { inlineLanguagePanels.forEach(p => { if (p.panel === except) return; p.panel.style.display = 'none'; }); } function positionInlinePanel(panel, selectEl) { if (!panel || panel.style.display !== 'block' || !selectEl) return; const rect = selectEl.getBoundingClientRect(); const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || 0; const panelWidth = panel.offsetWidth || 280; const left = Math.min(rect.left + scrollX, scrollX + window.innerWidth - panelWidth - 10); const top = rect.bottom + scrollY + 4; panel.style.left = `${left}px`; panel.style.top = `${top}px`; } function updateInlinePanelsPosition() { inlineLanguagePanels.forEach(({ panel, selectEl }) => { positionInlinePanel(panel, selectEl); }); } function buildInlinePanel(selectEl, placeholder = langNames.navigator) { const panel = document.createElement('div'); panel.style.cssText = ` all: initial; display:none; position: absolute; width: 280px; max-height: 260px; background: rgba(30,30,47,0.98); border: 1px solid rgba(255,255,255,0.12); box-shadow: 0 10px 24px rgba(0,0,0,0.35); border-radius: 10px; padding: 8px; z-index: 2147483646; `; panel.innerHTML = `
`; panel.classList.add('utst-inline-lang-panel'); const searchEl = panel.querySelector('.inlineLangSearch'); applyLanguagePanelContainerTheme(panel, searchEl); panel.classList.add("utst-scroll"); document.documentElement.appendChild(panel); inlineLanguagePanels.push({ panel, selectEl }); return panel; } function attachInlineLanguagePanel(selectEl) { if (!selectEl) return; const panel = buildInlinePanel(selectEl); const searchEl = panel.querySelector('.inlineLangSearch'); const gridEl = panel.querySelector('.inlineLangGrid'); function render() { const opts = Array.from(selectEl.options) .filter(o => !o.disabled) .map(o => ({ value: o.value, label: o.textContent || o.value })); renderLanguageGrid(gridEl, searchEl, selectEl, null, panel, opts); gridEl.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => { const code = btn.getAttribute('data-code'); selectEl.value = code; selectEl.dispatchEvent(new Event('change', { bubbles: true })); hideInlinePanels(); }); }); } if (searchEl) searchEl.addEventListener('input', render); const openInlinePanel = (e) => { e.preventDefault(); e.stopPropagation(); const isOpen = panel.style.display === 'block'; hideInlinePanels(panel); if (isOpen) { panel.style.display = 'none'; return; } render(); panel.style.display = 'block'; positionInlinePanel(panel, selectEl); }; selectEl.addEventListener('pointerdown', openInlinePanel, { capture: true }); selectEl.addEventListener('mousedown', openInlinePanel); selectEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { openInlinePanel(e); } }); } attachInlineLanguagePanel(sourceLangSelect); attachInlineLanguagePanel(targetLangSelect); attachInlineLanguagePanel(defaultTranslateLangSelect); attachInlineLanguagePanel(toolLanguageSelect); refreshLanguagePanelTheme(); if (fullscreenToggle) fullscreenToggle.addEventListener('click', openFullscreenOverlay); if (fullscreenClose) fullscreenClose.addEventListener('click', closeFullscreenOverlay); const closeButton = translationBox.querySelector('#closeButton'); function lockPanelDimensions() { if (!translationBox || translationBox.style.display !== 'block') return; const styles = window.getComputedStyle(translationBox); const width = parseFloat(styles.width); const height = parseFloat(styles.height); if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return; translationBox.style.width = `${width}px`; translationBox.style.minWidth = `${width}px`; translationBox.style.maxWidth = `${width}px`; translationBox.style.height = `${height}px`; translationBox.style.minHeight = `${height}px`; translationBox.style.maxHeight = `${height}px`; } closeButton.addEventListener('click', () => { panelTranslateRequestId++; setPanelLoading(false); translationBox.style.display = 'none'; translationBox.style.opacity = '0'; translationBox.style.transform = 'translateY(10px)'; sourceLangSelect.value = 'auto'; detectedSourceLang = 'auto'; stopSpeaking(); const translatorPanel = document.getElementById('translatorPanel'); const settingsPanel = document.getElementById('settingsPanel'); if (translatorPanel) translatorPanel.style.display = 'block'; if (settingsPanel) settingsPanel.style.display = 'none'; if (settingsHeader) settingsHeader.style.display = 'none'; translationBox.classList.remove('utst-settings-open'); scheduleSelectionBubbleUpdate(0); }); settingsButton.addEventListener('click', () => { lockPanelDimensions(); const translatorPanel = document.getElementById('translatorPanel'); const settingsPanel = document.getElementById('settingsPanel'); if (translatorPanel) translatorPanel.style.display = 'none'; if (settingsPanel) settingsPanel.style.display = 'block'; if (settingsHeaderTitle) settingsHeaderTitle.textContent = settingsTitle; if (settingsHeader) settingsHeader.style.display = 'flex'; translationBox.classList.remove('utst-settings-open'); }); backButton.addEventListener('click', () => { const translatorPanel = document.getElementById('translatorPanel'); const settingsPanel = document.getElementById('settingsPanel'); if (translatorPanel) translatorPanel.style.display = 'block'; if (settingsPanel) settingsPanel.style.display = 'none'; if (settingsHeader) settingsHeader.style.display = 'none'; translationBox.classList.remove('utst-settings-open'); }); document.addEventListener('mousedown', (e) => { const clickInInlinePanel = inlineLanguagePanels.some(({ panel, selectEl }) => panel.contains(e.target) || selectEl.contains(e.target) ); const clickInFullscreenLangPanel = (fullscreenSourceLangPanel && fullscreenSourceLangPanel.contains(e.target)) || (fullscreenTargetLangPanel && fullscreenTargetLangPanel.contains(e.target)) || (fullscreenSourceLangTrigger && fullscreenSourceLangTrigger.contains(e.target)) || (fullscreenTargetLangTrigger && fullscreenTargetLangTrigger.contains(e.target)); const clickInFullscreenOverlay = fullscreenOverlay && fullscreenOverlay.contains(e.target); const clickInSelectionBubble = selectionBubble && selectionBubble.contains(e.target); if (clickInInlinePanel || clickInFullscreenLangPanel || clickInFullscreenOverlay || clickInSelectionBubble) return; if (!translationBox.contains(e.target)) { panelTranslateRequestId++; setPanelLoading(false); translationBox.style.display = 'none'; translationBox.style.opacity = '0'; translationBox.style.transform = 'translateY(10px)'; sourceLangSelect.value = 'auto'; detectedSourceLang = 'auto'; if (settingsHeader) settingsHeader.style.display = 'none'; translationBox.classList.remove('utst-settings-open'); stopSpeaking(); scheduleSelectionBubbleUpdate(0); } }); function adjustBoxPosition() { const rect = translationBox.getBoundingClientRect(); if (rect.right > window.innerWidth) { translationBox.style.left = `${window.innerWidth - rect.width - 10}px`; } if (rect.bottom > window.innerHeight) { translationBox.style.top = `${window.innerHeight - rect.height - 10}px`; } } translationBox.addEventListener('transitionend', adjustBoxPosition); document.addEventListener('selectionchange', () => { if (isSelectingPointer) { hideSelectionBubble(); return; } scheduleSelectionBubbleUpdate(); }); document.addEventListener('mouseup', () => { if (fullscreenTextareaResizePending) { fullscreenTextareaResizePending = false; if (fullscreenOverlay.style.display === 'flex' && fullscreenTextareaResizeActive) { const endHeight = Math.round(fullscreenTextareaResizeActive.getBoundingClientRect().height || 0); if (Math.abs(endHeight - fullscreenTextareaResizeStartHeight) >= 1) { syncFullscreenTextareaHeights(endHeight); } } fullscreenTextareaResizeActive = null; fullscreenTextareaResizeStartHeight = 0; fullscreenTextareaLastSyncedHeight = 0; if (fullscreenTextareaResizeRaf) { cancelAnimationFrame(fullscreenTextareaResizeRaf); fullscreenTextareaResizeRaf = 0; } } isSelectingPointer = false; scheduleSelectionBubbleUpdate(); }, true); document.addEventListener('keyup', () => { scheduleSelectionBubbleUpdate(); }); window.addEventListener('scroll', () => { updateInlinePanelsPosition(); scheduleSelectionBubbleUpdate(0); }, true); window.addEventListener('resize', () => { updateInlinePanelsPosition(); syncFullscreenTextareaHeights(); hideSelectionBubble(); scheduleSelectionBubbleUpdate(40); }); } if (document.body) { bootstrap(); } else { window.addEventListener('DOMContentLoaded', bootstrap); } })();