// ==UserScript== // @name 🌐Reddit Translator Pro // @name:en 🌐Reddit Translator Pro // @name:ru 🌐Reddit Переводчик Pro // @name:uk 🌐Reddit Перекладач Pro // @name:de 🌐Reddit Übersetzer Pro // @name:fr 🌐Reddit Traducteur Pro // @name:es 🌐Reddit Traductor Pro // @name:it 🌐Reddit Traduttore Pro // @name:pl 🌐Reddit Tłumacz Pro // @name:tr 🌐Reddit Çevirmeni Pro // @name:vi 🌐Reddit Biên Dịch Viên Pro // @name:ko 🌐Reddit 번역기 Pro // @name:ja 🌐Reddit 翻訳者 Pro // @name:zh-CN 🌐Reddit 翻译器 Pro // @name:zh-HK 🌐Reddit 翻譯器 Pro // @name:zh-TW 🌐Reddit 翻譯器 Pro // @namespace https://github.com/ebayybe // @homepageURL https://github.com/ebayybe/reddit-translator // @supportURL https://github.com/ebayybe/reddit-translator/issues // @version 1.0.10 // @description:en 🏷️ Amateur Reddit translator — glassmorphism, 100+ languages, Google, MyMemory, DeepL, history, unit converter, easter eggs // @description:ru 🏷️ Любительский переводчик Reddit — glassmorphism, 100+ языков, Google, MyMemory, DeepL, история, конвертер единиц, пасхалки // @description:uk 🏷️ Любительський перекладач Reddit — glassmorphism, 100+ мов, Google, MyMemory, DeepL, історія, конвертер одиниць, пасхалки // @description:de 🏷️ Amateur-Reddit-Übersetzer — Glassmorphism, 100+ Sprachen, Google, MyMemory, DeepL, Verlauf, Einheitenumrechner, Ostereier // @description:it 🏷️ Traduttore Reddit amatoriale — glassmorphism, 100+ lingue, Google, MyMemory, DeepL, cronologia, convertitore di unità, easter egg // @description:fr 🏷️ Traducteur Reddit amateur — glassmorphism, 100+ langues, Google, MyMemory, DeepL, historique, convertisseur d'unités, easter eggs // @description:es 🏷️ Traductor Reddit amateur — glassmorphism, 100+ idiomas, Google, MyMemory, DeepL, historial, convertidor de unidades, easter eggs // @description:ko 🏷️ 아마추어 Reddit 번역기 — glassmorphism, 100+ 언어, Google, MyMemory, DeepL, 히스토리, 단위 변환기, 이스터 에그 // @description:pl 🏷️ Amatorski tłumacz Reddit — glassmorphism, 100+ języków, Google, MyMemory, DeepL, historia, konwerter jednostek, easter eggi // @description:tr 🏷️ Amatör Reddit çevirmeni — glassmorphism, 100+ dil, Google, MyMemory, DeepL, geçmiş, birim dönüştürücü, sürprizler // @description:vi 🏷️ Trình dịch Reddit nghiệp dư — glassmorphism, 100+ ngôn ngữ, Google, MyMemory, DeepL, lịch sử, bộ chuyển đổi đơn vị, easter egg // @description:ja 🏷️ アマチュアReddit翻訳者 — glassmorphism, 100以上の言語, Google, MyMemory, DeepL, 履歴, 単位変換器, イースターエッグ // @description:zh-CN 🏷️ 业余Reddit翻译器 — glassmorphism, 100多种语言, Google, MyMemory, DeepL, 历史记录, 单位转换器, 彩蛋 // @description:zh-HK 🏷️ 業餘Reddit翻譯器 — glassmorphism, 100多種語言, Google, MyMemory, DeepL, 歷史記錄, 單位轉換器, 彩蛋 // @description:zh-TW 🏷️ 業餘Reddit翻譯器 — glassmorphism, 100多種語言, Google, MyMemory, DeepL, 歷史記錄, 單位轉換器, 彩蛋 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij4KICA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjI4IiBmaWxsPSIjZmY0NTAwIi8+CiAgPHRleHQgeD0iNjQiIHk9IjkwIiBmb250LXNpemU9IjcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2Vnb2UgVUkgRW1vamksQXBwbGUgQ29sb3IgRW1vamksc2Fucy1zZXJpZiI+8J+MkDwvdGV4dD4KPC9zdmc+ // @author ebayybe // @license MIT // @match https://www.reddit.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect translate.googleapis.com // @connect api.mymemory.translated.net // @connect api.deepl.com // @connect api-free.deepl.com // @downloadURL https://raw.githubusercontent.com/ebayybe/reddit-translator/refs/heads/main/reddit-translator-pro-auto.user.js // @updateURL https://raw.githubusercontent.com/ebayybe/reddit-translator/refs/heads/main/reddit-translator-pro-auto.user.js // ==/UserScript== (function () { 'use strict'; // Unique prefix for this script to avoid conflicts with other scripts const PREFIX = '_x9_'; // ═══════════════════════════════════════════════════════════════════════════ // § КОНФИГУРАЦИЯ // ═══════════════════════════════════════════════════════════════════════════ // Auto-detect UI language from browser/system language const UI_SUPPORTED_LIST = ['ru', 'uk', 'en', 'de', 'fr', 'es', 'pl', 'tr', 'zh', 'ja', 'it', 'pt', 'ko', 'vi', 'ar']; function detectUiLang() { const langs = navigator.languages || [navigator.language || 'en']; for (const lang of langs) { const base = lang.split('-')[0].toLowerCase(); if (UI_SUPPORTED_LIST.includes(base)) return base; } return 'en'; } const AUTO_UI_LANG = detectUiLang(); const DEF = { targetLang: AUTO_UI_LANG, uiLang: AUTO_UI_LANG, engine: 'google', tone: 'normal', theme: 'dark', bilingualMode: true, autoConvert: true, autoScroll: false, incognito: false, requestDelay: 50, totalChars: 0, totalCount: 0, hotkeyPanel: 'F2', customColors: null, autoTranslateOnScroll: true, deeplApiKeys: '', maxRequestsPerSecond: 20, maxTextLengthPerRequest: 1800, maxParagraphsPerRequest: 6, maxConcurrentRequests: 5, }; // Загружаем конфиг const cfg = Object.fromEntries( Object.entries(DEF).map(([k, def]) => [k, GM_getValue(PREFIX + k, def)]) ); function save(key, val) { cfg[key] = val; GM_setValue(PREFIX + key, val); } // Батч-сохранение счётчиков let statTimer = null; function flushStats() { clearTimeout(statTimer); statTimer = setTimeout(() => { GM_setValue(PREFIX + 'totalChars', cfg.totalChars); GM_setValue(PREFIX + 'totalCount', cfg.totalCount); }, 1200); } // ═══════════════════════════════════════════════════════════════════════════ // § КЭШ (TTL 24ч, лимит 600 записей, дебаунс записи) // ═══════════════════════════════════════════════════════════════════════════ const CACHE_KEY = 'rtp_v8_cache'; let cache = {}; (function loadCache() { try { const now = Date.now(); const raw = JSON.parse(GM_getValue(PREFIX + CACHE_KEY, '{}')); const entries = Object.entries(raw) .filter(([, v]) => now - v.ts < 86_400_000) .sort((a, b) => b[1].ts - a[1].ts) .slice(0, 600); entries.forEach(([k, v]) => (cache[k] = v)); } catch { } })(); let cacheTimer = null; function flushCache() { if (cfg.incognito) return; clearTimeout(cacheTimer); cacheTimer = setTimeout(() => { try { GM_setValue(PREFIX + CACHE_KEY, JSON.stringify(cache)); } catch { } }, 2000); } function cacheSet(key, val) { const keys = Object.keys(cache); if (keys.length >= 600) { const oldest = keys.sort((a, b) => cache[a].ts - cache[b].ts)[0]; delete cache[oldest]; } cache[key] = { val, ts: Date.now() }; flushCache(); } // ═══════════════════════════════════════════════════════════════════════════ // § ИСТОРИЯ (50 записей) // ═══════════════════════════════════════════════════════════════════════════ let history = []; try { history = JSON.parse(GM_getValue(PREFIX + 'rtp_v8_history', '[]')); } catch { } function pushHistory(orig, translated, lang) { if (cfg.incognito) return; history.unshift({ orig: orig.slice(0, 130), translated: translated.slice(0, 130), lang, ts: Date.now() }); if (history.length > 50) history.length = 50; GM_setValue(PREFIX + 'rtp_v8_history', JSON.stringify(history)); } // ═══════════════════════════════════════════════════════════════════════════ // § UI — МУЛЬТИЯЗЫЧНОСТЬ ИНТЕРФЕЙСА // ═══════════════════════════════════════════════════════════════════════════ const UI_SUPPORTED = UI_SUPPORTED_LIST; const STRINGS = { ru: { title: 'Reddit Переводчик', ver: 'v1.0.10', tabSettings: '⚙️ Настройки', tabHistory: '📖 История', tabExtras: '✨ Дополнения', secUiLang: 'Язык интерфейса', applyUi: '✨ ПРИМЕНИТЬ ИНТЕРФЕЙС', secTargetLang: 'Язык перевода', saveLang: '💾 СОХРАНИТЬ ЯЗЫК', secEngine: 'Движок', secTone: 'Стиль перевода', secTheme: 'Тема', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Ключи DeepL API', saveDeepLApi: '🔑 СОХРАНИТЬ КЛЮЧИ', btnTestDeepL: '🧪 ПРОВЕРИТЬ', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Вставьте один или несколько ключей DeepL API, разделённых запятыми', deeplApiHelp: 'Поддерживаются Pro и Free ключи. Ключи, оканчивающиеся на :fx, автоматически используют deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Ключи DeepL сохранены', toastDeepLKeysMissing: '⚠️ Сначала добавьте хотя бы один ключ DeepL API', toastDeepLUnsupported: '⚠️ DeepL не поддерживает этот язык перевода:', toastDeepLTesting: '🧪 Проверка DeepL API...', toastDeepLOk: '✅ DeepL API доступен', toastDeepLFail: '❌ DeepL API недоступен', toneNeutral: 'Нейтральный', toneFormal: 'Официальный', toneSlang: 'Разговорный', themeDark: 'Тёмная', themeLight: 'Светлая', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Двуязычный режим', togTts: 'Озвучка (TTS)', togAutoConvert: 'Авто-конвертация единиц', togAutoScroll: 'Авто-скролл к новым', togIncognito: 'Инкогнито (без истории)', togAutoTranslateOnScroll: 'Автоперевод при прокрутке', btnResetPos: '🏠 Сброс позиции', btnClearCache: '🧹 Очистить кэш', btnExport: '📤 Экспорт', btnImport: '📥 Импорт', btnSurprise: '🎲 Случайный язык', btnPirate: '🏴‍☠️ Пиратский', btnYoda: '🧙 Режим Йоды', btnHide: '👁 Скрыть кнопки', btnShow: '👁 Показать кнопки', sliderDelay: 'Задержка запросов', unitMs: 'мс', statTranslations: 'Переводов', statChars: 'Символов', statOnPage: 'На странице', histEmpty: 'История пуста', histClear: '🗑 Очистить историю', searchLang: 'Поиск языка…', btnOrig: '↩ Оригинал', btnCopy: '📋 Копировать', btnSpeak: '🔊', btnRetry: '↺ Ещё раз', copied: '✅ Скопировано!', cacheCleared: '🧹 Кэш очищен', toastApply: '✅ Интерфейс обновлён', toastSave: '💾 Сохранено — перезагрузка…', toastSurprise: '🎲 Язык:', toastPirateOn: '🏴‍☠️ Arrr! Пиратский режим!', toastPirateOff: '🏴‍☠️ Режим выключен', toastYodaOn: '🧙 Включён режим, хммм.', toastYodaOff: '🧙 Выключен он.', toastAutoScrollOn: '✅ Автоперевод при прокрутке включён', toastAutoScrollOff: '⭕ Автоперевод при прокрутке выключен', shortcutHint: 'F2 = панель', secHotkeys: 'Горячие клавиши', hotkeyPanel: 'Открыть панель', hotkeyPress: 'Нажмите клавишу…', hotkeyReset: '↺ Сброс', secColors: 'Цвета темы', colorAcc: 'Акцент', colorTxt: 'Текст', colorBg: 'Фон', colorOk: 'Успех', btnResetColors: '↺ Сброс цветов', secRequestLimits: 'Ограничения запросов', maxConcurrentRequests: 'Макс. одновременных запросов', maxRequestsPerSecond: 'Макс. запросов / сек', maxTextLengthPerRequest: 'Макс. символов / запрос', maxParagraphsPerRequest: 'Макс. абзацев / запрос', btnShowOriginals: '📄 ПОКАЗАТЬ ОРИГИНАЛ', btnShowTranslations: '🌐 ПОКАЗАТЬ ПЕРЕВОД', toastShowingOriginals: '📄 Показан оригинальный текст', toastShowingTranslations: '🌐 Показан переведённый текст', }, uk: { title: 'Reddit Перекладач', ver: 'v1.0.10', tabSettings: '⚙️ Налаштування', tabHistory: '📖 Історія', tabExtras: '✨ Додатково', secUiLang: 'Мова інтерфейсу', applyUi: '✨ ЗАСТОСУВАТИ ІНТЕРФЕЙС', secTargetLang: 'Мова перекладу', saveLang: '💾 ЗБЕРЕГТИ МОВУ', secEngine: 'Рушій', secTone: 'Стиль', secTheme: 'Тема', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Ключі DeepL API', saveDeepLApi: '🔑 ЗБЕРЕГТИ КЛЮЧІ', btnTestDeepL: '🧪 ПЕРЕВІРИТИ', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Вставте один або кілька ключів DeepL API, розділених комами', deeplApiHelp: 'Підтримуються Pro і Free ключі. Ключі, що закінчуються на :fx, автоматично використовують deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Ключі DeepL збережено', toastDeepLKeysMissing: '⚠️ Спочатку додайте хоча б один ключ DeepL API', toastDeepLUnsupported: '⚠️ DeepL не підтримує цю мову перекладу:', toastDeepLTesting: '🧪 Перевірка DeepL API...', toastDeepLOk: '✅ DeepL API доступний', toastDeepLFail: '❌ DeepL API недоступний', toneNeutral: 'Нейтральний', toneFormal: 'Офіційний', toneSlang: 'Розмовний', themeDark: 'Темна', themeLight: 'Світла', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Двомовний режим', togTts: 'Озвучка (TTS)', togAutoConvert: 'Авто-конвертація', togAutoScroll: 'Авто-скрол', togIncognito: 'Інкогніто', togAutoTranslateOnScroll: 'Автопереклад при прокручуванні', btnResetPos: '🏠 Скинути позицію', btnClearCache: '🧹 Очистити кеш', btnExport: '📤 Експорт', btnImport: '📥 Імпорт', btnSurprise: '🎲 Випадкова мова', btnPirate: '🏴‍☠️ Піратський', btnYoda: '🧙 Режим Йоди', btnHide: '👁 Сховати', btnShow: '👁 Показати', sliderDelay: 'Затримка запитів', unitMs: 'мс', statTranslations: 'Перекладів', statChars: 'Символів', statOnPage: 'На сторінці', histEmpty: 'Історія порожня', histClear: '🗑 Очистити', searchLang: 'Пошук мови…', btnOrig: '↩ Оригінал', btnCopy: '📋 Копіювати', btnSpeak: '🔊', btnRetry: '↺ Ще раз', copied: '✅ Скопійовано!', cacheCleared: '🧹 Кеш очищено', toastApply: '✅ Інтерфейс оновлено', toastSave: '💾 Збережено — перезавантаження…', toastSurprise: '🎲 Мова:', toastPirateOn: '🏴‍☠️ Arrr! Піратський режим!', toastPirateOff: '🏴‍☠️ Вимкнено', toastYodaOn: '🧙 Увімкнено режим, хммм.', toastYodaOff: '🧙 Вимкнено.', toastAutoScrollOn: '✅ Автопереклад при прокручуванні увімкнено', toastAutoScrollOff: '⭕ Автопереклад при прокручуванні вимкнено', shortcutHint: 'F2 = панель', secHotkeys: 'Гарячі клавіші', hotkeyPanel: 'Відкрити панель', hotkeyPress: 'Натисніть клавішу…', hotkeyReset: '↺ Скинути', secColors: 'Кольори теми', colorAcc: 'Акцент', colorTxt: 'Текст', colorBg: 'Фон', colorOk: 'Успіх', btnResetColors: '↺ Скинути кольори', secRequestLimits: 'Обмеження запитів', maxConcurrentRequests: 'Макс. одночасних запитів', maxRequestsPerSecond: 'Макс. запитів / сек', maxTextLengthPerRequest: 'Макс. символів / запит', maxParagraphsPerRequest: 'Макс. абзаців / запит', btnShowOriginals: '📄 ПОКАЗАТИ ОРИГІНАЛ', btnShowTranslations: '🌐 ПОКАЗАТИ ПЕРЕКЛАД', toastShowingOriginals: '📄 Показано оригінальний текст', toastShowingTranslations: '🌐 Показано перекладений текст', }, en: { title: 'Reddit Translator', ver: 'v1.0.10', tabSettings: '⚙️ Settings', tabHistory: '📖 History', tabExtras: '✨ Extras', secUiLang: 'UI Language', applyUi: '✨ APPLY INTERFACE', secTargetLang: 'Target language', saveLang: '💾 SAVE LANGUAGE', secEngine: 'Engine', secTone: 'Translation tone', secTheme: 'Theme', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'DeepL API keys', saveDeepLApi: '🔑 SAVE DEEPL KEYS', btnTestDeepL: '🧪 TEST DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Paste one or more DeepL API keys, separated by commas', deeplApiHelp: 'Supports both Pro and Free keys. Keys ending with :fx use deepl.com/pro-api automatically.', toastDeepLKeysSaved: '🔑 DeepL keys saved', toastDeepLKeysMissing: '⚠️ Add at least one DeepL API key first', toastDeepLUnsupported: '⚠️ DeepL does not support this target language:', toastDeepLTesting: '🧪 Testing DeepL API...', toastDeepLOk: '✅ DeepL API is available', toastDeepLFail: '❌ DeepL API is unavailable', toneNeutral: 'Neutral', toneFormal: 'Formal', toneSlang: 'Casual', themeDark: 'Dark', themeLight: 'Light', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Bilingual mode', togTts: 'Text-to-Speech', togAutoConvert: 'Auto-convert units', togAutoScroll: 'Auto-scroll to new', togIncognito: 'Incognito (no history)', togAutoTranslateOnScroll: 'Auto-translate on scroll', btnResetPos: '🏠 Reset position', btnClearCache: '🧹 Clear cache', btnExport: '📤 Export', btnImport: '📥 Import', btnSurprise: '🎲 Surprise me', btnPirate: '🏴‍☠️ Pirate mode', btnYoda: '🧙 Yoda mode', btnHide: '👁 Hide buttons', btnShow: '👁 Show buttons', sliderDelay: 'Request delay', unitMs: 'ms', statTranslations: 'Translations', statChars: 'Characters', statOnPage: 'On page', histEmpty: 'No history yet', histClear: '🗑 Clear history', searchLang: 'Search language…', btnOrig: '↩ Original', btnCopy: '📋 Copy', btnSpeak: '🔊', btnRetry: '↺ Retry', copied: '✅ Copied!', cacheCleared: '🧹 Cache cleared', toastApply: '✅ Interface updated', toastSave: '💾 Saved — reloading…', toastSurprise: '🎲 Language:', toastPirateOn: '🏴‍☠️ Arrr! Pirate mode on!', toastPirateOff: '🏴‍☠️ Pirate mode off', toastYodaOn: '🧙 Yoda mode on, hmm.', toastYodaOff: '🧙 Yoda mode off.', toastAutoScrollOn: '✅ Auto-translate on scroll enabled', toastAutoScrollOff: '⭕ Auto-translate on scroll disabled', shortcutHint: 'F2 = panel', secHotkeys: 'Hotkeys', hotkeyPanel: 'Open panel', hotkeyPress: 'Press a key…', hotkeyReset: '↺ Reset', secColors: 'Theme colors', colorAcc: 'Accent', colorTxt: 'Text', colorBg: 'Background', colorOk: 'Success', btnResetColors: '↺ Reset colors', secRequestLimits: 'Request limits', maxConcurrentRequests: 'Max concurrent requests', maxRequestsPerSecond: 'Max requests / sec', maxTextLengthPerRequest: 'Max chars / request', maxParagraphsPerRequest: 'Max paragraphs / request', btnShowOriginals: '📄 SHOW ORIGINAL', btnShowTranslations: '🌐 SHOW TRANSLATION', toastShowingOriginals: '📄 Showing original text', toastShowingTranslations: '🌐 Showing translated text', }, de: { title: 'Reddit Übersetzer', ver: 'v1.0.10', tabSettings: '⚙️ Einstellungen', tabHistory: '📖 Verlauf', tabExtras: '✨ Extras', secUiLang: 'UI-Sprache', applyUi: '✨ INTERFACE ANWENDEN', secTargetLang: 'Zielsprache', saveLang: '💾 SPRACHE SPEICHERN', secEngine: 'Motor', secTone: 'Übersetzungsstil', secTheme: 'Thema', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'DeepL API-Schlüssel', saveDeepLApi: '🔑 DEEPL SPEICHERN', btnTestDeepL: '🧪 DEEPL TESTEN', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Einen oder mehrere DeepL API-Schlüssel einfügen, durch Kommas getrennt', deeplApiHelp: 'Unterstützt Pro- und Free-Schlüssel. Schlüssel, die auf :fx enden, verwenden automatisch deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 DeepL-Schlüssel gespeichert', toastDeepLKeysMissing: '⚠️ Bitte zuerst mindestens einen DeepL API-Schlüssel hinzufügen', toastDeepLUnsupported: '⚠️ DeepL unterstützt diese Zielsprache nicht:', toastDeepLTesting: '🧪 DeepL API wird getestet...', toastDeepLOk: '✅ DeepL API ist verfügbar', toastDeepLFail: '❌ DeepL API ist nicht verfügbar', toneNeutral: 'Neutral', toneFormal: 'Formell', toneSlang: 'Umgangssprachlich', themeDark: 'Dunkel', themeLight: 'Hell', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Zweisprachig', togTts: 'Sprachausgabe', togAutoConvert: 'Einheiten konvertieren', togAutoScroll: 'Auto-Scrollen', togIncognito: 'Inkognito', togAutoTranslateOnScroll: 'Automatisch beim Scrollen übersetzen', btnResetPos: '🏠 Position reset', btnClearCache: '🧹 Cache leeren', btnExport: '📤 Exportieren', btnImport: '📥 Importieren', btnSurprise: '🎲 Überrasch mich', btnPirate: '🏴‍☠️ Piraten-Modus', btnYoda: '🧙 Yoda-Modus', btnHide: '👁 Ausblenden', btnShow: '👁 Anzeigen', sliderDelay: 'Anfrageverzögerung', unitMs: 'ms', statTranslations: 'Übersetzungen', statChars: 'Zeichen', statOnPage: 'Auf Seite', histEmpty: 'Kein Verlauf', histClear: '🗑 Verlauf löschen', searchLang: 'Sprache suchen…', btnOrig: '↩ Original', btnCopy: '📋 Kopieren', btnSpeak: '🔊', btnRetry: '↺ Nochmal', copied: '✅ Kopiert!', cacheCleared: '🧹 Cache geleert', toastApply: '✅ Interface aktualisiert', toastSave: '💾 Gespeichert — neu laden…', toastSurprise: '🎲 Sprache:', toastPirateOn: '🏴‍☠️ Arrr! Piraten-Modus!', toastPirateOff: '🏴‍☠️ Modus aus', toastYodaOn: '🧙 Yoda-Modus an, hmm.', toastYodaOff: '🧙 Modus aus.', toastAutoScrollOn: '✅ Automatisches Übersetzen beim Scrollen aktiviert', toastAutoScrollOff: '⭕ Automatisches Übersetzen beim Scrollen deaktiviert', shortcutHint: 'F2 = Panel', secHotkeys: 'Tastenkürzel', hotkeyPanel: 'Panel öffnen', hotkeyPress: 'Taste drücken…', hotkeyReset: '↺ Zurücksetzen', secColors: 'Themenfarben', colorAcc: 'Akzent', colorTxt: 'Text', colorBg: 'Hintergrund', colorOk: 'Erfolg', btnResetColors: '↺ Farben zurücksetzen', secRequestLimits: 'Anfragelimits', maxConcurrentRequests: 'Max. gleichzeitige Anfragen', maxRequestsPerSecond: 'Max. Anfragen / Sek.', maxTextLengthPerRequest: 'Max. Zeichen / Anfrage', maxParagraphsPerRequest: 'Max. Absätze / Anfrage', btnShowOriginals: '📄 ORIGINAL ANZEIGEN', btnShowTranslations: '🌐 ÜBERSETZUNG ANZEIGEN', toastShowingOriginals: '📄 Originaltext wird angezeigt', toastShowingTranslations: '🌐 Übersetzung wird angezeigt', }, fr: { title: 'Traducteur Reddit', ver: 'v1.0.10', tabSettings: '⚙️ Paramètres', tabHistory: '📖 Historique', tabExtras: '✨ Extras', secUiLang: 'Langue UI', applyUi: '✨ APPLIQUER INTERFACE', secTargetLang: 'Langue cible', saveLang: '💾 ENREGISTRER', secEngine: 'Moteur', secTone: 'Style', secTheme: 'Thème', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Clés API DeepL', saveDeepLApi: '🔑 ENREGISTRER DEEPL', btnTestDeepL: '🧪 TESTER DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Collez une ou plusieurs clés API DeepL, séparées par des virgules', deeplApiHelp: 'Prend en charge les clés Pro et Free. Les clés se terminant par :fx utilisent automatiquement deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Clés DeepL enregistrées', toastDeepLKeysMissing: '⚠️ Ajoutez d\'abord au moins une clé API DeepL', toastDeepLUnsupported: '⚠️ DeepL ne prend pas en charge cette langue cible :', toastDeepLTesting: '🧪 Test de l\'API DeepL...', toastDeepLOk: '✅ API DeepL disponible', toastDeepLFail: '❌ API DeepL indisponible', toneNeutral: 'Neutre', toneFormal: 'Formel', toneSlang: 'Familier', themeDark: 'Sombre', themeLight: 'Clair', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Mode bilingue', togTts: 'Synthèse vocale', togAutoConvert: 'Conversion auto', togAutoScroll: 'Défilement auto', togIncognito: 'Incognito', togAutoTranslateOnScroll: 'Traduction automatique au défilement', btnResetPos: '🏠 Réinitialiser', btnClearCache: '🧹 Vider cache', btnExport: '📤 Exporter', btnImport: '📥 Importer', btnSurprise: '🎲 Surprends-moi', btnPirate: '🏴‍☠️ Mode Pirate', btnYoda: '🧙 Mode Yoda', btnHide: '👁 Masquer', btnShow: '👁 Afficher', sliderDelay: 'Délai de requête', unitMs: 'ms', statTranslations: 'Traductions', statChars: 'Caractères', statOnPage: 'Sur page', histEmpty: 'Historique vide', histClear: '🗑 Effacer', searchLang: 'Chercher langue…', btnOrig: '↩ Original', btnCopy: '📋 Copier', btnSpeak: '🔊', btnRetry: '↺ Réessayer', copied: '✅ Copié!', cacheCleared: '🧹 Cache vidé', toastApply: '✅ Interface mise à jour', toastSave: '💾 Sauvegardé — rechargement…', toastSurprise: '🎲 Langue:', toastPirateOn: '🏴‍☠️ Arrr! Mode Pirate!', toastPirateOff: '🏴‍☠️ Mode désactivé', toastYodaOn: '🧙 Mode Yoda activé, hmm.', toastYodaOff: '🧙 Mode Yoda désactivé.', toastAutoScrollOn: '✅ Traduction automatique au défilement activée', toastAutoScrollOff: '⭕ Traduction automatique au défilement désactivée', shortcutHint: 'F2 = panneau', secHotkeys: 'Raccourcis', hotkeyPanel: 'Ouvrir panneau', hotkeyPress: 'Appuyez sur une touche…', hotkeyReset: '↺ Réinitialiser', secColors: 'Couleurs du thème', colorAcc: 'Accent', colorTxt: 'Texte', colorBg: 'Fond', colorOk: 'Succès', btnResetColors: '↺ Réinitialiser couleurs', secRequestLimits: 'Limites de requêtes', maxConcurrentRequests: 'Max. requêtes simultanées', maxRequestsPerSecond: 'Max. requêtes / sec', maxTextLengthPerRequest: 'Max. caractères / requête', maxParagraphsPerRequest: 'Max. paragraphes / requête', btnShowOriginals: '📄 AFFICHER L\'ORIGINAL', btnShowTranslations: '🌐 AFFICHER LA TRADUCTION', toastShowingOriginals: '📄 Texte original affiché', toastShowingTranslations: '🌐 Traduction affichée', }, es: { title: 'Traductor Reddit', ver: 'v1.0.10', tabSettings: '⚙️ Config', tabHistory: '📖 Historial', tabExtras: '✨ Extras', secUiLang: 'Idioma UI', applyUi: '✨ APLICAR INTERFAZ', secTargetLang: 'Idioma destino', saveLang: '💾 GUARDAR IDIOMA', secEngine: 'Motor', secTone: 'Estilo', secTheme: 'Tema', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Claves API de DeepL', saveDeepLApi: '🔑 GUARDAR DEEPL', btnTestDeepL: '🧪 PROBAR DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Pegue una o más claves API de DeepL, separadas por comas', deeplApiHelp: 'Compatible con claves Pro y Free. Las claves que terminan en :fx usan automáticamente deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Claves DeepL guardadas', toastDeepLKeysMissing: '⚠️ Primero añada al menos una clave API de DeepL', toastDeepLUnsupported: '⚠️ DeepL no admite este idioma de destino:', toastDeepLTesting: '🧪 Probando API de DeepL...', toastDeepLOk: '✅ API de DeepL disponible', toastDeepLFail: '❌ API de DeepL no disponible', toneNeutral: 'Neutral', toneFormal: 'Formal', toneSlang: 'Coloquial', themeDark: 'Oscuro', themeLight: 'Claro', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Modo bilingüe', togTts: 'Texto a voz', togAutoConvert: 'Convertir unidades', togAutoScroll: 'Auto-desplazamiento', togIncognito: 'Incógnito', togAutoTranslateOnScroll: 'Traducción automática al desplazar', btnResetPos: '🏠 Resetear', btnClearCache: '🧹 Limpiar caché', btnExport: '📤 Exportar', btnImport: '📥 Importar', btnSurprise: '🎲 Sorpréndeme', btnPirate: '🏴‍☠️ Modo Pirata', btnYoda: '🧙 Modo Yoda', btnHide: '👁 Ocultar', btnShow: '👁 Mostrar', sliderDelay: 'Retraso de solicitud', unitMs: 'ms', statTranslations: 'Traducciones', statChars: 'Caracteres', statOnPage: 'En página', histEmpty: 'Sin historial', histClear: '🗑 Borrar', searchLang: 'Buscar idioma…', btnOrig: '↩ Original', btnCopy: '📋 Copiar', btnSpeak: '🔊', btnRetry: '↺ Reintentar', copied: '✅ ¡Copiado!', cacheCleared: '🧹 Caché limpiado', toastApply: '✅ Interfaz actualizada', toastSave: '💾 Guardado — recargando…', toastSurprise: '🎲 Idioma:', toastPirateOn: '🏴‍☠️ ¡Arrr! ¡Modo Pirata!', toastPirateOff: '🏴‍☠️ Modo desactivado', toastYodaOn: '🧙 Modo Yoda activado, hmm.', toastYodaOff: '🧙 Modo Yoda desactivado.', toastAutoScrollOn: '✅ Traducción automática al desplazar activada', toastAutoScrollOff: '⭕ Traducción automática al desplazar desactivada', shortcutHint: 'F2 = panel', secHotkeys: 'Atajos', hotkeyPanel: 'Abrir panel', hotkeyPress: 'Presiona una tecla…', hotkeyReset: '↺ Restablecer', secColors: 'Colores del tema', colorAcc: 'Acento', colorTxt: 'Texto', colorBg: 'Fondo', colorOk: 'Éxito', btnResetColors: '↺ Restablecer colores', secRequestLimits: 'Límites de solicitudes', maxConcurrentRequests: 'Máx. solicitudes simultáneas', maxRequestsPerSecond: 'Máx. solicitudes / seg', maxTextLengthPerRequest: 'Máx. caracteres / solicitud', maxParagraphsPerRequest: 'Máx. párrafos / solicitud', btnShowOriginals: '📄 MOSTRAR ORIGINAL', btnShowTranslations: '🌐 MOSTRAR TRADUCCIÓN', toastShowingOriginals: '📄 Mostrando texto original', toastShowingTranslations: '🌐 Mostrando traducción', }, pl: { title: 'Tłumacz Reddit', ver: 'v1.0.10', tabSettings: '⚙️ Ustawienia', tabHistory: '📖 Historia', tabExtras: '✨ Extras', secUiLang: 'Język interfejsu', applyUi: '✨ ZASTOSUJ INTERFEJS', secTargetLang: 'Język docelowy', saveLang: '💾 ZAPISZ JĘZYK', secEngine: 'Silnik', secTone: 'Styl', secTheme: 'Motyw', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Klucze API DeepL', saveDeepLApi: '🔑 ZAPISZ KLUCZE DEEPL', btnTestDeepL: '🧪 TESTUJ DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Wklej jeden lub więcej kluczy API DeepL, oddzielonych przecinkami', deeplApiHelp: 'Obsługuje klucze Pro i Free. Klucze kończące się na :fx automatycznie używają deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Klucze DeepL zapisane', toastDeepLKeysMissing: '⚠️ Najpierw dodaj co najmniej jeden klucz API DeepL', toastDeepLUnsupported: '⚠️ DeepL nie obsługuje tego języka docelowego:', toastDeepLTesting: '🧪 Testowanie API DeepL...', toastDeepLOk: '✅ API DeepL dostępne', toastDeepLFail: '❌ API DeepL niedostępne', toneNeutral: 'Neutralny', toneFormal: 'Formalny', toneSlang: 'Potoczny', themeDark: 'Ciemny', themeLight: 'Jasny', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Tryb dwujęzyczny', togTts: 'Mowa syntetyczna', togAutoConvert: 'Auto-konwersja', togAutoScroll: 'Auto-przewijanie', togIncognito: 'Incognito', togAutoTranslateOnScroll: 'Automatyczne tłumaczenie podczas przewijania', btnResetPos: '🏠 Resetuj pozycję', btnClearCache: '🧹 Wyczyść cache', btnExport: '📤 Eksport', btnImport: '📥 Import', btnSurprise: '🎲 Losowy język', btnPirate: '🏴‍☠️ Tryb Pirata', btnYoda: '🧙 Tryb Yody', btnHide: '👁 Ukryj', btnShow: '👁 Pokaż', sliderDelay: 'Opóźnienie żądania', unitMs: 'ms', statTranslations: 'Tłumaczenia', statChars: 'Znaki', statOnPage: 'Na stronie', histEmpty: 'Brak historii', histClear: '🗑 Wyczyść', searchLang: 'Szukaj języka…', btnOrig: '↩ Oryginał', btnCopy: '📋 Kopiuj', btnSpeak: '🔊', btnRetry: '↺ Ponów', copied: '✅ Skopiowano!', cacheCleared: '🧹 Cache wyczyszczony', toastApply: '✅ Interfejs zaktualizowany', toastSave: '💾 Zapisano — przeładowanie…', toastSurprise: '🎲 Język:', toastPirateOn: '🏴‍☠️ Arrr! Tryb Pirata!', toastPirateOff: '🏴‍☠️ Tryb wyłączony', toastYodaOn: '🧙 Tryb Yody włączony, hmm.', toastYodaOff: '🧙 Tryb wyłączony.', toastAutoScrollOn: '✅ Automatyczne tłumaczenie podczas przewijania włączone', toastAutoScrollOff: '⭕ Automatyczne tłumaczenie podczas przewijania wyłączone', shortcutHint: 'F2 = panel', secHotkeys: 'Skróty klawiszowe', hotkeyPanel: 'Otwórz panel', hotkeyPress: 'Naciśnij klawisz…', hotkeyReset: '↺ Resetuj', secColors: 'Kolory motywu', colorAcc: 'Akcent', colorTxt: 'Tekst', colorBg: 'Tło', colorOk: 'Sukces', btnResetColors: '↺ Resetuj kolory', secRequestLimits: 'Limity żądań', maxConcurrentRequests: 'Maks. równoległych żądań', maxRequestsPerSecond: 'Maks. żądań / sek', maxTextLengthPerRequest: 'Maks. znaków / żądanie', maxParagraphsPerRequest: 'Maks. akapitów / żądanie', btnShowOriginals: '📄 POKAŻ ORYGINAŁ', btnShowTranslations: '🌐 POKAŻ TŁUMACZENIE', toastShowingOriginals: '📄 Wyświetlany tekst oryginalny', toastShowingTranslations: '🌐 Wyświetlane tłumaczenie', }, tr: { title: 'Reddit Çevirmeni', ver: 'v1.0.10', tabSettings: '⚙️ Ayarlar', tabHistory: '📖 Geçmiş', tabExtras: '✨ Ekstra', secUiLang: 'Arayüz dili', applyUi: '✨ ARAYÜZÜ UYGULA', secTargetLang: 'Hedef dil', saveLang: '💾 DİLİ KAYDET', secEngine: 'Motor', secTone: 'Stil', secTheme: 'Tema', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'DeepL API Anahtarları', saveDeepLApi: '🔑 DEEPL KAYDET', btnTestDeepL: '🧪 DEEPL TEST ET', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Virgülle ayrılmış bir veya daha fazla DeepL API anahtarı yapıştırın', deeplApiHelp: 'Pro ve Free anahtarları destekler. :fx ile biten anahtarlar otomatik olarak deepl.com/pro-api kullanır.', toastDeepLKeysSaved: '🔑 DeepL anahtarları kaydedildi', toastDeepLKeysMissing: '⚠️ Önce en az bir DeepL API anahtarı ekleyin', toastDeepLUnsupported: '⚠️ DeepL bu hedef dili desteklemiyor:', toastDeepLTesting: '🧪 DeepL API test ediliyor...', toastDeepLOk: '✅ DeepL API kullanılabilir', toastDeepLFail: '❌ DeepL API kullanılamıyor', toneNeutral: 'Nötr', toneFormal: 'Resmi', toneSlang: 'Günlük', themeDark: 'Koyu', themeLight: 'Açık', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Çift dil modu', togTts: 'Metin okuma', togAutoConvert: 'Otomatik dönüştürme', togAutoScroll: 'Otomatik kaydırma', togIncognito: 'Gizli mod', togAutoTranslateOnScroll: 'Kaydırırken otomatik çeviri', btnResetPos: '🏠 Konumu sıfırla', btnClearCache: '🧹 Önbelleği temizle', btnExport: '📤 Dışa aktar', btnImport: '📥 İçe aktar', btnSurprise: '🎲 Rastgele', btnPirate: '🏴‍☠️ Korsan modu', btnYoda: '🧙 Yoda modu', btnHide: '👁 Gizle', btnShow: '👁 Göster', sliderDelay: 'İstek gecikmesi', unitMs: 'ms', statTranslations: 'Çeviriler', statChars: 'Karakterler', statOnPage: 'Sayfada', histEmpty: 'Geçmiş yok', histClear: '🗑 Temizle', searchLang: 'Dil ara…', btnOrig: '↩ Orijinal', btnCopy: '📋 Kopyala', btnSpeak: '🔊', btnRetry: '↺ Tekrar', copied: '✅ Kopyalandı!', cacheCleared: '🧹 Önbellek temizlendi', toastApply: '✅ Arayüz güncellendi', toastSave: '💾 Kaydedildi — yenileniyor…', toastSurprise: '🎲 Dil:', toastPirateOn: '🏴‍☠️ Arrr! Korsan modu!', toastPirateOff: '🏴‍☠️ Mod kapatıldı', toastYodaOn: '🧙 Yoda modu açık, hmm.', toastYodaOff: '🧙 Yoda modu kapalı.', toastAutoScrollOn: '✅ Kaydırırken otomatik çeviri etkin', toastAutoScrollOff: '⭕ Kaydırırken otomatik çeviri devre dışı', shortcutHint: 'F2 = panel', secHotkeys: 'Kısayollar', hotkeyPanel: 'Paneli aç', hotkeyPress: 'Bir tuşa basın…', hotkeyReset: '↺ Sıfırla', secColors: 'Tema renkleri', colorAcc: 'Vurgu', colorTxt: 'Metin', colorBg: 'Arka plan', colorOk: 'Başarı', btnResetColors: '↺ Renkleri sıfırla', secRequestLimits: 'İstek limitleri', maxConcurrentRequests: 'Maks. eşzamanlı istek', maxRequestsPerSecond: 'Maks. istek / sn', maxTextLengthPerRequest: 'Maks. karakter / istek', maxParagraphsPerRequest: 'Maks. paragraf / istek', btnShowOriginals: '📄 ORİJİNALİ GÖSTER', btnShowTranslations: '🌐 ÇEVİRİYİ GÖSTER', toastShowingOriginals: '📄 Orijinal metin gösteriliyor', toastShowingTranslations: '🌐 Çeviri gösteriliyor', }, zh: { title: 'Reddit翻译器', ver: 'v1.0.10', tabSettings: '⚙️ 设置', tabHistory: '📖 历史', tabExtras: '✨ 更多', secUiLang: '界面语言', applyUi: '✨ 应用界面', secTargetLang: '目标语言', saveLang: '💾 保存语言', secEngine: '引擎', secTone: '风格', secTheme: '主题', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'DeepL API 密钥', saveDeepLApi: '🔑 保存 DeepL 密钥', btnTestDeepL: '🧪 测试 DeepL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: '粘贴一个或多个 DeepL API 密钥,使用英文逗号分隔', deeplApiHelp: '同时支持 Pro 和 Free 密钥。以 :fx 结尾的密钥会自动使用 deepl.com/pro-api。', toastDeepLKeysSaved: '🔑 DeepL 密钥已保存', toastDeepLKeysMissing: '⚠️ 请先添加至少一个 DeepL API 密钥', toastDeepLUnsupported: '⚠️ DeepL 暂不支持该目标语言:', toastDeepLTesting: '🧪 正在测试 DeepL API...', toastDeepLOk: '✅ DeepL API 可用', toastDeepLFail: '❌ DeepL API 不可用', toneNeutral: '中性', toneFormal: '正式', toneSlang: '口语', themeDark: '深色', themeLight: '浅色', themeCyber: '赛博朋克', themeDracula: '德古拉', togBilingual: '双语模式', togTts: '文字转语音', togAutoConvert: '自动单位转换', togAutoScroll: '自动滚动', togIncognito: '隐身模式', togAutoTranslateOnScroll: '滚动时自动翻译', btnResetPos: '🏠 重置位置', btnClearCache: '🧹 清除缓存', btnExport: '📤 导出', btnImport: '📥 导入', btnSurprise: '🎲 随机语言', btnPirate: '🏴‍☠️ 海盗模式', btnYoda: '🧙 尤达模式', btnHide: '👁 隐藏', btnShow: '👁 显示', sliderDelay: '请求延迟', unitMs: '毫秒', statTranslations: '翻译', statChars: '字符', statOnPage: '页面上', histEmpty: '暂无历史', histClear: '🗑 清除历史', searchLang: '搜索语言…', btnOrig: '↩ 原文', btnCopy: '📋 复制', btnSpeak: '🔊', btnRetry: '↺ 重试', copied: '✅ 已复制!', cacheCleared: '🧹 缓存已清除', toastApply: '✅ 界面已更新', toastSave: '💾 已保存 — 重新加载…', toastSurprise: '🎲 语言:', toastPirateOn: '🏴‍☠️ Arrr! 海盗模式!', toastPirateOff: '🏴‍☠️ 模式关闭', toastYodaOn: '🧙 尤达模式已开启,嗯。', toastYodaOff: '🧙 尤达模式已关闭。', toastAutoScrollOn: '✅ 滚动时自动翻译已启用', toastAutoScrollOff: '⭕ 滚动时自动翻译已禁用', shortcutHint: 'F2 = 面板', secHotkeys: '快捷键', hotkeyPanel: '打开面板', hotkeyPress: '按下一个键…', hotkeyReset: '↺ 重置', secColors: '主题颜色', colorAcc: '强调色', colorTxt: '文字', colorBg: '背景', colorOk: '成功', btnResetColors: '↺ 重置颜色', secRequestLimits: '请求限制', maxConcurrentRequests: '最大并发请求数', maxRequestsPerSecond: '每秒最大请求数', maxTextLengthPerRequest: '每次请求最大文本长度', maxParagraphsPerRequest: '每次请求最大段落数', btnShowOriginals: '📄 显示原文', btnShowTranslations: '🌐 显示译文', toastShowingOriginals: '📄 当前显示原文', toastShowingTranslations: '🌐 当前显示译文', }, ja: { title: 'Reddit翻訳', ver: 'v1.0.10', tabSettings: '⚙️ 設定', tabHistory: '📖 履歴', tabExtras: '✨ その他', secUiLang: 'UI言語', applyUi: '✨ UIを適用', secTargetLang: '翻訳先言語', saveLang: '💾 言語を保存', secEngine: 'エンジン', secTone: 'スタイル', secTheme: 'テーマ', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'DeepL APIキー', saveDeepLApi: '🔑 DEEPL キーを保存', btnTestDeepL: '🧪 DEEPL テスト', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'DeepL APIキーをカンマ区切りで1つ以上貼り付けてください', deeplApiHelp: 'ProおよびFreeキーに対応。:fx で終わるキーは自動的に deepl.com/pro-api を使用します。', toastDeepLKeysSaved: '🔑 DeepLキーを保存しました', toastDeepLKeysMissing: '⚠️ まずDeepL APIキーを1つ以上追加してください', toastDeepLUnsupported: '⚠️ DeepLはこのターゲット言語に対応していません:', toastDeepLTesting: '🧪 DeepL APIをテスト中...', toastDeepLOk: '✅ DeepL APIは利用可能です', toastDeepLFail: '❌ DeepL APIは利用できません', toneNeutral: '標準', toneFormal: '公式', toneSlang: 'くだけた', themeDark: 'ダーク', themeLight: 'ライト', themeCyber: 'サイバーパンク', themeDracula: 'ドラキュラ', togBilingual: 'バイリンガルモード', togTts: '音声合成', togAutoConvert: '単位自動変換', togAutoScroll: '自動スクロール', togIncognito: 'シークレット', togAutoTranslateOnScroll: 'スクロール時に自動翻訳', btnResetPos: '🏠 位置リセット', btnClearCache: '🧹 キャッシュ削除', btnExport: '📤 エクスポート', btnImport: '📥 インポート', btnSurprise: '🎲 ランダム言語', btnPirate: '🏴‍☠️ 海賊モード', btnYoda: '🧙 ヨーダモード', btnHide: '👁 ボタンを隠す', btnShow: '👁 ボタンを表示', sliderDelay: 'リクエスト遅延', unitMs: 'ms', statTranslations: '翻訳数', statChars: '文字数', statOnPage: 'ページ上', histEmpty: '履歴なし', histClear: '🗑 履歴を消去', searchLang: '言語を検索…', btnOrig: '↩ 元テキスト', btnCopy: '📋 コピー', btnSpeak: '🔊', btnRetry: '↺ やり直し', copied: '✅ コピーしました!', cacheCleared: '🧹 キャッシュ削除済み', toastApply: '✅ UIを更新しました', toastSave: '💾 保存しました — 再読込中…', toastSurprise: '🎲 言語:', toastPirateOn: '🏴‍☠️ Arrr! 海賊モード!', toastPirateOff: '🏴‍☠️ モードオフ', toastYodaOn: '🧙 ヨーダモードオン、ふむ。', toastYodaOff: '🧙 ヨーダモードオフ。', toastAutoScrollOn: '✅ スクロール時に自動翻訳が有効', toastAutoScrollOff: '⭕ スクロール時に自動翻訳が無効', shortcutHint: 'F2 = パネル', secHotkeys: 'ショートカット', hotkeyPanel: 'パネルを開く', hotkeyPress: 'キーを押してください…', hotkeyReset: '↺ リセット', secColors: 'テーマカラー', colorAcc: 'アクセント', colorTxt: 'テキスト', colorBg: '背景', colorOk: '成功', btnResetColors: '↺ 色をリセット', secRequestLimits: 'リクエスト制限', maxConcurrentRequests: '最大同時リクエスト数', maxRequestsPerSecond: '最大リクエスト数 / 秒', maxTextLengthPerRequest: '最大文字数 / リクエスト', maxParagraphsPerRequest: '最大段落数 / リクエスト', btnShowOriginals: '📄 原文を表示', btnShowTranslations: '🌐 翻訳を表示', toastShowingOriginals: '🌐 翻訳を表示中', toastShowingTranslations: '🌐 翻訳を表示中', }, it: { title: 'Reddit Traduttore', ver: 'v1.0.10', tabSettings: '⚙️ Impostazioni', tabHistory: '📖 Cronologia', tabExtras: '✨ Extra', secUiLang: 'Lingua interfaccia', applyUi: '✨ APPLICA INTERFACCIA', secTargetLang: 'Lingua di destinazione', saveLang: '💾 SALVA LINGUA', secEngine: 'Motore', secTone: 'Tono di traduzione', secTheme: 'Tema', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Chiavi API DeepL', saveDeepLApi: '🔑 SALVA CHIAVI DEEPL', btnTestDeepL: '🧪 TESTA DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Incolla una o più chiavi API DeepL, separate da virgole', deeplApiHelp: 'Supporta chiavi Pro e Free. Le chiavi che terminano con :fx usano automaticamente deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Chiavi DeepL salvate', toastDeepLKeysMissing: '⚠️ Aggiungi prima almeno una chiave API DeepL', toastDeepLUnsupported: '⚠️ DeepL non supporta questa lingua di destinazione:', toastDeepLTesting: '🧪 Test API DeepL in corso...', toastDeepLOk: '✅ API DeepL disponibile', toastDeepLFail: '❌ API DeepL non disponibile', toneNeutral: 'Neutro', toneFormal: 'Formale', toneSlang: 'Colloquiale', themeDark: 'Scuro', themeLight: 'Chiaro', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Modalità bilingue', togTts: 'Sintesi vocale', togAutoConvert: 'Conversione unità automatica', togAutoScroll: 'Scorri automaticamente', togIncognito: 'Incognito (nessuna cronologia)', togAutoTranslateOnScroll: 'Traduci automaticamente allo scroll', btnResetPos: '🏠 Ripristina posizione', btnClearCache: '🧹 Svuota cache', btnExport: '📤 Esporta', btnImport: '📥 Importa', btnSurprise: '🎲 Sorprendimi', btnPirate: '🏴‍☠️ Modalità pirata', btnYoda: '🧙 Modalità Yoda', btnHide: '👁 Nascondi pulsanti', btnShow: '👁 Mostra pulsanti', sliderDelay: 'Ritardo richieste', unitMs: 'ms', statTranslations: 'Traduzioni', statChars: 'Caratteri', statOnPage: 'In pagina', histEmpty: 'Nessuna cronologia', histClear: '🗑 Cancella cronologia', searchLang: 'Cerca lingua…', btnOrig: '↩ Originale', btnCopy: '📋 Copia', btnSpeak: '🔊', btnRetry: '↺ Riprova', copied: '✅ Copiato!', cacheCleared: '🧹 Cache svuotata', toastApply: '✅ Interfaccia aggiornata', toastSave: '💾 Salvato — ricaricamento…', toastSurprise: '🎲 Lingua:', toastPirateOn: '🏴‍☠️ Arrr! Modalità pirata attiva!', toastPirateOff: '🏴‍☠️ Modalità pirata disattivata', toastYodaOn: '🧙 Modalità Yoda attiva, hmm.', toastYodaOff: '🧙 Modalità Yoda disattivata.', toastAutoScrollOn: '✅ Traduzione automatica allo scroll attiva', toastAutoScrollOff: '⭕ Traduzione automatica allo scroll disattivata', shortcutHint: 'F2 = pannello', secHotkeys: 'Tasti rapidi', hotkeyPanel: 'Apri pannello', hotkeyPress: 'Premi un tasto…', hotkeyReset: '↺ Reimposta', secColors: 'Colori tema', colorAcc: 'Accento', colorTxt: 'Testo', colorBg: 'Sfondo', colorOk: 'Successo', btnResetColors: '↺ Reimposta colori', secRequestLimits: 'Limiti richieste', maxConcurrentRequests: 'Max richieste simultanee', maxRequestsPerSecond: 'Max richieste / sec', maxTextLengthPerRequest: 'Max caratteri / richiesta', maxParagraphsPerRequest: 'Max paragrafi / richiesta', btnShowOriginals: '📄 MOSTRA ORIGINALE', btnShowTranslations: '🌐 MOSTRA TRADUZIONE', toastShowingOriginals: '📄 Testo originale visualizzato', toastShowingTranslations: '🌐 Traduzione visualizzata', }, pt: { title: 'Reddit Tradutor', ver: 'v1.0.10', tabSettings: '⚙️ Configurações', tabHistory: '📖 Histórico', tabExtras: '✨ Extras', secUiLang: 'Idioma da interface', applyUi: '✨ APLICAR INTERFACE', secTargetLang: 'Idioma de destino', saveLang: '💾 SALVAR IDIOMA', secEngine: 'Motor', secTone: 'Tom de tradução', secTheme: 'Tema', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Chaves API DeepL', saveDeepLApi: '🔑 SALVAR CHAVES DEEPL', btnTestDeepL: '🧪 TESTAR DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Cole uma ou mais chaves API DeepL, separadas por vírgulas', deeplApiHelp: 'Suporta chaves Pro e Free. Chaves terminadas em :fx usam automaticamente deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Chaves DeepL salvas', toastDeepLKeysMissing: '⚠️ Adicione pelo menos uma chave API DeepL primeiro', toastDeepLUnsupported: '⚠️ DeepL não suporta este idioma de destino:', toastDeepLTesting: '🧪 Testando API DeepL...', toastDeepLOk: '✅ API DeepL disponível', toastDeepLFail: '❌ API DeepL indisponível', toneNeutral: 'Neutro', toneFormal: 'Formal', toneSlang: 'Informal', themeDark: 'Escuro', themeLight: 'Claro', themeCyber: 'Cyberpunk', themeDracula: 'Drácula', togBilingual: 'Modo bilíngue', togTts: 'Texto para fala', togAutoConvert: 'Converter unidades automaticamente', togAutoScroll: 'Rolar automaticamente', togIncognito: 'Incógnito (sem histórico)', togAutoTranslateOnScroll: 'Traduzir automaticamente ao rolar', btnResetPos: '🏠 Redefinir posição', btnClearCache: '🧹 Limpar cache', btnExport: '📤 Exportar', btnImport: '📥 Importar', btnSurprise: '🎲 Surpreenda-me', btnPirate: '🏴‍☠️ Modo pirata', btnYoda: '🧙 Modo Yoda', btnHide: '👁 Ocultar botões', btnShow: '👁 Mostrar botões', sliderDelay: 'Atraso de solicitação', unitMs: 'ms', statTranslations: 'Traduções', statChars: 'Caracteres', statOnPage: 'Na página', histEmpty: 'Sem histórico', histClear: '🗑 Limpar histórico', searchLang: 'Pesquisar idioma…', btnOrig: '↩ Original', btnCopy: '📋 Copiar', btnSpeak: '🔊', btnRetry: '↺ Tentar novamente', copied: '✅ Copiado!', cacheCleared: '🧹 Cache limpo', toastApply: '✅ Interface atualizada', toastSave: '💾 Salvo — recarregando…', toastSurprise: '🎲 Idioma:', toastPirateOn: '🏴‍☠️ Arrr! Modo pirata ativado!', toastPirateOff: '🏴‍☠️ Modo pirata desativado', toastYodaOn: '🧙 Modo Yoda ativado, hmm.', toastYodaOff: '🧙 Modo Yoda desativado.', toastAutoScrollOn: '✅ Tradução automática ao rolar ativada', toastAutoScrollOff: '⭕ Tradução automática ao rolar desativada', shortcutHint: 'F2 = painel', secHotkeys: 'Atalhos', hotkeyPanel: 'Abrir painel', hotkeyPress: 'Pressione uma tecla…', hotkeyReset: '↺ Redefinir', secColors: 'Cores do tema', colorAcc: 'Destaque', colorTxt: 'Texto', colorBg: 'Fundo', colorOk: 'Sucesso', btnResetColors: '↺ Redefinir cores', secRequestLimits: 'Limites de solicitações', maxConcurrentRequests: 'Máx. solicitações simultâneas', maxRequestsPerSecond: 'Máx. solicitações / seg', maxTextLengthPerRequest: 'Máx. caracteres / solicitação', maxParagraphsPerRequest: 'Máx. parágrafos / solicitação', btnShowOriginals: '📄 MOSTRAR ORIGINAL', btnShowTranslations: '🌐 MOSTRAR TRADUÇÃO', toastShowingOriginals: '📄 Mostrando texto original', toastShowingTranslations: '🌐 Mostrando tradução', }, ko: { title: 'Reddit 번역기', ver: 'v1.0.10', tabSettings: '⚙️ 설정', tabHistory: '📖 기록', tabExtras: '✨ 기타', secUiLang: 'UI 언어', applyUi: '✨ 인터페이스 적용', secTargetLang: '번역 대상 언어', saveLang: '💾 언어 저장', secEngine: '엔진', secTone: '번역 어조', secTheme: '테마', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'DeepL API 키', saveDeepLApi: '🔑 DEEPL 키 저장', btnTestDeepL: '🧪 DEEPL 테스트', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'DeepL API 키를 쉼표로 구분하여 하나 이상 붙여넣으세요', deeplApiHelp: 'Pro 및 Free 키를 지원합니다. :fx로 끝나는 키는 자동으로 deepl.com/pro-api를 사용합니다.', toastDeepLKeysSaved: '🔑 DeepL 키 저장됨', toastDeepLKeysMissing: '⚠️ DeepL API 키를 먼저 하나 이상 추가하세요', toastDeepLUnsupported: '⚠️ DeepL이 이 대상 언어를 지원하지 않습니다:', toastDeepLTesting: '🧪 DeepL API 테스트 중...', toastDeepLOk: '✅ DeepL API 사용 가능', toastDeepLFail: '❌ DeepL API 사용 불가', toneNeutral: '보통', toneFormal: '격식체', toneSlang: '구어체', themeDark: '다크', themeLight: '라이트', themeCyber: '사이버펑크', themeDracula: '드라큘라', togBilingual: '이중 언어 모드', togTts: '텍스트 음성 변환', togAutoConvert: '단위 자동 변환', togAutoScroll: '자동 스크롤', togIncognito: '시크릿 (기록 없음)', togAutoTranslateOnScroll: '스크롤 시 자동 번역', btnResetPos: '🏠 위치 초기화', btnClearCache: '🧹 캐시 지우기', btnExport: '📤 내보내기', btnImport: '📥 가져오기', btnSurprise: '🎲 랜덤 언어', btnPirate: '🏴‍☠️ 해적 모드', btnYoda: '🧙 요다 모드', btnHide: '👁 버튼 숨기기', btnShow: '👁 버튼 표시', sliderDelay: '요청 지연', unitMs: 'ms', statTranslations: '번역 수', statChars: '문자 수', statOnPage: '페이지 내', histEmpty: '기록 없음', histClear: '🗑 기록 지우기', searchLang: '언어 검색…', btnOrig: '↩ 원문', btnCopy: '📋 복사', btnSpeak: '🔊', btnRetry: '↺ 다시 시도', copied: '✅ 복사됨!', cacheCleared: '🧹 캐시 삭제됨', toastApply: '✅ 인터페이스 업데이트됨', toastSave: '💾 저장됨 — 새로고침 중…', toastSurprise: '🎲 언어:', toastPirateOn: '🏴‍☠️ Arrr! 해적 모드 켜짐!', toastPirateOff: '🏴‍☠️ 해적 모드 꺼짐', toastYodaOn: '🧙 요다 모드 켜짐, 흠.', toastYodaOff: '🧙 요다 모드 꺼짐.', toastAutoScrollOn: '✅ 스크롤 시 자동 번역 켜짐', toastAutoScrollOff: '⭕ 스크롤 시 자동 번역 꺼짐', shortcutHint: 'F2 = 패널', secHotkeys: '단축키', hotkeyPanel: '패널 열기', hotkeyPress: '키를 누르세요…', hotkeyReset: '↺ 초기화', secColors: '테마 색상', colorAcc: '강조', colorTxt: '텍스트', colorBg: '배경', colorOk: '성공', btnResetColors: '↺ 색상 초기화', secRequestLimits: '요청 제한', maxConcurrentRequests: '최대 동시 요청 수', maxRequestsPerSecond: '최대 요청 수 / 초', maxTextLengthPerRequest: '최대 문자 수 / 요청', maxParagraphsPerRequest: '최대 단락 수 / 요청', btnShowOriginals: '📄 원문 보기', btnShowTranslations: '🌐 번역 보기', toastShowingOriginals: '📄 원문 표시 중', toastShowingTranslations: '🌐 번역 표시 중', }, vi: { title: 'Reddit Dịch thuật', ver: 'v1.0.10', tabSettings: '⚙️ Cài đặt', tabHistory: '📖 Lịch sử', tabExtras: '✨ Thêm', secUiLang: 'Ngôn ngữ giao diện', applyUi: '✨ ÁP DỤNG GIAO DIỆN', secTargetLang: 'Ngôn ngữ đích', saveLang: '💾 LƯU NGÔN NGỮ', secEngine: 'Công cụ', secTone: 'Giọng dịch', secTheme: 'Chủ đề', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'Khóa API DeepL', saveDeepLApi: '🔑 LƯU KHÓA DEEPL', btnTestDeepL: '🧪 KIỂM TRA DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'Dán một hoặc nhiều khóa API DeepL, phân cách bằng dấu phẩy', deeplApiHelp: 'Hỗ trợ khóa Pro và Free. Khóa kết thúc bằng :fx tự động dùng deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 Đã lưu khóa DeepL', toastDeepLKeysMissing: '⚠️ Vui lòng thêm ít nhất một khóa API DeepL trước', toastDeepLUnsupported: '⚠️ DeepL không hỗ trợ ngôn ngữ đích này:', toastDeepLTesting: '🧪 Đang kiểm tra API DeepL...', toastDeepLOk: '✅ API DeepL khả dụng', toastDeepLFail: '❌ API DeepL không khả dụng', toneNeutral: 'Trung lập', toneFormal: 'Trang trọng', toneSlang: 'Thông thường', themeDark: 'Tối', themeLight: 'Sáng', themeCyber: 'Cyberpunk', themeDracula: 'Dracula', togBilingual: 'Chế độ song ngữ', togTts: 'Chuyển văn bản thành giọng nói', togAutoConvert: 'Tự động chuyển đổi đơn vị', togAutoScroll: 'Tự động cuộn', togIncognito: 'Ẩn danh (không lưu lịch sử)', togAutoTranslateOnScroll: 'Tự động dịch khi cuộn', btnResetPos: '🏠 Đặt lại vị trí', btnClearCache: '🧹 Xóa bộ nhớ đệm', btnExport: '📤 Xuất', btnImport: '📥 Nhập', btnSurprise: '🎲 Ngẫu nhiên', btnPirate: '🏴‍☠️ Chế độ cướp biển', btnYoda: '🧙 Chế độ Yoda', btnHide: '👁 Ẩn nút', btnShow: '👁 Hiện nút', sliderDelay: 'Độ trễ yêu cầu', unitMs: 'ms', statTranslations: 'Bản dịch', statChars: 'Ký tự', statOnPage: 'Trên trang', histEmpty: 'Chưa có lịch sử', histClear: '🗑 Xóa lịch sử', searchLang: 'Tìm ngôn ngữ…', btnOrig: '↩ Gốc', btnCopy: '📋 Sao chép', btnSpeak: '🔊', btnRetry: '↺ Thử lại', copied: '✅ Đã sao chép!', cacheCleared: '🧹 Đã xóa bộ nhớ đệm', toastApply: '✅ Giao diện đã cập nhật', toastSave: '💾 Đã lưu — đang tải lại…', toastSurprise: '🎲 Ngôn ngữ:', toastPirateOn: '🏴‍☠️ Arrr! Chế độ cướp biển bật!', toastPirateOff: '🏴‍☠️ Chế độ cướp biển tắt', toastYodaOn: '🧙 Chế độ Yoda bật, hmm.', toastYodaOff: '🧙 Chế độ Yoda tắt.', toastAutoScrollOn: '✅ Tự động dịch khi cuộn đã bật', toastAutoScrollOff: '⭕ Tự động dịch khi cuộn đã tắt', shortcutHint: 'F2 = bảng điều khiển', secHotkeys: 'Phím tắt', hotkeyPanel: 'Mở bảng điều khiển', hotkeyPress: 'Nhấn một phím…', hotkeyReset: '↺ Đặt lại', secColors: 'Màu chủ đề', colorAcc: 'Nhấn mạnh', colorTxt: 'Văn bản', colorBg: 'Nền', colorOk: 'Thành công', btnResetColors: '↺ Đặt lại màu', secRequestLimits: 'Giới hạn yêu cầu', maxConcurrentRequests: 'Tối đa yêu cầu đồng thời', maxRequestsPerSecond: 'Tối đa yêu cầu / giây', maxTextLengthPerRequest: 'Tối đa ký tự / yêu cầu', maxParagraphsPerRequest: 'Tối đa đoạn / yêu cầu', btnShowOriginals: '📄 HIỂN THỊ BẢN GỐC', btnShowTranslations: '🌐 HIỂN THỊ BẢN DỊCH', toastShowingOriginals: '📄 Đang hiển thị văn bản gốc', toastShowingTranslations: '🌐 Đang hiển thị bản dịch', }, ar: { title: 'Reddit مترجم', ver: 'v1.0.10', tabSettings: '⚙️ الإعدادات', tabHistory: '📖 السجل', tabExtras: '✨ إضافات', secUiLang: 'لغة الواجهة', applyUi: '✨ تطبيق الواجهة', secTargetLang: 'لغة الهدف', saveLang: '💾 حفظ اللغة', secEngine: 'المحرك', secTone: 'أسلوب الترجمة', secTheme: 'المظهر', engGoogle: 'Google', engMymemory: 'MyMemory', engDeepL: 'DeepL', secDeepLApi: 'مفاتيح DeepL API', saveDeepLApi: '🔑 حفظ مفاتيح DEEPL', btnTestDeepL: '🧪 اختبار DEEPL', btnToggleSecretsShow: '👁', btnToggleSecretsHide: '🙈', deeplApiPlaceholder: 'الصق مفتاحاً أو أكثر من مفاتيح DeepL API، مفصولة بفواصل', deeplApiHelp: 'يدعم مفاتيح Pro و Free. المفاتيح التي تنتهي بـ :fx تستخدم تلقائياً deepl.com/pro-api.', toastDeepLKeysSaved: '🔑 تم حفظ مفاتيح DeepL', toastDeepLKeysMissing: '⚠️ أضف مفتاح DeepL API واحداً على الأقل أولاً', toastDeepLUnsupported: '⚠️ DeepL لا يدعم لغة الهدف هذه:', toastDeepLTesting: '🧪 جارٍ اختبار DeepL API...', toastDeepLOk: '✅ DeepL API متاح', toastDeepLFail: '❌ DeepL API غير متاح', toneNeutral: 'محايد', toneFormal: 'رسمي', toneSlang: 'عامي', themeDark: 'داكن', themeLight: 'فاتح', themeCyber: 'سايبربانك', themeDracula: 'دراكولا', togBilingual: 'وضع ثنائي اللغة', togTts: 'تحويل النص إلى كلام', togAutoConvert: 'تحويل الوحدات تلقائياً', togAutoScroll: 'التمرير التلقائي', togIncognito: 'التصفح الخاص (بدون سجل)', togAutoTranslateOnScroll: 'ترجمة تلقائية عند التمرير', btnResetPos: '🏠 إعادة تعيين الموضع', btnClearCache: '🧹 مسح ذاكرة التخزين المؤقت', btnExport: '📤 تصدير', btnImport: '📥 استيراد', btnSurprise: '🎲 لغة عشوائية', btnPirate: '🏴‍☠️ وضع القراصنة', btnYoda: '🧙 وضع يودا', btnHide: '👁 إخفاء الأزرار', btnShow: '👁 إظهار الأزرار', sliderDelay: 'تأخير الطلب', unitMs: 'ms', statTranslations: 'الترجمات', statChars: 'الأحرف', statOnPage: 'في الصفحة', histEmpty: 'لا يوجد سجل', histClear: '🗑 مسح السجل', searchLang: 'البحث عن لغة…', btnOrig: '↩ الأصل', btnCopy: '📋 نسخ', btnSpeak: '🔊', btnRetry: '↺ إعادة المحاولة', copied: '✅ تم النسخ!', cacheCleared: '🧹 تم مسح ذاكرة التخزين المؤقت', toastApply: '✅ تم تحديث الواجهة', toastSave: '💾 تم الحفظ — جارٍ إعادة التحميل…', toastSurprise: '🎲 اللغة:', toastPirateOn: '🏴‍☠️ Arrr! وضع القراصنة مفعّل!', toastPirateOff: '🏴‍☠️ وضع القراصنة معطّل', toastYodaOn: '🧙 وضع يودا مفعّل، همم.', toastYodaOff: '🧙 وضع يودا معطّل.', toastAutoScrollOn: '✅ الترجمة التلقائية عند التمرير مفعّلة', toastAutoScrollOff: '⭕ الترجمة التلقائية عند التمرير معطّلة', shortcutHint: 'F2 = اللوحة', secHotkeys: 'الاختصارات', hotkeyPanel: 'فتح اللوحة', hotkeyPress: 'اضغط مفتاحاً…', hotkeyReset: '↺ إعادة تعيين', secColors: 'ألوان المظهر', colorAcc: 'لون مميز', colorTxt: 'نص', colorBg: 'خلفية', colorOk: 'نجاح', btnResetColors: '↺ إعادة تعيين الألوان', secRequestLimits: 'حدود الطلبات', maxConcurrentRequests: 'أقصى طلبات متزامنة', maxRequestsPerSecond: 'أقصى طلبات / ثانية', maxTextLengthPerRequest: 'أقصى أحرف / طلب', maxParagraphsPerRequest: 'أقصى فقرات / طلب', btnShowOriginals: '📄 إظهار الأصل', btnShowTranslations: '🌐 إظهار الترجمة', toastShowingOriginals: '📄 يتم عرض النص الأصلي', toastShowingTranslations: '🌐 يتم عرض الترجمة', }, }; const S = (key) => (STRINGS[cfg.uiLang] || STRINGS.en)[key] ?? (STRINGS.en[key] ?? key); // ═══════════════════════════════════════════════════════════════════════════ // § ЯЗЫКИ // ═══════════════════════════════════════════════════════════════════════════ const ALL_LANGS = [ 'af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg', 'ca', 'ceb', 'co', 'hr', 'cs', 'da', 'nl', 'en', 'eo', 'et', 'tl', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gu', 'ht', 'ha', 'haw', 'he', 'hi', 'hmn', 'hu', 'is', 'ig', 'id', 'ga', 'it', 'ja', 'jw', 'kn', 'kk', 'km', 'ko', 'ku', 'ky', 'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi', 'mr', 'mn', 'my', 'ne', 'no', 'ps', 'fa', 'pl', 'pt', 'pa', 'ro', 'ru', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tg', 'ta', 'te', 'th', 'tr', 'uk', 'ur', 'uz', 'vi', 'cy', 'xh', 'yi', 'yo', 'zu', 'zh', ]; function langName(code, locale) { try { const d = new Intl.DisplayNames([locale || cfg.uiLang], { type: 'language' }); const n = d.of(code); return n.charAt(0).toUpperCase() + n.slice(1); } catch { return code.toUpperCase(); } } // Кешируем имена языков const langNameCache = {}; function getLangName(code) { const key = `${code}:${cfg.uiLang}`; if (!langNameCache[key]) langNameCache[key] = langName(code, cfg.uiLang); return langNameCache[key]; } function buildLangSelect(selectEl, searchEl, codes, selected) { const sorted = codes.map(c => ({ c, n: getLangName(c) })).sort((a, b) => a.n.localeCompare(b.n)); function render(q) { const f = q.toLowerCase(); const filtered = f ? sorted.filter(({ c, n }) => n.toLowerCase().includes(f) || c.includes(f)) : sorted; selectEl.innerHTML = filtered.map(({ c, n }) => `` ).join(''); } render(''); if (searchEl) searchEl.addEventListener('input', () => render(searchEl.value)); return { refresh: (q = '') => render(q) }; } // ═══════════════════════════════════════════════════════════════════════════ // § ТЕМЫ // ═══════════════════════════════════════════════════════════════════════════ const THEMES = { dark: { bg: 'rgba(10,10,14,.96)', surf: 'rgba(24,24,32,.9)', brd: 'rgba(255,255,255,.06)', txt: '#e0e0ec', mut: 'rgba(255,255,255,.28)', acc: '#ff4500', glow: 'rgba(255,69,0,.36)', dim: 'rgba(255,69,0,.11)', ok: '#60d394', okd: 'rgba(96,211,148,.11)', biBg: 'rgba(20,22,30,.96)', biTxt: '#fff3ed', biBrd: 'rgba(255,69,0,.42)', btnBg: 'var(--rtp-dim)', btnTxt: '#ff6a2b', btnBrd: 'rgba(255,69,0,.24)', btnDoneBg: 'rgba(96,211,148,.11)', btnDoneTxt: '#60d394', btnDoneBrd: 'rgba(96,211,148,.26)' }, light: { bg: 'rgba(246,246,250,.97)', surf: 'rgba(255,255,255,.93)', brd: 'rgba(0,0,0,.07)', txt: '#17171e', mut: 'rgba(0,0,0,.36)', acc: '#ff4500', glow: 'rgba(255,69,0,.2)', dim: 'rgba(255,69,0,.09)', ok: '#1a9e5a', okd: 'rgba(26,158,90,.09)', biBg: 'rgba(255,245,240,.95)', biTxt: '#17171e', biBrd: 'rgba(255,69,0,.28)', btnBg: 'rgba(255,241,234,.96)', btnTxt: '#c63a00', btnBrd: 'rgba(255,69,0,.26)', btnDoneBg: 'rgba(226,247,236,.98)', btnDoneTxt: '#157347', btnDoneBrd: 'rgba(26,158,90,.28)' }, cyberpunk: { bg: 'rgba(3,0,16,.97)', surf: 'rgba(10,3,32,.93)', brd: 'rgba(0,255,255,.11)', txt: '#ddf4ff', mut: 'rgba(0,255,255,.36)', acc: '#00ffff', glow: 'rgba(0,255,255,.44)', dim: 'rgba(0,255,255,.09)', ok: '#ff00aa', okd: 'rgba(255,0,170,.1)', biBg: 'rgba(4,18,34,.97)', biTxt: '#e9feff', biBrd: 'rgba(0,255,255,.5)', btnBg: 'rgba(0,255,255,.09)', btnTxt: '#66ffff', btnBrd: 'rgba(0,255,255,.24)', btnDoneBg: 'rgba(255,0,170,.12)', btnDoneTxt: '#ff6ecf', btnDoneBrd: 'rgba(255,0,170,.3)' }, dracula: { bg: 'rgba(14,14,26,.97)', surf: 'rgba(26,26,46,.91)', brd: 'rgba(139,92,246,.16)', txt: '#f8f8f2', mut: 'rgba(189,147,249,.5)', acc: '#bd93f9', glow: 'rgba(189,147,249,.42)', dim: 'rgba(189,147,249,.1)', ok: '#50fa7b', okd: 'rgba(80,250,123,.1)', biBg: 'rgba(33,34,54,.97)', biTxt: '#f8f8f2', biBrd: 'rgba(189,147,249,.52)', btnBg: 'rgba(189,147,249,.12)', btnTxt: '#d5b6ff', btnBrd: 'rgba(189,147,249,.26)', btnDoneBg: 'rgba(80,250,123,.12)', btnDoneTxt: '#50fa7b', btnDoneBrd: 'rgba(80,250,123,.28)' }, }; function applyTheme(t) { const th = Object.assign({}, THEMES[t] || THEMES.dark); // Применяем кастомные цвета поверх базовой темы if (cfg.customColors) { try { const cc = typeof cfg.customColors === 'string' ? JSON.parse(cfg.customColors) : cfg.customColors; Object.assign(th, cc); } catch { } } const r = document.documentElement; Object.entries(th).forEach(([k, v]) => r.style.setProperty(`--rtp-${k}`, v)); document.body.setAttribute('data-rtp-theme', t); } function colorToHex(color) { // Если уже hex — возвращаем как есть if (/^#[0-9a-f]{6}$/i.test(color)) return color; // rgba/rgb — конвертируем const m = color.match(/[\d.]+/g); if (!m) return '#888888'; const r = (+m[0]).toString(16).padStart(2, '0'); const g = (+m[1]).toString(16).padStart(2, '0'); const b = (+m[2]).toString(16).padStart(2, '0'); return `#${r}${g}${b}`; } // ═══════════════════════════════════════════════════════════════════════════ // § CSS // ═══════════════════════════════════════════════════════════════════════════ GM_addStyle(` @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); :root { --rtp-bg:rgba(10,10,14,.96); --rtp-surf:rgba(24,24,32,.9); --rtp-brd:rgba(255,255,255,.06); --rtp-txt:#e0e0ec; --rtp-mut:rgba(255,255,255,.28); --rtp-acc:#ff4500; --rtp-glow:rgba(255,69,0,.36); --rtp-dim:rgba(255,69,0,.11); --rtp-ok:#60d394; --rtp-okd:rgba(96,211,148,.11); --rtp-biBg:rgba(20,22,30,.96); --rtp-biTxt:#fff3ed; --rtp-biBrd:rgba(255,69,0,.42); --rtp-btnBg:rgba(255,69,0,.11); --rtp-btnTxt:#ff6a2b; --rtp-btnBrd:rgba(255,69,0,.24); --rtp-btnDoneBg:rgba(96,211,148,.11); --rtp-btnDoneTxt:#60d394; --rtp-btnDoneBrd:rgba(96,211,148,.26); --f:'Outfit',sans-serif; --fm:'JetBrains Mono',monospace; --r:15px; } /* ─ КОНТЕНТНЫЕ КНОПКИ ─ */ .rtp-btn { display:inline-flex; align-items:center; gap:5px; margin:3px 6px; padding:3px 11px; font-family:var(--f); font-size:10.5px; font-weight:600; letter-spacing:.04em; color:var(--rtp-btnTxt); background:var(--rtp-btnBg); border:1px solid var(--rtp-btnBrd); border-radius:20px; cursor:pointer; vertical-align:middle; white-space:nowrap; transition:all .22s cubic-bezier(.34,1.56,.64,1); opacity:.6; } .rtp-btn:hover { opacity:1; transform:translateY(-1px) scale(1.05); box-shadow:0 4px 14px var(--rtp-glow); } .rtp-btn.done { color:var(--rtp-btnDoneTxt); background:var(--rtp-btnDoneBg); border-color:var(--rtp-btnDoneBrd); opacity:1; } .rtp-btn.busy { opacity:.4; pointer-events:none; } /* Спиннер */ .sp { display:inline-block; width:8px; height:8px; border:1.5px solid currentColor; border-top-color:transparent; border-radius:50%; animation:spin .75s linear infinite; } @keyframes spin { to{transform:rotate(360deg)} } /* Fade-in при появлении перевода */ .rtp-fi { animation:fi .4s ease; } @keyframes fi { from{opacity:0;transform:translateY(3px)} to{opacity:1;transform:none} } /* Двуязычный блок */ .rtp-ctrl { position:relative; z-index:20; display:flex; align-items:center; gap:6px; flex-wrap:wrap; width:fit-content; max-width:100%; margin:4px 0; pointer-events:auto; } .rtp-bi { position:relative; z-index:20; margin:6px 0 8px; padding:10px 14px; background:linear-gradient(180deg, var(--rtp-biBg), var(--rtp-surf)); border:1px solid var(--rtp-biBrd); border-left:3px solid var(--rtp-acc); border-radius:0 11px 11px 0; font-size:13px; line-height:1.65; color:var(--rtp-biTxt); font-family:var(--f); box-shadow:0 8px 24px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.03); text-shadow:0 1px 0 rgba(0,0,0,.28); white-space:pre-wrap; overflow-wrap:anywhere; word-break:break-word; max-height:none; overflow:visible; } /* Тулбар */ .rtp-tb { position:relative; z-index:21; display:inline-flex; gap:3px; margin:0; } .rtp-t { display:inline-flex; align-items:center; padding:2px 8px; font-family:var(--f); font-size:9.5px; font-weight:600; letter-spacing:.04em; color:var(--rtp-mut); border:1px solid var(--rtp-brd); border-radius:12px; cursor:pointer; transition:all .16s; background:transparent; } .rtp-t:hover { color:var(--rtp-txt); background:var(--rtp-surf); } /* ─ FAB ─ */ #rtp-fab, #rtp-view-toggle { position:fixed; bottom:28px; right:28px; z-index:9998; display:flex; align-items:center; justify-content:center; min-width:160px; height:42px; padding:0 16px; background:rgba(24,24,32,.92); color:#fff; border:1px solid var(--rtp-brd); border-radius:22px; font-family:var(--f); font-size:12px; font-weight:700; letter-spacing:.05em; cursor:pointer; box-shadow:0 8px 24px rgba(0,0,0,.28); backdrop-filter:blur(10px); transition:all .22s cubic-bezier(.34,1.56,.64,1); } #rtp-fab { bottom:78px; min-width:52px; width:52px; padding:0; border-radius:50%; font-size:18px; } #rtp-fab.busy { opacity:.62; pointer-events:none; } #rtp-fab.done { color:var(--rtp-ok); border-color:var(--rtp-ok); } #rtp-fab:hover, #rtp-view-toggle:hover { transform:translateY(-3px) scale(1.02); border-color:var(--rtp-acc); box-shadow:0 12px 30px var(--rtp-glow); } #rtp-view-toggle.originals { color:#000; background:var(--rtp-dim); border-color:rgba(255,69,0,.24); text-shadow:0 1px 0 rgba(0,0,0,.22); } /* ─ Горячие клавиши ─ */ .hk-row { display:flex; align-items:center; justify-content:space-between; gap:8px; margin:5px 0; } .hk-lbl { font-size:12px; color:var(--rtp-mut); flex:1; } .hk-btn { font-family:var(--fm); font-size:11px; font-weight:600; letter-spacing:.03em; padding:5px 12px; border-radius:8px; cursor:pointer; min-width:90px; text-align:center; background:var(--rtp-surf); color:var(--rtp-txt); border:1px solid var(--rtp-brd); transition:all .16s; } .hk-btn:hover { border-color:var(--rtp-acc); color:var(--rtp-acc); } .hk-btn.capturing { border-color:var(--rtp-acc); color:var(--rtp-acc); background:var(--rtp-dim); animation:hkpulse 1s ease infinite; } @keyframes hkpulse { 0%,100%{opacity:1} 50%{opacity:.5} } .hk-reset { font-size:11px; padding:4px 8px; cursor:pointer; color:var(--rtp-mut); border:1px solid var(--rtp-brd); border-radius:8px; background:transparent; transition:all .16s; flex-shrink:0; } .hk-reset:hover { color:var(--rtp-txt); border-color:var(--rtp-acc); } /* ─ Цвета темы ─ */ .clr-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; margin-top:4px; } .clr-row { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 10px; background:var(--rtp-surf); border:1px solid var(--rtp-brd); border-radius:9px; } .clr-lbl { font-size:11px; color:var(--rtp-mut); } .clr-inp { width:32px; height:24px; border:none; border-radius:5px; cursor:pointer; background:none; padding:0; } /* ─ ПАНЕЛЬ ─ */ #rtp-panel { position:fixed; z-index:10000; width:372px; font-family:var(--f); border-radius:var(--r); background:var(--rtp-bg); backdrop-filter:blur(32px) saturate(180%); -webkit-backdrop-filter:blur(32px) saturate(180%); border:1px solid var(--rtp-brd); box-shadow:0 32px 80px rgba(0,0,0,.75), inset 0 1px 0 rgba(255,255,255,.045); animation:pan .3s cubic-bezier(.34,1.56,.64,1); overflow:hidden; color:var(--rtp-txt); } @keyframes pan { from{opacity:0;transform:translateY(-14px) scale(.97)} to{opacity:1;transform:none} } /* Шапка */ #rtp-hdr { background:linear-gradient(135deg,var(--rtp-dim),transparent 65%); border-bottom:1px solid var(--rtp-brd); padding:13px 16px; display:flex; align-items:center; justify-content:space-between; cursor:move; user-select:none; } .logo-w { display:flex; align-items:center; gap:10px; } .logo-ic { width:35px; height:35px; background:var(--rtp-acc); border-radius:11px; display:flex; align-items:center; justify-content:center; font-size:18px; box-shadow:0 4px 14px var(--rtp-glow); flex-shrink:0; } .logo-nm { font-size:13.5px; font-weight:700; letter-spacing:.04em; } .logo-vr { font-size:9px; color:var(--rtp-mut); font-family:var(--fm); margin-top:1px; } #rtp-close { width:29px; height:29px; display:flex; align-items:center; justify-content:center; border:1px solid var(--rtp-brd); border-radius:8px; cursor:pointer; color:var(--rtp-mut); transition:all .17s; background:transparent; font-size:13px; flex-shrink:0; } #rtp-close:hover { color:var(--rtp-txt); background:rgba(255,255,255,.07); } /* Статистика */ #rtp-stats { display:flex; border-bottom:1px solid var(--rtp-brd); } .st { flex:1; padding:10px 5px; text-align:center; } .st+.st { border-left:1px solid var(--rtp-brd); } .st-v { font-family:var(--fm); font-size:18px; font-weight:700; color:var(--rtp-acc); } .st-l { font-size:8.5px; color:var(--rtp-mut); text-transform:uppercase; letter-spacing:.1em; margin-top:2px; } /* Табы */ #rtp-tabs { display:flex; border-bottom:1px solid var(--rtp-brd); background:var(--rtp-surf); } .tab { flex:1; padding:10px 4px; text-align:center; font-size:10px; font-weight:700; letter-spacing:.06em; color:var(--rtp-mut); cursor:pointer; transition:all .18s; border-bottom:2px solid transparent; text-transform:uppercase; } .tab.on { color:var(--rtp-acc); border-bottom-color:var(--rtp-acc); } .tab:hover:not(.on) { color:var(--rtp-txt); } /* Панели */ .pane { padding:15px; display:flex; flex-direction:column; gap:12px; max-height:475px; overflow-y:auto; } .pane::-webkit-scrollbar { width:3px; } .pane::-webkit-scrollbar-thumb { background:var(--rtp-brd); border-radius:2px; } /* Лейблы */ .lbl { display:block; font-size:9.5px; font-weight:700; color:var(--rtp-mut); text-transform:uppercase; letter-spacing:.1em; margin-bottom:6px; } /* Языковый блок (поиск + список) */ .lang-wrap { display:flex; flex-direction:column; } .lang-search { background:var(--rtp-surf); border:1px solid var(--rtp-brd); border-bottom:none; color:var(--rtp-txt); padding:8px 12px; border-radius:9px 9px 0 0; font-family:var(--f); font-size:12.5px; outline:none; } .lang-search::placeholder { color:var(--rtp-mut); } .lang-search:focus { border-color:var(--rtp-acc); } .lang-sel { background:var(--rtp-surf); border:1px solid var(--rtp-brd); color:var(--rtp-txt); padding:7px 10px; border-radius:0 0 9px 9px; font-family:var(--f); font-size:12.5px; outline:none; appearance:none; cursor:pointer; max-height:175px; transition:border-color .18s; } .lang-sel:focus { border-color:var(--rtp-acc); } .lang-sel option { background:var(--rtp-surf); color:var(--rtp-txt); } .lang-sel option:checked, .lang-sel option:hover { background:var(--rtp-dim); color:var(--rtp-txt); } .api-box { width:100%; min-height:84px; resize:vertical; box-sizing:border-box; background:var(--rtp-surf); border:1px solid var(--rtp-brd); color:var(--rtp-txt); padding:10px 12px; border-radius:9px; font-family:var(--fm); font-size:11.5px; line-height:1.45; outline:none; transition:border-color .18s; } .api-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; } .api-head .lbl { margin-bottom:0; } .api-wrap { position:relative; } .api-box:focus { border-color:var(--rtp-acc); } .api-box::placeholder { color:var(--rtp-mut); } .api-box.masked { -webkit-text-security:disc; } .api-eye { width:32px; height:32px; display:flex; align-items:center; justify-content:center; border:1px solid var(--rtp-brd); border-radius:8px; background:var(--rtp-surf); color:var(--rtp-mut); cursor:pointer; transition:all .16s; flex-shrink:0; } .api-eye:hover { color:var(--rtp-txt); border-color:var(--rtp-acc); } .api-help { margin-top:6px; font-size:10.5px; line-height:1.45; color:var(--rtp-mut); } .num-grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:7px; } .num-card { padding:8px 10px; background:var(--rtp-surf); border:1px solid var(--rtp-brd); border-radius:10px; } @media (max-width: 430px) { .num-grid { grid-template-columns:1fr; } } .num-lbl { display:block; margin-bottom:6px; font-size:10px; line-height:1.35; color:var(--rtp-mut); } .num-inp { width:100%; box-sizing:border-box; background:transparent; border:1px solid var(--rtp-brd); color:var(--rtp-txt); padding:7px 9px; border-radius:8px; font-family:var(--fm); font-size:12px; outline:none; } .num-inp:focus { border-color:var(--rtp-acc); } /* Пилюли */ .pills { display:flex; gap:3px; background:var(--rtp-surf); border:1px solid var(--rtp-brd); border-radius:10px; padding:3px; } .pill { flex:1; text-align:center; padding:7px 4px; border-radius:8px; font-size:10.5px; font-weight:700; color:var(--rtp-mut); cursor:pointer; transition:all .18s; white-space:nowrap; } .pill.on { background:var(--rtp-acc); color:#fff; box-shadow:0 2px 8px var(--rtp-glow); } /* Тогглы */ .tog-row { display:flex; align-items:center; justify-content:space-between; padding:2px 0; } .tog-lbl { font-size:12.5px; color:var(--rtp-txt); } .tog { position:relative; width:38px; height:21px; flex-shrink:0; } .tog input { opacity:0; width:0; height:0; } .tog-tr { position:absolute; inset:0; background:var(--rtp-surf); border:1px solid var(--rtp-brd); border-radius:21px; cursor:pointer; transition:all .24s; } .tog input:checked+.tog-tr { background:var(--rtp-acc); border-color:var(--rtp-acc); } .tog-tr::after { content:''; position:absolute; left:3px; top:3px; width:13px; height:13px; background:#fff; border-radius:50%; transition:.24s; } .tog input:checked+.tog-tr::after { transform:translateX(17px); } /* Кнопки */ .btn-p { width:100%; height:44px; background:var(--rtp-acc); border:none; color:#fff; border-radius:11px; font-family:var(--f); font-size:11.5px; font-weight:800; letter-spacing:.06em; cursor:pointer; transition:all .2s; position:relative; overflow:hidden; } .btn-p::before { content:''; position:absolute; inset:0; background:linear-gradient(135deg,rgba(255,255,255,.14),transparent); } .btn-p:hover { box-shadow:0 8px 22px var(--rtp-glow); transform:translateY(-1px); } .btn-p.ghost { background:var(--rtp-surf); border:1px solid var(--rtp-brd); color:var(--rtp-txt); box-shadow:none; } .btn-p.ghost:hover { background:rgba(255,255,255,.07); } .g2 { display:grid; grid-template-columns:1fr 1fr; gap:7px; } .btn-s { padding:9px 7px; background:var(--rtp-surf); border:1px solid var(--rtp-brd); color:var(--rtp-mut); border-radius:10px; font-size:10.5px; font-family:var(--f); font-weight:600; cursor:pointer; transition:all .17s; text-align:center; } .btn-s:hover { color:var(--rtp-txt); background:rgba(255,255,255,.06); border-color:rgba(255,255,255,.1); } .btn-s.active { color:var(--rtp-acc); border-color:var(--rtp-acc); } /* Разделитель */ .div { height:1px; background:var(--rtp-brd); } /* История */ .hi { padding:9px 12px; background:var(--rtp-surf); border:1px solid var(--rtp-brd); border-radius:10px; cursor:pointer; transition:border-color .18s; } .hi:hover { border-color:var(--rtp-acc); } .hi-o { font-size:10px; color:var(--rtp-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:3px; } .hi-t { font-size:12px; color:var(--rtp-txt); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .hi-m { font-size:9px; color:var(--rtp-acc); font-family:var(--fm); margin-top:4px; } /* Слайдер */ .slider { width:100%; accent-color:var(--rtp-acc); cursor:pointer; } .slider-v { text-align:right; font-size:10px; color:var(--rtp-mut); margin-top:3px; font-family:var(--fm); } /* Тост */ #rtp-toast { position:fixed; bottom:92px; right:28px; z-index:10010; background:var(--rtp-bg); backdrop-filter:blur(16px); border:1px solid var(--rtp-brd); color:var(--rtp-txt); padding:10px 18px; border-radius:12px; font-family:var(--f); font-size:12.5px; font-weight:600; box-shadow:0 8px 28px rgba(0,0,0,.5); pointer-events:none; opacity:0; transform:translateY(8px); transition:all .24s; } #rtp-toast.on { opacity:1; transform:none; } /* Cyberpunk overrides */ [data-rtp-theme=cyberpunk] #rtp-panel { box-shadow:0 0 40px rgba(0,255,255,.1),0 32px 80px rgba(0,0,0,.85); } `); // ═══════════════════════════════════════════════════════════════════════════ // § СЛЭНГ // ═══════════════════════════════════════════════════════════════════════════ const SLANG = { OP: 'автор поста', 'TL;DR': 'краткое содержание', TIL: 'сегодня узнал', AMA: 'задайте любой вопрос', IMO: 'по моему мнению', IMHO: 'по моему скромному мнению', IRL: 'в реальной жизни', ELI5: 'объясни как пятилетнему', AFAIK: 'насколько я знаю', IIRC: 'если я правильно помню', SMH: 'качаю головой', FTW: 'победа', LMK: 'дай знать', NGL: 'не буду врать', YMMV: 'у каждого по-разному', FWIW: 'к вашему сведению', ICYMI: 'если вы пропустили', FTFY: 'исправил за тебя', }; const SLANG_RE = new RegExp(`(? k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?![\\w;])`, 'gi'); function expandSlang(text) { return text.replace(SLANG_RE, m => { const k = m.toUpperCase(); return SLANG[k] ? `${m}[=${SLANG[k]}]` : m; }); } // ═══════════════════════════════════════════════════════════════════════════ // § КОНВЕРТЕРЫ ЕДИНИЦ // ═══════════════════════════════════════════════════════════════════════════ function convertUnits(text) { if (!cfg.autoConvert) return text; return text .replace(/(-?\d+(?:[.,]\d+)?)\s*°?F\b/g, (_, n) => `${n}°F (${((+n.replace(',', '.') - 32) * 5 / 9).toFixed(1)}°C)`) .replace(/(\d+(?:[.,]\d+)?)\s*miles?\b/gi, (_, n) => `${n} миль (≈${(+n.replace(',', '.') * 1.609).toFixed(1)} км)`) .replace(/(\d+(?:[.,]\d+)?)\s*lbs?\b/gi, (_, n) => `${n} фунт (≈${(+n.replace(',', '.') * 0.4536).toFixed(1)} кг)`) .replace(/(\d+(?:[.,]\d+)?)\s*(?:inch(?:es)?|")\b/gi, (_, n) => `${n}" (≈${(+n.replace(',', '.') * 2.54).toFixed(1)} см)`) .replace(/(\d+(?:[.,]\d+)?)\s*(?:foot|feet|ft)\b/gi, (_, n) => `${n} фут (≈${(+n.replace(',', '.') * 0.3048).toFixed(2)} м)`) .replace(/(\d+(?:[.,]\d+)?)\s*(?:yard|yd)s?\b/gi, (_, n) => `${n} ярд (≈${(+n.replace(',', '.') * 0.9144).toFixed(2)} м)`); } // ═══════════════════════════════════════════════════════════════════════════ // § ПАСХАЛКИ // ═══════════════════════════════════════════════════════════════════════════ let pirateMode = false; let yodaMode = false; function pirateify(t) { return t.replace(/\bthe\b/gi, "th'").replace(/\byou\b/gi, 'ye').replace(/\bis\b/gi, 'be') .replace(/\bmy\b/gi, 'me').replace(/\byes\b/gi, 'aye').replace(/\bno\b/gi, 'nay') .replace(/\bfriend\b/gi, 'matey').replace(/\bhello\b/gi, 'ahoy') + ' ⚓ Arrr!'; } function yodaify(t) { const w = t.split(' '); if (w.length < 4) return t + ', hmm.'; const n = Math.ceil(w.length / 3); return [...w.slice(-n), ...w.slice(0, -n)].join(' ') + ', hmm.'; } // ═══════════════════════════════════════════════════════════════════════════ // § ПЕРЕВОД // ═══════════════════════════════════════════════════════════════════════════ let reqGen = 0; // поколение очереди — сброс при отмене let recentRequestTimes = []; let activeTasks = 0; let pendingTasks = []; let activeNetworkRequests = 0; let pendingNetworkRequests = []; const AUTO_TRANSLATE_VIEWPORT_MARGIN = 140; const AUTO_TRANSLATE_SETTLE_MS = 50; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getPositiveInt(value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) { const n = Math.floor(Number(value)); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, n)); } function getMaxRequestsPerSecond() { return getPositiveInt(cfg.maxRequestsPerSecond, DEF.maxRequestsPerSecond, 1, 50); } function getMaxConcurrentRequests() { return getPositiveInt(cfg.maxConcurrentRequests, DEF.maxConcurrentRequests, 1, 20); } function getMaxTextLengthPerRequest() { return getPositiveInt(cfg.maxTextLengthPerRequest, DEF.maxTextLengthPerRequest, 100, 20000); } function getMaxParagraphsPerRequest() { return getPositiveInt(cfg.maxParagraphsPerRequest, DEF.maxParagraphsPerRequest, 1, 100); } async function waitForRateSlot() { const limit = getMaxRequestsPerSecond(); while (true) { const now = Date.now(); recentRequestTimes = recentRequestTimes.filter(ts => now - ts < 1000); if (recentRequestTimes.length < limit) { recentRequestTimes.push(now); return; } const oldest = recentRequestTimes[0] || now; await sleep(Math.max(20, 1000 - (now - oldest))); } } function pumpNetworkQueue() { const limit = getMaxConcurrentRequests(); const isCurrentTask = (task) => task.gen === reqGen; while (activeNetworkRequests < limit && pendingNetworkRequests.length) { const task = pendingNetworkRequests.shift(); activeNetworkRequests++; (async () => { try { if (!isCurrentTask(task)) return task.resolve(null); await sleep(cfg.requestDelay); if (!isCurrentTask(task)) return task.resolve(null); await waitForRateSlot(); if (!isCurrentTask(task)) return task.resolve(null); const result = await task.fn(); task.resolve(isCurrentTask(task) ? result : null); } catch { task.resolve(null); } finally { activeNetworkRequests = Math.max(0, activeNetworkRequests - 1); pumpNetworkQueue(); } })(); } } function runLimitedRequest(fn) { return new Promise(resolve => { pendingNetworkRequests.push({ fn, resolve, gen: reqGen }); pumpNetworkQueue(); }); } function pumpQueue() { const limit = getMaxConcurrentRequests(); const isCurrentTask = (task) => task.gen === reqGen; while (activeTasks < limit && pendingTasks.length) { const task = pendingTasks.shift(); activeTasks++; (async () => { try { if (!isCurrentTask(task)) return task.resolve(null); const result = await task.fn(); task.resolve(isCurrentTask(task) ? result : null); } catch { task.resolve(null); } finally { activeTasks = Math.max(0, activeTasks - 1); pumpQueue(); } })(); } } function enqueue(fn) { return new Promise(resolve => { pendingTasks.push({ fn, resolve, gen: reqGen }); pumpQueue(); }); } function toneHint() { if (cfg.tone === 'formal') return 'Translate formally and professionally: '; if (cfg.tone === 'slang') return 'Translate casually and colloquially: '; return ''; } function escapeHtml(text) { return String(text || '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[ch])); } function parseJsonSafe(text, fallback = null) { try { return JSON.parse(text); } catch { return fallback; } } function gmRequest({ method = 'GET', url, headers, data, parse, fallback = null, timeout = 15000 }) { return new Promise(resolve => GM_xmlhttpRequest({ method, url, headers, data, timeout, onload: (r) => { let parsed = fallback; try { parsed = parse ? parse(r) : r.responseText; } catch { } resolve({ ok: r.status >= 200 && r.status < 300, status: r.status, data: parsed ?? fallback, raw: r, }); }, onerror: () => resolve({ ok: false, status: 0, data: fallback, raw: null, }), ontimeout: () => resolve({ ok: false, status: 0, data: fallback, raw: null, }), })); } function getDeepLCacheScope() { const keys = parseDeepLKeys(); const tiers = keys.map(key => (key.endsWith(':fx') ? 'free' : 'pro')).join(','); return `deepl|keys:${keys.length}|tiers:${tiers}`; } function getEngineScope() { return cfg.engine === 'deepl' ? getDeepLCacheScope() : cfg.engine; } function getTranslationCacheKey(text) { return `${getEngineScope()}|${cfg.targetLang}|${cfg.tone}|${text}`; } function parseDeepLKeys(raw = cfg.deeplApiKeys) { return String(raw || '') .split(',') .map(k => k.trim()) .filter(Boolean) .filter((k, i, arr) => arr.indexOf(k) === i); } function getDeepLEndpoint(apiKey) { return apiKey.endsWith(':fx') ? 'https://api-free.deepl.com' : 'https://api.deepl.com'; } function splitLongText(text, maxChars) { const chunks = []; let remaining = String(text || '').trim(); while (remaining.length > maxChars) { let cut = remaining.lastIndexOf('. ', maxChars); if (cut < maxChars * 0.5) cut = remaining.lastIndexOf('! ', maxChars); if (cut < maxChars * 0.5) cut = remaining.lastIndexOf('? ', maxChars); if (cut < maxChars * 0.5) cut = remaining.lastIndexOf('。', maxChars); if (cut < maxChars * 0.5) cut = remaining.lastIndexOf(',', maxChars); if (cut < maxChars * 0.5) cut = remaining.lastIndexOf(',', maxChars); if (cut < maxChars * 0.5) cut = remaining.lastIndexOf(' ', maxChars); if (cut < maxChars * 0.5) cut = maxChars; const part = remaining.slice(0, cut + (cut === maxChars ? 0 : 1)).trim(); if (part) chunks.push(part); remaining = remaining.slice(cut + (cut === maxChars ? 0 : 1)).trim(); } if (remaining) chunks.push(remaining); return chunks.length ? chunks : ['']; } function buildRequestChunks(text) { const source = String(text || '').trim(); if (!source) return ['']; const maxChars = getMaxTextLengthPerRequest(); const maxParagraphs = getMaxParagraphsPerRequest(); const paragraphs = source .split(/\n\s*\n+|\r?\n/) .map(p => p.trim()) .filter(Boolean); const normalizedParagraphs = (paragraphs.length ? paragraphs : [source]).flatMap(p => p.length > maxChars ? splitLongText(p, maxChars) : [p] ); const chunks = []; let current = []; let currentLen = 0; normalizedParagraphs.forEach(paragraph => { const nextLen = currentLen + paragraph.length + (current.length ? 2 : 0); const exceedsChars = nextLen > maxChars; const exceedsParagraphs = current.length >= maxParagraphs; if (current.length && (exceedsChars || exceedsParagraphs)) { chunks.push(current.join('\n\n')); current = []; currentLen = 0; } current.push(paragraph); currentLen += paragraph.length + (current.length > 1 ? 2 : 0); }); if (current.length) chunks.push(current.join('\n\n')); return chunks.length ? chunks : [source]; } function maskDeepLKey(apiKey) { const raw = String(apiKey || ''); const isFree = raw.endsWith(':fx'); const base = isFree ? raw.slice(0, -3) : raw; if (!base) return isFree ? '***:fx' : '***'; if (base.length <= 8) return `${base.slice(0, 2)}***${isFree ? ':fx' : ''}`; return `${base.slice(0, 4)}...${base.slice(-4)}${isFree ? ':fx' : ''}`; } async function requestDeepLUsage(apiKey) { const endpoint = getDeepLEndpoint(apiKey); return runLimitedRequest(() => gmRequest({ url: `${endpoint}/v2/usage`, headers: { Authorization: `DeepL-Auth-Key ${apiKey}`, }, parse: r => parseJsonSafe(r.responseText), })); } async function testDeepLApi(raw = cfg.deeplApiKeys) { const keys = parseDeepLKeys(raw); if (!keys.length) { return { ok: false, reason: 'missing_keys' }; } let lastError = null; for (const apiKey of keys) { const result = await requestDeepLUsage(apiKey); if (result.ok) { return { ok: true, apiKey, status: result.status, data: result.data || {}, }; } lastError = { ok: false, apiKey, status: result.status, data: result.data || null, }; } return lastError || { ok: false, reason: 'unknown' }; } function getDeepLTargetLang(code) { const mapped = { en: 'EN', de: 'DE', fr: 'FR', es: 'ES', pt: 'PT', it: 'IT', nl: 'NL', pl: 'PL', ru: 'RU', ja: 'JA', zh: 'ZH', bg: 'BG', cs: 'CS', da: 'DA', el: 'EL', et: 'ET', fi: 'FI', hu: 'HU', id: 'ID', ko: 'KO', lt: 'LT', lv: 'LV', nb: 'NB', no: 'NB', ro: 'RO', sk: 'SK', sl: 'SL', sv: 'SV', tr: 'TR', uk: 'UK', }; return mapped[String(code || '').toLowerCase()] || null; } let deeplKeyCursor = 0; // 检测文本语言 async function detectLanguage(text) { try { const sample = buildRequestChunks(text)[0] || text; const q = toneHint() + sample; const result = await runLimitedRequest(() => gmRequest({ url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=${encodeURIComponent(q)}`, parse: r => parseJsonSafe(r.responseText, []), })); return result.data?.[2] || null; } catch { return null; } } function normalizeLangCode(code) { return String(code || '') .trim() .toLowerCase() .replace('_', '-') .split('-')[0]; } function getElementSourceText(el) { return expandSlang((el?.dataset?.orig || el?.innerText || '').trim()); } function isElementInAutoTranslateViewport(el) { if (!el || !el.isConnected) return false; const rect = el.getBoundingClientRect(); return rect.bottom >= -AUTO_TRANSLATE_VIEWPORT_MARGIN && rect.top <= window.innerHeight + AUTO_TRANSLATE_VIEWPORT_MARGIN && rect.right >= 0 && rect.left <= window.innerWidth; } function canAutoTranslateButton(btn) { const el = btn?._targetEl; return !!btn && !!el && btn.dataset.st === 'orig' && !btn.dataset.skip && !btn.classList.contains('busy') && isElementInAutoTranslateViewport(el); } function markButtonBusy(btn) { if (!btn || btn.dataset.st !== 'orig') return; btn.innerHTML = ''; btn.classList.add('busy'); } function restoreIdleButton(btn) { if (!btn || btn.dataset.st === 'done') return; btn.classList.remove('busy'); btn.innerHTML = `🌐 ${cfg.targetLang.toUpperCase()}`; } const autoTranslateTimers = new WeakMap(); function clearAutoTranslateTimer(btn) { const timer = autoTranslateTimers.get(btn); if (timer) { clearTimeout(timer); autoTranslateTimers.delete(btn); } } function scheduleAutoTranslate(btn) { if (!cfg.autoTranslateOnScroll || !canAutoTranslateButton(btn)) return; clearAutoTranslateTimer(btn); const timer = setTimeout(() => { autoTranslateTimers.delete(btn); if (!canAutoTranslateButton(btn)) return; btn.dataset.autoMode = 'scroll'; btn.click(); }, AUTO_TRANSLATE_SETTLE_MS); autoTranslateTimers.set(btn, timer); } async function getDetectedLang(el, text) { if (!el) return normalizeLangCode(await detectLanguage(text)); const cached = normalizeLangCode(el.dataset.rtpLang); if (cached) return cached; const detected = normalizeLangCode(await detectLanguage(text)); if (detected) el.dataset.rtpLang = detected; return detected; } function shouldSkipDetectedLang(detectedLang) { if (!detectedLang) return false; const detected = normalizeLangCode(detectedLang); return detected === normalizeLangCode(cfg.targetLang) || detected === normalizeLangCode(cfg.uiLang); } async function doTranslateDeepL(text) { const keys = parseDeepLKeys(); if (!keys.length) { toast(S('toastDeepLKeysMissing')); return text; } const targetLang = getDeepLTargetLang(cfg.targetLang); if (!targetLang) { toast(`${S('toastDeepLUnsupported')} ${cfg.targetLang.toUpperCase()}`); return text; } const chunks = buildRequestChunks(text); const start = deeplKeyCursor % keys.length; const translatedChunks = []; for (const chunk of chunks) { const q = toneHint() + chunk; let translatedChunk = null; for (let offset = 0; offset < keys.length; offset++) { const idx = (start + offset) % keys.length; const apiKey = keys[idx]; const endpoint = getDeepLEndpoint(apiKey); const result = await runLimitedRequest(() => gmRequest({ method: 'POST', url: `${endpoint}/v2/translate`, headers: { Authorization: `DeepL-Auth-Key ${apiKey}`, 'Content-Type': 'application/json', }, data: JSON.stringify({ text: [q], target_lang: targetLang, }), parse: r => parseJsonSafe(r.responseText), })); const translated = result.ok ? result.data?.translations?.[0]?.text || null : null; if (translated) { deeplKeyCursor = idx + 1; translatedChunk = translated; break; } } translatedChunks.push(translatedChunk || chunk); } return translatedChunks.join('\n\n'); } const ENGINE_TRANSLATORS = { google: async (chunk) => { const q = toneHint() + chunk; const response = await runLimitedRequest(() => gmRequest({ url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${cfg.targetLang}&dt=t&q=${encodeURIComponent(q)}`, parse: r => parseJsonSafe(r.responseText, []), fallback: [], })); return response.data?.[0]?.map(i => i[0]).join('') || chunk; }, mymemory: async (chunk) => { const q = toneHint() + chunk; const response = await runLimitedRequest(() => gmRequest({ url: `https://api.mymemory.translated.net/get?q=${encodeURIComponent(q)}&langpair=auto|${cfg.targetLang}`, parse: r => parseJsonSafe(r.responseText), fallback: { responseData: { translatedText: chunk } }, })); return response.data?.responseData?.translatedText || chunk; }, deepl: doTranslateDeepL, }; async function doTranslate(text) { const key = getTranslationCacheKey(text); if (cache[key]) { cache[key].ts = Date.now(); return cache[key].val; } let result = text; try { const translateWithEngine = ENGINE_TRANSLATORS[cfg.engine] || ENGINE_TRANSLATORS.google; if (cfg.engine === 'deepl') { result = await translateWithEngine(text); } else { const translatedChunks = []; for (const chunk of buildRequestChunks(text)) { translatedChunks.push(await translateWithEngine(chunk)); } result = translatedChunks.join('\n\n'); } } catch { } if (pirateMode) result = pirateify(result); if (yodaMode) result = yodaify(result); result = convertUnits(result); cacheSet(key, result); return result; } // ═══════════════════════════════════════════════════════════════════════════ // § TTS // ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════ // § УТИЛИТЫ // ═══════════════════════════════════════════════════════════════════════════ function toast(msg, ms = 2800) { let el = document.getElementById('rtp-toast'); if (!el) { el = document.createElement('div'); el.id = 'rtp-toast'; document.body.appendChild(el); } el.textContent = msg; el.classList.add('on'); clearTimeout(el._t); el._t = setTimeout(() => el.classList.remove('on'), ms); } function mkToggle(checked, onChange) { const wrap = document.createElement('label'); wrap.className = 'tog'; const inp = document.createElement('input'); inp.type = 'checkbox'; inp.checked = !!checked; inp.onchange = () => onChange(inp.checked); const tr = document.createElement('span'); tr.className = 'tog-tr'; wrap.append(inp, tr); return wrap; } function fmt(n) { return Number(n).toLocaleString(); } function fmtK(n) { return n >= 10000 ? (n / 1000).toFixed(1) + 'K' : fmt(n); } function updateStats() { const map = { 'st-cnt': fmt(cfg.totalCount), 'st-chr': fmtK(cfg.totalChars), 'st-pg': document.querySelectorAll('.rtp-btn').length }; for (const [id, v] of Object.entries(map)) { const el = document.getElementById(id); if (el) el.textContent = v; } } let translationDisplayMode = 'translated'; function syncControlVisibility() { document.querySelectorAll('.rtp-ctrl').forEach(host => { host.style.display = btnsHidden ? 'none' : 'flex'; }); document.querySelectorAll('.rtp-btn').forEach(btn => { btn.style.display = btnsHidden ? 'none' : ''; }); document.querySelectorAll('.rtp-tb').forEach(tb => { tb.style.display = btnsHidden ? 'none' : ''; }); document.querySelectorAll('.rtp-bi').forEach(bi => { bi.style.display = translationDisplayMode === 'original' ? '' : 'none'; }); if (!btnsHidden || cfg.bilingualMode) { document.querySelectorAll('.rtp-btn.done').forEach(btn => renderTranslatedState(btn, btn._targetEl)); } } function getViewToggleLabel() { return translationDisplayMode === 'translated' ? S('btnShowOriginals') : S('btnShowTranslations'); } function updateViewToggleButton() { const btn = document.getElementById('rtp-view-toggle'); if (!btn) return; btn.textContent = getViewToggleLabel(); btn.classList.toggle('originals', translationDisplayMode === 'original'); } function setTranslatedContentLayout(el, translated) { if (!el) return; if (translated) { if (el.dataset.rtpOrigStyle == null) { el.dataset.rtpOrigStyle = el.getAttribute('style') || ''; } el.style.setProperty('white-space', 'pre-wrap', 'important'); el.style.setProperty('overflow-wrap', 'anywhere', 'important'); el.style.setProperty('word-break', 'break-word', 'important'); el.style.setProperty('max-height', 'none', 'important'); el.style.setProperty('height', 'auto', 'important'); el.style.setProperty('overflow', 'visible', 'important'); el.style.setProperty('-webkit-line-clamp', 'unset', 'important'); el.style.setProperty('line-clamp', 'unset', 'important'); return; } if (el.dataset.rtpOrigStyle == null) return; const originalStyle = el.dataset.rtpOrigStyle; if (originalStyle) el.setAttribute('style', originalStyle); else el.removeAttribute('style'); delete el.dataset.rtpOrigStyle; } function ensureBilingualBlock(btn, el, translated) { if (!btn || !el || !translated) return null; if (!btn._bi || !btn._bi.isConnected) { const bi = document.createElement('div'); bi.className = 'rtp-bi'; bi.innerText = translated; btn._bi = bi; (btn._ctrlHost || el).after(bi); } else if (btn._bi.innerText !== translated) { btn._bi.innerText = translated; } return btn._bi; } function renderTranslatedState(btn, el, mode = translationDisplayMode) { if (!btn || !el || btn.dataset.st !== 'done') return; const orig = el.dataset.orig || ''; const translated = btn.dataset.translation || ''; if (!translated) return; if (cfg.bilingualMode) { const showOriginalWithBilingual = mode === 'original'; const bi = ensureBilingualBlock(btn, el, translated); el.innerText = showOriginalWithBilingual ? orig : translated; setTranslatedContentLayout(el, !showOriginalWithBilingual); if (bi) bi.style.display = showOriginalWithBilingual ? '' : 'none'; return; } if (btn._bi) btn._bi.style.display = 'none'; el.innerText = mode === 'translated' ? translated : orig; setTranslatedContentLayout(el, mode === 'translated'); } function applyDisplayModeToButton(btn, el, mode = translationDisplayMode) { renderTranslatedState(btn, el, mode); } function applyDisplayModeToAll(mode) { translationDisplayMode = mode; document.querySelectorAll('.rtp-btn.done').forEach(btn => renderTranslatedState(btn, btn._targetEl, mode)); updateViewToggleButton(); } function resetTranslationView(btn, el) { if (btn._bi) { btn._bi.remove(); btn._bi = null; } setTranslatedContentLayout(el, false); if (el.dataset.orig) el.innerText = el.dataset.orig; if (btn._tb) { btn._tb.remove(); btn._tb = null; } btn.innerHTML = `🌐 ${cfg.targetLang.toUpperCase()}`; btn.classList.remove('done', 'busy'); btn.dataset.st = 'orig'; btn.dataset.skip = ''; btn.dataset.translation = ''; } function renderTranslationResult(btn, el, text) { btn.classList.remove('busy'); btn.classList.add('done'); btn.innerHTML = `✓ ${S('btnOrig')}`; btn.dataset.st = 'done'; btn.dataset.translation = text; if (cfg.bilingualMode) { const bi = ensureBilingualBlock(btn, el, text); if (bi) bi.classList.add('rtp-fi'); renderTranslatedState(btn, el); if (bi) setTimeout(() => bi.classList.remove('rtp-fi'), 450); return; } el.classList.add('rtp-fi'); renderTranslatedState(btn, el); setTimeout(() => el.classList.remove('rtp-fi'), 450); } function finalizeTranslation(btn, el, src, res) { renderTranslationResult(btn, el, res); if (cfg.autoScroll && !cfg.autoTranslateOnScroll) { btn.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } buildTranslationToolbar(btn, el, src, res); cfg.totalCount++; cfg.totalChars += (el.dataset.orig || '').length; flushStats(); pushHistory(el.dataset.orig, res, cfg.targetLang); updateStats(); } function buildTranslationToolbar(btn, el, src, res) { const tb = document.createElement('div'); tb.className = 'rtp-tb'; const addAction = (label, fn) => { const t = document.createElement('span'); t.className = 'rtp-t'; t.textContent = label; t.onclick = (e) => { e.preventDefault(); e.stopPropagation(); fn(); }; tb.appendChild(t); }; addAction(S('btnCopy'), () => navigator.clipboard.writeText(res).then(() => toast(S('copied')))); addAction(S('btnRetry'), async () => { delete cache[getTranslationCacheKey(src)]; flushCache(); resetTranslationView(btn, el); tb.remove(); btn._tb = null; await new Promise(r => setTimeout(r, 50)); btn.click(); }); btn._tb = tb; (btn._ctrlHost || btn).append(tb); if (btnsHidden) tb.style.display = 'none'; } function getCustomColors() { try { return cfg.customColors ? (typeof cfg.customColors === 'string' ? JSON.parse(cfg.customColors) : cfg.customColors) : {}; } catch { return {}; } } function setExclusivePills(panel, attr, onSelect) { panel.querySelectorAll(`[${attr}]`).forEach(pill => { pill.onclick = () => { panel.querySelectorAll(`[${attr}]`).forEach(x => x.classList.remove('on')); pill.classList.add('on'); onSelect(pill.dataset[attr.slice(5)]); }; }); } // ═══════════════════════════════════════════════════════════════════════════ // § ИНЖЕКТ КНОПОК // ═══════════════════════════════════════════════════════════════════════════ const SELS = [ 'shreddit-post [slot="title"]', 'h1[slot="title"]', 'a[id^="post-title"]', 'div[shreddit-comment-content]', '.md:not(.rtp-done)', ].join(','); function attachBtn(el, opts = {}) { const { autoStart = false } = opts; if (!el || el.dataset.rtpDone) return; const txt = (el.innerText || '').trim(); if (txt.length < 5) return; el.dataset.rtpDone = '1'; el.classList.add('rtp-done'); el.dataset.rtpLang = ''; // 用于缓存检测到的语言 const ctrlHost = document.createElement('div'); ctrlHost.className = 'rtp-ctrl'; const btn = document.createElement('button'); btn.className = 'rtp-btn'; btn.innerHTML = `🌐 ${cfg.targetLang.toUpperCase()}`; btn.dataset.st = 'orig'; btn.dataset.skip = ''; // 初始化跳过标记为空 btn.dataset.translation = ''; btn._targetEl = el; btn._ctrlHost = ctrlHost; el._rtpBtn = btn; btn.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); const scrollDriven = btn.dataset.autoMode === 'scroll'; btn.dataset.autoMode = ''; clearAutoTranslateTimer(btn); if (btn.dataset.st === 'done') return resetTranslationView(btn, el); if (btn.classList.contains('busy')) return; if (!el.dataset.orig) el.dataset.orig = el.innerText.trim(); const src = getElementSourceText(el); const detectedLang = await getDetectedLang(el, src); if (shouldSkipDetectedLang(detectedLang)) { btn.dataset.skip = '1'; return; } if (scrollDriven && !isElementInAutoTranslateViewport(el)) return; markButtonBusy(btn); const res = await enqueue(() => doTranslate(src)); if (res == null) return restoreIdleButton(btn); finalizeTranslation(btn, el, src, res); }; ctrlHost.append(btn); el.after(ctrlHost); if (btnsHidden) ctrlHost.style.display = 'none'; autoTranslateObserver.observe(el); // 如果启用滚动时自动翻译,自动触发翻译 if (autoStart && cfg.autoTranslateOnScroll) { requestAnimationFrame(() => scheduleAutoTranslate(btn)); } } // IntersectionObserver — переводим сначала видимые const ioQueue = new Set(); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { attachBtn(e.target, { autoStart: true }); ioQueue.delete(e.target); io.unobserve(e.target); } }); }, { rootMargin: '200px' }); const autoTranslateObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { const btn = entry.target._rtpBtn; if (!btn) return; if (entry.isIntersecting) scheduleAutoTranslate(btn); else clearAutoTranslateTimer(btn); }); }, { rootMargin: `${AUTO_TRANSLATE_VIEWPORT_MARGIN}px 0px ${AUTO_TRANSLATE_VIEWPORT_MARGIN}px 0px` }); function injectButtons() { document.querySelectorAll(SELS).forEach(el => { if (el.dataset.rtpDone || (el.innerText || '').trim().length < 5) return; // 检查元素是否在视口内(包括200px边距,与IntersectionObserver一致) const rect = el.getBoundingClientRect(); const inViewport = ( rect.top <= window.innerHeight + 200 && rect.bottom >= -200 && rect.left <= window.innerWidth + 200 && rect.right >= -200 ); if (inViewport) { // 视口内元素也需要遵循自动翻译配置: // 否则首屏内容和动态加载后直接落在视口里的内容只会显示按钮,不会自动翻译。 attachBtn(el, { autoStart: cfg.autoTranslateOnScroll }); // 如果之前在队列中,移除并取消观察 if (ioQueue.has(el)) { ioQueue.delete(el); io.unobserve(el); } } else { // 不在视口内,加入观察队列 if (!ioQueue.has(el)) { ioQueue.add(el); io.observe(el); } } }); } function getVisibleTranslateButtons() { injectButtons(); return Array.from(document.querySelectorAll('.rtp-btn')) .filter(canAutoTranslateButton); } async function translateVisibleContent(fab) { const buttons = getVisibleTranslateButtons(); if (!buttons.length) return toast(S('toastDone')); if (fab) { fab.classList.add('busy'); fab.innerHTML = ''; } buttons.forEach(btn => { btn.dataset.autoMode = 'scroll'; btn.click(); }); await Promise.all(buttons.map(btn => new Promise(resolve => { const startedAt = Date.now(); const done = () => btn.dataset.st === 'done' || btn.dataset.skip === '1' || !btn.classList.contains('busy'); const tick = () => { if (done() || Date.now() - startedAt > 20000) return resolve(); setTimeout(tick, 120); }; tick(); }))); if (fab) { fab.classList.remove('busy'); fab.classList.add('done'); fab.textContent = '✓'; setTimeout(() => { fab.classList.remove('done'); fab.textContent = '🌐'; }, 900); } } // MutationObserver с debounce let mutTimer = null; new MutationObserver(() => { clearTimeout(mutTimer); mutTimer = setTimeout(injectButtons, 400); }).observe(document.body, { childList: true, subtree: true }); // ═══════════════════════════════════════════════════════════════════════════ // § FAB // ═══════════════════════════════════════════════════════════════════════════ function createTranslateFab() { if (document.getElementById('rtp-fab')) return; const fab = document.createElement('button'); fab.id = 'rtp-fab'; fab.type = 'button'; fab.textContent = '🌐'; fab.title = `${S('title')} · ${cfg.hotkeyPanel}`; fab.setAttribute('aria-label', S('title')); fab.onclick = () => translateVisibleContent(fab); fab.ondblclick = (e) => { e.preventDefault(); e.stopPropagation(); buildPanel(); }; document.body.appendChild(fab); } function createViewToggle() { if (document.getElementById('rtp-view-toggle')) return; const viewToggleBtn = document.createElement('button'); viewToggleBtn.id = 'rtp-view-toggle'; viewToggleBtn.onclick = () => { const nextMode = translationDisplayMode === 'translated' ? 'original' : 'translated'; applyDisplayModeToAll(nextMode); toast(S(nextMode === 'original' ? 'toastShowingOriginals' : 'toastShowingTranslations')); }; document.body.appendChild(viewToggleBtn); updateViewToggleButton(); } // ═══════════════════════════════════════════════════════════════════════════ // § ПАНЕЛЬ // ═══════════════════════════════════════════════════════════════════════════ let activeTab = 'settings'; let btnsHidden = true; function setupPanelLanguageControls(panel) { const uiSel = panel.querySelector('#ui-sel'); const uiSearch = panel.querySelector('#ui-s'); const uiOpts = UI_SUPPORTED.map(c => ({ c, n: langName(c, c) })).sort((a, b) => a.n.localeCompare(b.n)); const renderUi = (q = '') => { const f = q.toLowerCase(); uiSel.innerHTML = uiOpts .filter(({ c, n }) => !f || n.toLowerCase().includes(f) || c.includes(f)) .map(({ c, n }) => ``) .join(''); }; renderUi(); uiSearch.addEventListener('input', () => renderUi(uiSearch.value)); buildLangSelect(panel.querySelector('#tg-sel'), panel.querySelector('#tg-s'), ALL_LANGS, cfg.targetLang); panel.querySelector('#btn-apply-ui').onclick = () => { save('uiLang', uiSel.value); updateViewToggleButton(); panel.remove(); toast(S('toastApply')); setTimeout(buildPanel, 180); }; panel.querySelector('#btn-save-lang').onclick = () => { save('targetLang', panel.querySelector('#tg-sel').value); toast(S('toastSave')); setTimeout(() => location.reload(), 900); }; } function setupDeepLControls(panel) { const box = panel.querySelector('#deepl-api-keys'); const testBtn = panel.querySelector('#btn-test-deepl'); const toggleBtn = panel.querySelector('#btn-toggle-deepl-visibility'); let secretsVisible = false; function syncSecretVisibility() { if (!box || !toggleBtn) return; box.classList.toggle('masked', !secretsVisible); const label = S(secretsVisible ? 'btnToggleSecretsHide' : 'btnToggleSecretsShow'); toggleBtn.textContent = label; toggleBtn.title = label; toggleBtn.setAttribute('aria-label', label); } syncSecretVisibility(); if (toggleBtn) { toggleBtn.onclick = () => { secretsVisible = !secretsVisible; syncSecretVisibility(); }; } panel.querySelector('#btn-save-deepl').onclick = () => { const normalized = parseDeepLKeys(box.value).join(', '); save('deeplApiKeys', normalized); box.value = normalized; toast(S('toastDeepLKeysSaved')); }; testBtn.onclick = async () => { const originalLabel = S('btnTestDeepL'); const normalized = parseDeepLKeys(box.value).join(', '); box.value = normalized; if (!normalized) return toast(S('toastDeepLKeysMissing')); testBtn.disabled = true; testBtn.textContent = S('toastDeepLTesting'); const result = await testDeepLApi(normalized); testBtn.disabled = false; testBtn.textContent = originalLabel; if (result.ok) { const used = Number(result.data?.character_count || 0).toLocaleString(); const limitRaw = Number(result.data?.character_limit || 0); const limit = limitRaw > 0 ? limitRaw.toLocaleString() : '∞'; return toast(`${S('toastDeepLOk')} · ${maskDeepLKey(result.apiKey)} · ${used}/${limit}`); } if (result.reason === 'missing_keys') return toast(S('toastDeepLKeysMissing')); const statusText = result?.status ? `HTTP ${result.status}` : 'NETWORK'; toast(`${S('toastDeepLFail')} · ${maskDeepLKey(result?.apiKey)} · ${statusText}`, 4200); }; } function setupPanelPills(panel) { setExclusivePills(panel, 'data-eng', (engine) => { save('engine', engine); if (engine === 'deepl' && !parseDeepLKeys().length) toast(S('toastDeepLKeysMissing')); }); setExclusivePills(panel, 'data-tone', (tone) => save('tone', tone)); setExclusivePills(panel, 'data-th', (theme) => { save('theme', theme); save('customColors', null); cfg.customColors = null; // Сбрасываем пикеры к значениям новой темы const base = THEMES[theme] || THEMES.dark; ['clr-acc', 'clr-bg', 'clr-ok'].forEach((id, i) => { const key = ['acc', 'bg', 'ok'][i]; const inp = panel.querySelector(`#${id}`); if (inp) inp.value = colorToHex(base[key] || '#888888'); }); applyTheme(theme); }); } function setupPanelToggles(panel) { const toggleKeys = ['bilingualMode', 'autoConvert', 'autoScroll', 'incognito', 'autoTranslateOnScroll']; panel.querySelectorAll('.tog-row').forEach((row, i) => { const key = toggleKeys[i]; if (!key) return; row.appendChild(mkToggle(cfg[key], v => { save(key, v); if (key === 'bilingualMode') { document.querySelectorAll('.rtp-btn.done').forEach(btn => renderTranslatedState(btn, btn._targetEl)); syncControlVisibility(); } })); }); } function setupRequestLimitControls(panel) { const sl = panel.querySelector('#sl-delay'); sl.oninput = () => { save('requestDelay', +sl.value); panel.querySelector('#sl-val').textContent = sl.value + ' ' + S('unitMs'); }; [ ['#limit-concurrency', 'maxConcurrentRequests', DEF.maxConcurrentRequests, 1, 20], ['#limit-rps', 'maxRequestsPerSecond', DEF.maxRequestsPerSecond, 1, 50], ['#limit-chars', 'maxTextLengthPerRequest', DEF.maxTextLengthPerRequest, 100, 20000], ['#limit-paragraphs', 'maxParagraphsPerRequest', DEF.maxParagraphsPerRequest, 1, 100], ].forEach(([selector, key, fallback, min, max]) => { const input = panel.querySelector(selector); if (!input) return; const sync = () => { const value = getPositiveInt(input.value, fallback, min, max); input.value = value; save(key, value); }; input.addEventListener('change', sync); input.addEventListener('blur', sync); }); } function setupHotkeyCapturer(panel, btnId, resetId, cfgKey, defaultVal) { const btn = panel.querySelector(`#${btnId}`); const rst = panel.querySelector(`#${resetId}`); if (!btn || !rst) return; let capturing = false; let captureHandler = null; const stopCapture = () => { if (captureHandler) document.removeEventListener('keydown', captureHandler, true); captureHandler = null; capturing = false; btn.classList.remove('capturing'); }; btn.onclick = () => { if (capturing) return; capturing = true; btn.textContent = S('hotkeyPress'); btn.classList.add('capturing'); captureHandler = (e) => { e.preventDefault(); e.stopPropagation(); if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return; const parts = []; if (e.ctrlKey) parts.push('Ctrl'); if (e.altKey) parts.push('Alt'); if (e.shiftKey) parts.push('Shift'); if (e.metaKey) parts.push('Meta'); parts.push(e.key.length === 1 ? e.key.toUpperCase() : e.key); const combo = parts.join('+'); save(cfgKey, combo); btn.textContent = combo; stopCapture(); }; document.addEventListener('keydown', captureHandler, true); }; rst.onclick = () => { stopCapture(); save(cfgKey, defaultVal); btn.textContent = defaultVal; }; } function setupPanelHotkeys(panel) { setupHotkeyCapturer(panel, 'hk-panel', 'hk-panel-r', 'hotkeyPanel', 'F2'); } function setupPanelColorPickers(panel) { const base = THEMES[cfg.theme] || THEMES.dark; const merged = Object.assign({}, base, getCustomColors()); const colorInputs = [ { id: 'clr-acc', key: 'acc' }, { id: 'clr-bg', key: 'bg' }, { id: 'clr-ok', key: 'ok' }, ]; colorInputs.forEach(({ id, key }) => { const inp = panel.querySelector(`#${id}`); if (!inp) return; inp.value = colorToHex(merged[key] || '#888888'); inp.oninput = () => { const newCC = getCustomColors(); newCC[key] = inp.value; if (key === 'bg') { const r = parseInt(inp.value.slice(1, 3), 16); const g = parseInt(inp.value.slice(3, 5), 16); const b = parseInt(inp.value.slice(5, 7), 16); newCC.surf = `rgba(${r},${g},${b},.85)`; } if (key === 'acc') { const r = parseInt(inp.value.slice(1, 3), 16); const g = parseInt(inp.value.slice(3, 5), 16); const b = parseInt(inp.value.slice(5, 7), 16); newCC.glow = `rgba(${r},${g},${b},.38)`; newCC.dim = `rgba(${r},${g},${b},.11)`; } if (key === 'ok') { const r = parseInt(inp.value.slice(1, 3), 16); const g = parseInt(inp.value.slice(3, 5), 16); const b = parseInt(inp.value.slice(5, 7), 16); newCC.okd = `rgba(${r},${g},${b},.11)`; } save('customColors', JSON.stringify(newCC)); applyTheme(cfg.theme); }; }); panel.querySelector('#btn-reset-clr').onclick = () => { save('customColors', null); applyTheme(cfg.theme); colorInputs.forEach(({ id, key }) => { const inp = panel.querySelector(`#${id}`); if (inp) inp.value = colorToHex(base[key] || '#888888'); }); }; } function setupPanelMiscControls(panel) { panel.querySelector('#btn-rpos').onclick = () => { panel.style.top = '11%'; panel.style.left = 'calc(50% - 186px)'; GM_setValue(PREFIX + 'panelX', null); GM_setValue(PREFIX + 'panelY', null); }; panel.querySelector('#btn-ccache').onclick = () => { cache = {}; flushCache(); toast(S('cacheCleared')); }; panel.querySelector('#btn-exp').onclick = () => { const a = document.createElement('a'); a.href = 'data:text/json,' + encodeURIComponent(JSON.stringify({ v: 8, cfg }, null, 2)); a.download = 'rtp-v8-settings.json'; a.click(); }; panel.querySelector('#btn-imp').onclick = () => { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.json'; inp.onchange = e => { const fr = new FileReader(); fr.onload = ev => { try { const d = JSON.parse(ev.target.result); const src = d.cfg || d; Object.entries(src).forEach(([k, v]) => { if (k in DEF) { cfg[k] = v; GM_setValue(PREFIX + k, v); } }); location.reload(); } catch { toast('❌ Ошибка импорта'); } }; fr.readAsText(e.target.files[0]); }; inp.click(); }; panel.querySelector('#btn-surp').onclick = () => { const r = ALL_LANGS[Math.floor(Math.random() * ALL_LANGS.length)]; save('targetLang', r); toast(`${S('toastSurprise')} ${getLangName(r)}`); setTimeout(() => location.reload(), 1100); }; const pirBtn = panel.querySelector('#btn-pir'); pirBtn.onclick = () => { pirateMode = !pirateMode; pirBtn.classList.toggle('active', pirateMode); toast(pirateMode ? S('toastPirateOn') : S('toastPirateOff')); }; pirBtn.classList.toggle('active', pirateMode); const yodBtn = panel.querySelector('#btn-yoda'); yodBtn.onclick = () => { yodaMode = !yodaMode; yodBtn.classList.toggle('active', yodaMode); toast(yodaMode ? S('toastYodaOn') : S('toastYodaOff')); }; yodBtn.classList.toggle('active', yodaMode); const hideBtn = panel.querySelector('#btn-hide'); hideBtn.classList.toggle('active', btnsHidden); hideBtn.onclick = () => { btnsHidden = !btnsHidden; syncControlVisibility(); hideBtn.textContent = btnsHidden ? S('btnShow') : S('btnHide'); hideBtn.classList.toggle('active', btnsHidden); }; } function setupPanelTabs(panel) { panel.querySelectorAll('.tab').forEach(tab => tab.onclick = () => { activeTab = tab.dataset.tab; panel.querySelectorAll('.tab').forEach(t => t.classList.remove('on')); tab.classList.add('on'); panel.querySelectorAll('.pane').forEach(p => p.style.display = 'none'); panel.querySelector(`#pane-${activeTab}`).style.display = 'flex'; if (activeTab === 'history') renderHistory(panel); if (activeTab === 'settings') updateStats(); }); } function setupPanelDrag(panel) { const hdr = panel.querySelector('#rtp-hdr'); hdr.onmousedown = e => { if (e.target.id === 'rtp-close') return; const ox = e.clientX - panel.offsetLeft; const oy = e.clientY - panel.offsetTop; const mm = ev => { panel.style.left = (ev.clientX - ox) + 'px'; panel.style.top = (ev.clientY - oy) + 'px'; }; const cleanup = () => { GM_setValue(PREFIX + 'panelX', panel.style.left); GM_setValue(PREFIX + 'panelY', panel.style.top); document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', cleanup); }; document.addEventListener('mousemove', mm); document.addEventListener('mouseup', cleanup, { once: true }); document.addEventListener('mouseleave', cleanup, { once: true }); }; } function buildPanel() { const old = document.getElementById('rtp-panel'); if (old) { old.remove(); return; } const panel = document.createElement('div'); panel.id = 'rtp-panel'; panel.style.top = GM_getValue(PREFIX + 'panelY', '11%'); panel.style.left = GM_getValue(PREFIX + 'panelX', 'calc(50% - 186px)'); panel.innerHTML = `
🌐
${S('title')}
${S('ver')} · ${escapeHtml(cfg.hotkeyPanel)}
${fmt(cfg.totalCount)}
${S('statTranslations')}
${fmtK(cfg.totalChars)}
${S('statChars')}
${S('statOnPage')}
${S('tabSettings')}
${S('tabHistory')}
${S('tabExtras')}
${S('secUiLang')}
${S('secTargetLang')}
${S('secEngine')}
${S('engGoogle')}
${S('engMymemory')}
${S('engDeepL')}
${S('secDeepLApi')}
${S('deeplApiHelp')}
${S('secTone')}
${S('toneNeutral')}
${S('toneFormal')}
${S('toneSlang')}
${S('secTheme')}
${S('themeDark')}
${S('themeCyber')}
${S('themeDracula')}
${S('togBilingual')}
${S('togAutoConvert')}
${S('togAutoScroll')}
${S('togIncognito')}
${S('togAutoTranslateOnScroll')}
${S('btnResetPos')}
${S('btnClearCache')}
${S('btnExport')}
${S('btnImport')}
${S('btnSurprise')}
${S('btnPirate')}
${S('btnYoda')}
${btnsHidden ? S('btnShow') : S('btnHide')}
${S('sliderDelay')}
${cfg.requestDelay} ${S('unitMs')}
${S('secRequestLimits')}
${S('maxConcurrentRequests')}
${S('maxRequestsPerSecond')}
${S('maxTextLengthPerRequest')}
${S('maxParagraphsPerRequest')}
⌨️ ${S('secHotkeys')}
${S('hotkeyPanel')}
🎨 ${S('secColors')}
${S('colorAcc')}
${S('colorBg')}
${S('colorOk')}
${S('btnResetColors')}
`; document.body.appendChild(panel); setupPanelLanguageControls(panel); setupDeepLControls(panel); setupPanelPills(panel); setupPanelToggles(panel); setupRequestLimitControls(panel); setupPanelHotkeys(panel); setupPanelColorPickers(panel); setupPanelMiscControls(panel); setupPanelTabs(panel); renderHistory(panel); setupPanelDrag(panel); panel.querySelector('#rtp-close').onclick = () => panel.remove(); updateStats(); } function renderHistory(panel) { const pane = panel.querySelector('#pane-history'); if (!pane) return; pane.innerHTML = ''; if (!history.length) { pane.innerHTML = `
📭 ${S('histEmpty')}
`; return; } history.forEach(item => { const div = document.createElement('div'); div.className = 'hi'; const o = document.createElement('div'); o.className = 'hi-o'; o.textContent = item.orig; const t = document.createElement('div'); t.className = 'hi-t'; t.textContent = item.translated; const m = document.createElement('div'); m.className = 'hi-m'; m.textContent = `→ ${item.lang.toUpperCase()} · ${new Date(item.ts).toLocaleTimeString()}`; div.append(o, t, m); div.onclick = () => navigator.clipboard.writeText(item.translated).then(() => toast(S('copied'))); pane.appendChild(div); }); const clr = document.createElement('div'); clr.className = 'btn-s'; clr.style.marginTop = '4px'; clr.textContent = S('histClear'); clr.onclick = () => { history = []; GM_setValue(PREFIX + 'rtp_v8_history', '[]'); renderHistory(panel); }; pane.appendChild(clr); } // ═══════════════════════════════════════════════════════════════════════════ // § ЗАПУСК // ═══════════════════════════════════════════════════════════════════════════ if (cfg.theme === 'light') { save('theme', 'dark'); cfg.theme = 'dark'; } applyTheme(cfg.theme); createTranslateFab(); createViewToggle(); injectButtons(); function matchesHotkey(e, combo) { if (!combo) return false; const parts = combo.split('+'); const key = parts[parts.length - 1]; const needCtrl = parts.includes('Ctrl'); const needAlt = parts.includes('Alt'); const needShift = parts.includes('Shift'); const needMeta = parts.includes('Meta'); return e.key === key && e.ctrlKey === needCtrl && e.altKey === needAlt && e.shiftKey === needShift && e.metaKey === needMeta; } window.addEventListener('keydown', e => { if (matchesHotkey(e, cfg.hotkeyPanel)) { e.preventDefault(); buildPanel(); } }); })();