// ==UserScript==
// @name Ultimate Text Selection Translator – Instantly Translate Any Selected Text
// @name:fr Ultimate Text Selection Translator – Traduis instantanément n’importe quel texte sélectionné
// @name:es Ultimate Text Selection Translator – Traduce al instante cualquier texto seleccionado
// @name:de Ultimate Text Selection Translator – Übersetze sofort ausgewählten Text
// @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 all’istante qualsiasi testo selezionato
// @namespace http://tampermonkey.net/
// @version 1.3.2
// @description Translate selected text instantly using Ctrl+L. Supports all languages and automatically detects the selected language, translating it into your browser's default language. Simple, fast, and efficient.
// @description:fr Traduis instantanément n’importe quel texte sélectionné avec Ctrl+L. Prend en charge toutes les langues, détecte automatiquement la langue sélectionnée et la traduit dans la langue par défaut de ton navigateur. Simple, rapide et efficace.
// @description:es Traduce al instante cualquier texto seleccionado con Ctrl+L. Compatible con todos los idiomas, detecta automáticamente el idioma seleccionado y lo traduce al idioma predeterminado de tu navegador. Simple, rápido y eficiente.
// @description:de Übersetze ausgewählten Text sofort mit Ctrl+L. Unterstützt alle Sprachen, erkennt automatisch die ausgewählte Sprache und übersetzt sie in die Standardsprache deines Browsers. Einfach, schnell und effizient.
// @description:ru Мгновенно переводите выделенный текст с помощью Ctrl+L. Поддерживает все языки, автоматически определяет выделенный язык и переводит его на язык по умолчанию вашего браузера. Просто, быстро и эффективно.
// @description:zh-CN 使用 Ctrl+L 可即时翻译所选文本。支持所有语言,自动检测所选语言,并翻译为浏览器的默认语言。简单、快速、高效。
// @description:zh-TW 使用 Ctrl+L 可即時翻譯所選文字。支援所有語言,自動偵測所選語言,並翻譯為瀏覽器的預設語言。簡單、快速、高效。
// @description:ja Ctrl+L で選択したテキストを即座に翻訳。すべての言語に対応し、選択された言語を自動的に検出して、ブラウザのデフォルト言語に翻訳。シンプル、スピーディー、効率的。
// @description:pt Traduza texto selecionado instantaneamente usando Ctrl+L. Suporta todos os idiomas e detecta automaticamente o idioma selecionado, traduzindo para o idioma padrão do seu navegador. Simples, rápido e eficiente.
// @description:it Traduci immediatamente il testo selezionato usando Ctrl+L. Supporta tutte le lingue e rileva automaticamente la lingua selezionata, traducendola nella lingua predefinita del tuo browser. Semplice, veloce ed efficiente.
// @author Dℝ∃wX
// @copyright 2025 DℝᴇwX
// @license Apache-2.0
// @match *://*/*
// @grant GM_xmlhttpRequest
// @icon https://raw.githubusercontent.com/DREwX-code/Ultimate-Text-Selection-Translator/refs/heads/main/assets/icons/Icon_Translate_Script.png
// @connect translate.googleapis.com
// @tag translation
// @tag text selection
// @tag translate
// @tag google translate
// @tag shortcut
// @tag productivity
// @tag accessibility
// @tag language
// @tag multilingual
// @grant GM_getValue
// @grant GM_setValue
// @downloadURL none
// ==/UserScript==
/*
Copyright 2025 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';
const browserLang = navigator.language.split('-')[0];
const supportedUiLanguages = ['en', 'fr', 'es', 'de', 'it', 'pt', 'ru', 'zh-CN', 'ja'];
const languageNames = {
'en': {
'auto': 'Detect',
'en': 'English',
'fr': 'French',
'es': 'Spanish',
'de': 'German',
'it': 'Italian',
'pt': 'Portuguese',
'ru': 'Russian',
'zh-CN': 'Chinese (Simplified)',
'ja': 'Japanese',
'errors': {
'noText': 'No text selected',
'translation': 'Translation error',
'connection': 'Connection error'
},
'tooltips': {
'listenTranslated': 'Listen to translated text',
'listenOriginal': 'Listen to original text'
},
'overlay': {
'title': 'Fullscreen Translator',
'source': 'Source text',
'target': 'Translated text',
'translate': 'Translate',
'open': 'Fullscreen',
'sourceLangLabel': 'Source language',
'targetLangLabel': 'Target language'
},
'dragHandleLabel': 'Move',
'settingsTitle': 'Settings',
'settingsDefaultLabel': 'Default translation language:',
'settingsToolLabel': 'Tool language:',
'navigator': 'Browser language',
},
'fr': {
'auto': 'Détecter',
'en': 'Anglais',
'fr': 'Français',
'es': 'Espagnol',
'de': 'Allemand',
'it': 'Italien',
'pt': 'Portugais',
'ru': 'Russe',
'zh-CN': 'Chinois (Simplifié)',
'ja': 'Japonais',
'errors': {
'noText': 'Aucun texte sélectionné',
'translation': 'Erreur de traduction',
'connection': 'Erreur de connexion'
},
'tooltips': {
'listenTranslated': 'Écoute le texte traduit',
'listenOriginal': 'Écoute le texte original'
},
'overlay': {
'title': 'Traduction plein écran',
'source': 'Texte source',
'target': 'Texte traduit',
'translate': 'Traduire',
'open': 'Plein écran',
'sourceLangLabel': 'Langue source',
'targetLangLabel': 'Langue cible'
},
'dragHandleLabel': 'Déplacer',
'settingsTitle': 'Paramètres',
'settingsDefaultLabel': 'Langue de traduction par défaut :',
'settingsToolLabel': "Langue de l'outil :",
'navigator': 'Langue du navigateur',
},
'es': {
'auto': 'Detectar',
'en': 'Inglés',
'fr': 'Francés',
'es': 'Español',
'de': 'Alemán',
'it': 'Italiano',
'pt': 'Portugués',
'ru': 'Ruso',
'zh-CN': 'Chino (Simplificado)',
'ja': 'Japonés',
'errors': {
'noText': 'No hay texto seleccionado',
'translation': 'Error de traducción',
'connection': 'Error de conexión'
},
'tooltips': {
'listenTranslated': 'Escuchar el texto traducido',
'listenOriginal': 'Escuchar el texto original'
},
'dragHandleLabel': 'Mover',
'settingsTitle': 'Configuración',
'settingsDefaultLabel': 'Idioma de traducción predeterminado:',
'settingsToolLabel': 'Idioma de la interfaz:',
'navigator': 'Idioma del navegador',
},
'de': {
'auto': 'Erkennen',
'en': 'Englisch',
'fr': 'Französisch',
'es': 'Spanisch',
'de': 'Deutsch',
'it': 'Italienisch',
'pt': 'Portugiesisch',
'ru': 'Russisch',
'zh-CN': 'Chinesisch (Vereinfacht)',
'ja': 'Japanisch',
'errors': {
'noText': 'Kein Text ausgewählt',
'translation': 'Übersetzungsfehler',
'connection': 'Verbindungsfehler'
},
'tooltips': {
'listenTranslated': 'Übersetzten Text anhören',
'listenOriginal': 'Originaltext anhören'
},
'dragHandleLabel': 'Verschieben',
'settingsTitle': 'Einstellungen',
'settingsDefaultLabel': 'Standardübersetzungssprache:',
'settingsToolLabel': 'Werkzeugsprache:',
'navigator': 'Browser-Sprache',
},
'it': {
'auto': 'Rileva',
'en': 'Inglese',
'fr': 'Francese',
'es': 'Spagnolo',
'de': 'Tedesco',
'it': 'Italiano',
'pt': 'Portoghese',
'ru': 'Russo',
'zh-CN': 'Cinese (Semplificato)',
'ja': 'Giapponese',
'errors': {
'noText': 'Nessun testo selezionato',
'translation': 'Errore di traduzione',
'connection': 'Errore di connessione'
},
'tooltips': {
'listenTranslated': 'Ascolta il testo tradotto',
'listenOriginal': 'Ascolta il testo originale'
},
'dragHandleLabel': 'Spostare',
'settingsTitle': 'Impostazioni',
'settingsDefaultLabel': 'Lingua di traduzione predefinita:',
'settingsToolLabel': "Lingua dell'interfaccia:",
'navigator': 'Lingua del browser',
},
'pt': {
'auto': 'Detectar',
'en': 'Inglês',
'fr': 'Francês',
'es': 'Espanhol',
'de': 'Alemão',
'it': 'Italiano',
'pt': 'Português',
'ru': 'Russo',
'zh-CN': 'Chinês (Simplificado)',
'ja': 'Japonês',
'errors': {
'noText': 'Nenhum texto selecionado',
'translation': 'Erro de tradução',
'connection': 'Erro de conexão'
},
'tooltips': {
'listenTranslated': 'Ouvir o texto traduzido',
'listenOriginal': 'Ouvir o texto original'
},
'dragHandleLabel': 'Mover',
'settingsTitle': 'Configurações',
'settingsDefaultLabel': 'Idioma de tradução padrão:',
'settingsToolLabel': 'Idioma da interface:',
'navigator': 'Idioma do navegador',
},
'ru': {
'auto': 'Определить',
'en': 'Английский',
'fr': 'Французский',
'es': 'Испанский',
'de': 'Немецкий',
'it': 'Итальянский',
'pt': 'Португальский',
'ru': 'Русский',
'zh-CN': 'Китайский (упрощённый)',
'ja': 'Японский',
'errors': {
'noText': 'Текст не выделен',
'translation': 'Ошибка перевода',
'connection': 'Ошибка соединения'
},
'tooltips': {
'listenTranslated': 'Прослушать переведённый текст',
'listenOriginal': 'Прослушать оригинальный текст'
},
'dragHandleLabel': 'Переместить',
'settingsTitle': 'Настройки',
'settingsDefaultLabel': 'Язык перевода по умолчанию:',
'settingsToolLabel': 'Язык интерфейса:',
'navigator': 'Язык браузера',
},
'zh-CN': {
'auto': '检测',
'en': '英语',
'fr': '法语',
'es': '西班牙语',
'de': '德语',
'it': '意大利语',
'pt': '葡萄牙语',
'ru': '俄语',
'zh-CN': '中文(简体)',
'ja': '日语',
'errors': {
'noText': '未选择文本',
'translation': '翻译错误',
'connection': '连接错误'
},
'tooltips': {
'listenTranslated': '聆听翻译文本',
'listenOriginal': '聆听原文'
},
'dragHandleLabel': '移动',
'settingsTitle': '设置',
'settingsDefaultLabel': '默认翻译语言:',
'settingsToolLabel': '界面语言:',
'navigator': '浏览器语言',
},
'ja': {
'auto': '検出',
'en': '英語',
'fr': 'フランス語',
'es': 'スペイン語',
'de': 'ドイツ語',
'it': 'イタリア語',
'pt': 'ポルトガル語',
'ru': 'ロシア語',
'zh-CN': '中国語(簡体)',
'ja': '日本語',
'errors': {
'noText': 'テキストが選択されていません',
'translation': '翻訳エラー',
'connection': '接続エラー'
},
'tooltips': {
'listenTranslated': '翻訳されたテキストを聞く',
'listenOriginal': '元のテキストを聞く'
},
'dragHandleLabel': '移動',
'settingsTitle': '設定',
'settingsDefaultLabel': '既定の翻訳言語:',
'settingsToolLabel': 'ツールの言語:',
'navigator': 'ブラウザの言語',
}
};
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: langNames.auto },
{ code: 'en', name: langNames.en },
{ code: 'fr', name: langNames.fr },
{ code: 'es', name: langNames.es },
{ code: 'de', name: langNames.de },
{ code: 'it', name: langNames.it },
{ code: 'pt', name: langNames.pt },
{ code: 'ru', name: langNames.ru },
{ code: 'zh-CN', name: langNames['zh-CN'] },
{ code: 'ja', name: langNames.ja },
{ code: 'navigator', name: langNames.navigator }
];
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 === 'navigator') {
return langNames.navigator;
}
return langNames[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 langNames.navigator;
}
return langNames[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.style.cssText = `
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;
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);
#closeButton:hover svg {
stroke: #ff0000;
transform: scale(1.1);
transition: all 0.2s ease;
}
`;
document.body.appendChild(translationBox);
translationBox.innerHTML = `
→
`;
const fullscreenOverlay = document.createElement('div');
fullscreenOverlay.id = 'fullscreenOverlay';
fullscreenOverlay.style.cssText = `
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;
`;
fullscreenOverlay.innerHTML = `
`;
document.body.appendChild(fullscreenOverlay);
const BOX_W = 420;
const BOX_H = 260;
const MARGIN = 10;
function placeBoxAtSelection() {
const sel = window.getSelection();
if (!sel || !sel.rangeCount) 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 (!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 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 defaultTranslateLangLabel = translationBox.querySelector('label[for="defaultTranslateLang"]');
const toolLanguageLabel = translationBox.querySelector('label[for="toolLanguage"]');
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 fullscreenSourceCopy = fullscreenOverlay.querySelector('#fullscreenSourceCopy');
const fullscreenSourceSpeak = fullscreenOverlay.querySelector('#fullscreenSourceSpeak');
const fullscreenTargetCopy = fullscreenOverlay.querySelector('#fullscreenTargetCopy');
const fullscreenTargetSpeak = fullscreenOverlay.querySelector('#fullscreenTargetSpeak');
const fullscreenToggle = translationBox.querySelector('#fullscreenToggle');
sourceLangSelect.value = 'auto';
const inlineLanguagePanels = [];
let fullscreenSwapRotation = 0;
let currentSelectedText = '';
let currentTranslatedText = '';
let detectedSourceLang = 'auto';
let currentResolvedTargetLang = browserLang;
let fullscreenTranslateTimer = null;
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;
if (settingsHeaderTitle) settingsHeaderTitle.textContent = settingsTitle;
if (defaultTranslateLangLabel) defaultTranslateLangLabel.textContent = settingsDefaultLabel;
if (toolLanguageLabel) toolLanguageLabel.textContent = settingsToolLabel;
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 (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;
ensureSelectValue(fullscreenTargetLangSelect, prev);
}
if (toolLanguageSelect) {
toolLanguageSelect.innerHTML = buildToolLanguageOptionsHtml();
toolLanguageSelect.value = normalizedSelection;
}
if (translationText && previousErrors && translationText.textContent === previousErrors.noText) {
translationText.textContent = errors.noText;
}
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', () => {
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 });
});
}
applyToolLanguage(toolLanguagePreference);
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';
}
const fullTranslation = translatedSentences.join('');
callback(fullTranslation, position, resolvedTargetLang);
}
});
});
}
let currentUtterance = null;
let currentSpeakerId = null;
let speechPlaying = false;
function stopSpeaking() {
speechPlaying = false;
if (window.speechSynthesis.speaking || window.speechSynthesis.pending) {
window.speechSynthesis.cancel();
}
currentUtterance = null;
currentSpeakerId = null;
}
function speak(text, lang, speakerId = null) {
if (!text) return;
const normalizedLang = lang || browserLang;
const engineSpeaking = speechPlaying || window.speechSynthesis.speaking || window.speechSynthesis.pending || !!currentUtterance;
const sameSpeaker = speakerId && speakerId === currentSpeakerId;
if (sameSpeaker) {
stopSpeaking();
return;
}
if (engineSpeaking) {
stopSpeaking();
}
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = normalizedLang;
currentUtterance = utterance;
currentSpeakerId = speakerId;
speechPlaying = true;
utterance.onend = utterance.onerror = () => {
currentUtterance = null;
currentSpeakerId = null;
speechPlaying = false;
};
window.speechSynthesis.speak(utterance);
}
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key.toLowerCase() === 'l' && !e.altKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
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';
const selectedText = getSelectedText();
if (!selectedText) {
translationText.textContent = errors.noText;
translationBox.style.display = 'block';
translationBox.style.left = `${window.innerWidth / 2 - 150}px`;
translationBox.style.top = `${window.innerHeight / 2 - 50}px`;
translationBox.style.opacity = '1';
translationBox.style.transform = 'translateY(0)';
return;
}
currentSelectedText = selectedText;
const selection = window.getSelection();
let position = { x: 0, y: 0 };
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
position = {
x: rect.left + window.scrollX,
y: rect.bottom + window.scrollY
};
}
const savedTargetLang = getSavedTargetLanguage();
const targetLangForSession = ensureSelectValue(targetLangSelect, savedTargetLang);
ensureSelectValue(defaultTranslateLangSelect, savedTargetLang);
translateText(selectedText, 'auto', targetLangForSession, (translation, pos, resolvedTargetLang) => {
currentTranslatedText = translation;
translationText.textContent = translation;
currentResolvedTargetLang = resolvedTargetLang || currentResolvedTargetLang;
placeBoxAtSelection();
translationBox.style.display = 'block';
translationBox.style.opacity = '1';
translationBox.style.transform = 'translateY(0)';
}, position);
}
});
function handleLanguageChange() {
if (currentSelectedText) {
translateText(currentSelectedText, sourceLangSelect.value, targetLangSelect.value, (translation, pos, resolvedTargetLang) => {
currentTranslatedText = translation;
translationText.textContent = translation;
currentResolvedTargetLang = resolvedTargetLang || currentResolvedTargetLang;
}, { x: parseFloat(translationBox.style.left), y: parseFloat(translationBox.style.top) });
}
}
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';
});
[speakTranslated, speakOriginal].forEach(el => {
if (!el) return;
el.addEventListener('mouseenter', () => {
el.style.background = 'rgba(255,255,255,0.12)';
});
el.addEventListener('mouseleave', () => {
el.style.background = 'transparent';
});
});
speakTranslated.addEventListener('click', () => {
if (currentTranslatedText) {
const langForSpeech = currentResolvedTargetLang || (targetLangSelect.value === 'navigator' ? browserLang : targetLangSelect.value);
speak(currentTranslatedText, langForSpeech, 'panel-translated');
}
});
speakOriginal.addEventListener('click', () => {
if (currentSelectedText) {
speak(currentSelectedText, detectedSourceLang, 'panel-original');
}
});
copyButton.addEventListener('click', () => {
if (currentTranslatedText) {
navigator.clipboard.writeText(currentTranslatedText);
copyButton.querySelector('svg').style.stroke = '#00ff00';
setTimeout(() => {
copyButton.querySelector('svg').style.stroke = '#ffffff';
}, 1000);
}
});
function openFullscreenOverlay() {
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;
ensureSelectValue(fullscreenTargetLangSelect, tgtVal);
}
hideLanguagePanels();
renderLanguageGrid(fullscreenSourceLangGrid, fullscreenSourceLangSearch, fullscreenSourceLangSelect, fullscreenSourceLangCurrent, fullscreenSourceLangPanel);
renderLanguageGrid(fullscreenTargetLangGrid, fullscreenTargetLangSearch, fullscreenTargetLangSelect, fullscreenTargetLangCurrent, fullscreenTargetLangPanel);
scheduleFullscreenTranslate(0);
}
function closeFullscreenOverlay() {
fullscreenOverlay.style.display = 'none';
}
function translateInFullscreen() {
const text = fullscreenSource.value.trim();
const target = fullscreenTargetLangSelect ? fullscreenTargetLangSelect.value : (targetLangSelect ? targetLangSelect.value : defaultTargetLang);
const srcLang = fullscreenSourceLangSelect ? fullscreenSourceLangSelect.value || 'auto' : 'auto';
translateText(text, srcLang, target, (translation, pos, resolvedTargetLang) => {
fullscreenTarget.value = translation;
currentResolvedTargetLang = resolvedTargetLang || currentResolvedTargetLang;
}, { x: 0, y: 0 });
}
function scheduleFullscreenTranslate(delay = 250) {
if (fullscreenTranslateTimer) clearTimeout(fullscreenTranslateTimer);
fullscreenTranslateTimer = setTimeout(() => {
fullscreenTranslateTimer = null;
translateInFullscreen();
}, delay);
}
function hideLanguagePanels() {
if (fullscreenSourceLangPanel) fullscreenSourceLangPanel.style.display = 'none';
if (fullscreenTargetLangPanel) fullscreenTargetLangPanel.style.display = 'none';
}
function renderLanguageGrid(gridEl, searchEl, selectEl, currentLabelEl, panelEl) {
if (!gridEl || !selectEl) return;
const query = (searchEl && searchEl.value || '').toLowerCase();
const btns = [];
const current = selectEl.value || 'auto';
const pushBtn = (code, name) => {
const active = code === current;
btns.push(``);
};
const entries = [['auto', langNames.auto], ...Object.entries(googleTranslateLanguages).sort(([, a], [, b]) => a.localeCompare(b))];
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');
selectEl.value = code;
if (currentLabelEl) currentLabelEl.textContent = getLanguageLabel(code);
renderLanguageGrid(gridEl, searchEl, selectEl, currentLabelEl, panelEl);
if (panelEl) panelEl.style.display = 'none';
scheduleFullscreenTranslate(0);
});
});
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 = '#00ff99';
setTimeout(() => { svg.style.stroke = '#ffffff'; }, 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 = '#00ff99';
setTimeout(() => { svg.style.stroke = '#ffffff'; }, 900);
}
});
if (fullscreenSourceSpeak) fullscreenSourceSpeak.addEventListener('click', () => {
const text = fullscreenSource.value.trim();
if (!text) return;
const selectedSrc = fullscreenSourceLangSelect ? fullscreenSourceLangSelect.value : 'auto';
const langForSpeech = (selectedSrc && selectedSrc !== 'auto') ? selectedSrc
: (detectedSourceLang && detectedSourceLang !== 'auto') ? detectedSourceLang
: (sourceLangSelect.value && sourceLangSelect.value !== 'auto' ? sourceLangSelect.value : browserLang);
speak(text, langForSpeech, 'fs-source');
});
if (fullscreenTargetSpeak) fullscreenTargetSpeak.addEventListener('click', () => {
const text = fullscreenTarget.value.trim();
if (!text) return;
let tgtLang = fullscreenTargetLangSelect ? fullscreenTargetLangSelect.value : (targetLangSelect ? targetLangSelect.value : defaultTargetLang);
if (tgtLang === 'navigator') tgtLang = browserLang;
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);
});
if (fullscreenSource) fullscreenSource.addEventListener('input', () => scheduleFullscreenTranslate());
if (fullscreenSourceLangSelect) fullscreenSourceLangSelect.addEventListener('change', () => scheduleFullscreenTranslate(0));
if (fullscreenTargetLangSelect) fullscreenTargetLangSelect.addEventListener('change', () => scheduleFullscreenTranslate(0));
function swapFullscreenContent() {
if (!fullscreenSource || !fullscreenTarget || !fullscreenSourceLangSelect || !fullscreenTargetLangSelect) return;
const srcText = fullscreenSource.value;
fullscreenSource.value = fullscreenTarget.value;
fullscreenTarget.value = srcText;
const srcLang = fullscreenSourceLangSelect.value || 'auto';
const tgtLang = fullscreenTargetLangSelect.value || defaultTargetLang;
fullscreenSourceLangSelect.value = tgtLang;
fullscreenTargetLangSelect.value = srcLang;
if (fullscreenSourceLangCurrent) fullscreenSourceLangCurrent.textContent = getLanguageLabel(fullscreenSourceLangSelect.value);
if (fullscreenTargetLangCurrent) fullscreenTargetLangCurrent.textContent = getLanguageLabel(fullscreenTargetLangSelect.value);
scheduleFullscreenTranslate(0);
}
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 (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';
}
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 buildInlinePanel(selectEl, placeholder = langNames.navigator) {
const panel = document.createElement('div');
panel.style.cssText = `
display:none;
position: fixed;
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: 10002;
`;
panel.innerHTML = `
`;
document.body.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 }));
const query = (searchEl.value || '').toLowerCase();
const current = selectEl.value;
const btns = opts
.filter(({ value, label }) => !query || label.toLowerCase().includes(query) || value.toLowerCase().includes(query))
.map(({ value, label }) => {
const active = value === current;
return ``;
}).join('');
gridEl.innerHTML = btns;
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);
selectEl.addEventListener('mousedown', (e) => {
e.preventDefault();
const rect = selectEl.getBoundingClientRect();
const scrollX = window.scrollX || document.documentElement.scrollLeft || 0;
const scrollY = window.scrollY || document.documentElement.scrollTop || 0;
const top = rect.bottom + scrollY + 4;
const left = Math.min(rect.left + scrollX, scrollX + window.innerWidth - 290);
const isOpen = panel.style.display === 'block';
hideInlinePanels(panel);
if (isOpen) {
panel.style.display = 'none';
return;
}
render();
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
panel.style.display = 'block';
});
}
attachInlineLanguagePanel(sourceLangSelect);
attachInlineLanguagePanel(targetLangSelect);
attachInlineLanguagePanel(defaultTranslateLangSelect);
attachInlineLanguagePanel(toolLanguageSelect);
if (fullscreenToggle) fullscreenToggle.addEventListener('click', openFullscreenOverlay);
if (fullscreenClose) fullscreenClose.addEventListener('click', closeFullscreenOverlay);
const closeButton = translationBox.querySelector('#closeButton');
closeButton.addEventListener('click', () => {
translationBox.style.display = 'none';
translationBox.style.opacity = '0';
translationBox.style.transform = 'translateY(10px)';
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';
});
settingsButton.addEventListener('click', () => {
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';
});
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';
});
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);
if (clickInInlinePanel || clickInFullscreenLangPanel || clickInFullscreenOverlay) return;
if (!translationBox.contains(e.target)) {
translationBox.style.display = 'none';
translationBox.style.opacity = '0';
translationBox.style.transform = 'translateY(10px)';
sourceLangSelect.value = 'auto';
detectedSourceLang = 'auto';
if (settingsHeader) settingsHeader.style.display = 'none';
}
});
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);
})();