Ultimate Text Selection Translator – 即時翻譯所選文字

使用 Cmd+L(Mac)或 Ctrl+L(Windows)可即時翻譯所選文字。支援所有語言,自動偵測所選語言,並翻譯為瀏覽器的預設語言。簡單、快速、高效。

// ==UserScript==
// @name              Ultimate Text Selection Translator – Instantly Translate Any Selected Text
// @name:fr           Ultimate Text Selection Translator – Traduis instantanément n’importe quel texte sélectionné
// @name:es           Ultimate Text Selection Translator – Traduce al instante cualquier texto seleccionado
// @name:de           Ultimate Text Selection Translator – Übersetze sofort ausgewählten Text
// @name:ru           Ultimate Text Selection Translator – Мгновенный перевод выделенного текста
// @name:zh-CN        Ultimate Text Selection Translator – 即时翻译所选文本
// @name:zh-TW        Ultimate Text Selection Translator – 即時翻譯所選文字
// @name:ja           Ultimate Text Selection Translator – 選択テキストを即座に翻訳
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @description      Translate selected text instantly using Cmd+L (Mac) or Ctrl+L (Windows). Supports all languages and automatically detects the selected language, translating it into your browser's default language. Simple, fast, and efficient.
// @description:fr   Traduis instantanément n’importe quel texte sélectionné avec Cmd+L (Mac) ou Ctrl+L (Windows). Prend en charge toutes les langues, détecte automatiquement la langue sélectionnée et la traduit dans la langue par défaut de ton navigateur. Simple, rapide et efficace.
// @description:es   Traduce al instante cualquier texto seleccionado con Cmd+L (Mac) o Ctrl+L (Windows). Compatible con todos los idiomas, detecta automáticamente el idioma seleccionado y lo traduce al idioma predeterminado de tu navegador. Simple, rápido y eficiente.
// @description:de   Übersetze ausgewählten Text sofort mit Cmd+L (Mac) oder Ctrl+L (Windows). Unterstützt alle Sprachen, erkennt automatisch die ausgewählte Sprache und übersetzt sie in die Standardsprache deines Browsers. Einfach, schnell und effizient.
// @description:ru   Мгновенно переводите выделенный текст с помощью Cmd+L (Mac) или Ctrl+L (Windows). Поддерживает все языки, автоматически определяет выделенный язык и переводит его на язык по умолчанию вашего браузера. Просто, быстро и эффективно.
// @description:zh-CN 使用 Cmd+L(Mac)或 Ctrl+L(Windows)可即时翻译所选文本。支持所有语言,自动检测所选语言,并翻译为浏览器的默认语言。简单、快速、高效。
// @description:zh-TW 使用 Cmd+L(Mac)或 Ctrl+L(Windows)可即時翻譯所選文字。支援所有語言,自動偵測所選語言,並翻譯為瀏覽器的預設語言。簡單、快速、高效。
// @description:ja   Cmd+L(Mac)または Ctrl+L(Windows)で選択したテキストを即座に翻訳。すべての言語に対応し、選択された言語を自動的に検出して、ブラウザのデフォルト言語に翻訳。シンプル、スピーディー、効率的。
// @author       Dℝ∃wX
// @license      Apache-2.0
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @connect      translate.googleapis.com
// ==/UserScript==

/*
Copyright 2025 Dℝ∃wX

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/



(function() {
    'use strict';

    const browserLang = navigator.language.split('-')[0];

    const languageNames = {
        'en': {
            'auto': 'Detect',
            'en': 'English',
            'fr': 'French',
            'es': 'Spanish',
            'de': 'German',
            'it': 'Italian',
            'pt': 'Portuguese',
            'ru': 'Russian',
            'zh-CN': 'Chinese (Simplified)',
            'ja': 'Japanese',
            'errors': {
                'noText': 'No text selected',
                'translation': 'Translation error',
                'connection': 'Connection error'
            },
            'tooltips': {
                'listenTranslated': 'Listen to translated text',
                'listenOriginal': 'Listen to original text'
            }
        },
        'fr': {
            'auto': 'Détecter',
            'en': 'Anglais',
            'fr': 'Français',
            'es': 'Espagnol',
            'de': 'Allemand',
            'it': 'Italien',
            'pt': 'Portugais',
            'ru': 'Russe',
            'zh-CN': 'Chinois (Simplifié)',
            'ja': 'Japonais',
            'errors': {
                'noText': 'Aucun texte sélectionné',
                'translation': 'Erreur de traduction',
                'connection': 'Erreur de connexion'
            },
            'tooltips': {
                'listenTranslated': 'Écoute le texte traduit',
                'listenOriginal': 'Écoute le texte original'
            }
        },
        'es': {
            'auto': 'Detectar',
            'en': 'Inglés',
            'fr': 'Francés',
            'es': 'Español',
            'de': 'Alemán',
            'it': 'Italiano',
            'pt': 'Portugués',
            'ru': 'Ruso',
            'zh-CN': 'Chino (Simplificado)',
            'ja': 'Japonés',
            'errors': {
                'noText': 'No hay texto seleccionado',
                'translation': 'Error de traducción',
                'connection': 'Error de conexión'
            },
            'tooltips': {
                'listenTranslated': 'Escuchar el texto traducido',
                'listenOriginal': 'Escuchar el texto original'
            }
        },
        'de': {
            'auto': 'Erkennen',
            'en': 'Englisch',
            'fr': 'Französisch',
            'es': 'Spanisch',
            'de': 'Deutsch',
            'it': 'Italienisch',
            'pt': 'Portugiesisch',
            'ru': 'Russisch',
            'zh-CN': 'Chinesisch (Vereinfacht)',
            'ja': 'Japanisch',
            'errors': {
                'noText': 'Kein Text ausgewählt',
                'translation': 'Übersetzungsfehler',
                'connection': 'Verbindungsfehler'
            },
            'tooltips': {
                'listenTranslated': 'Übersetzten Text anhören',
                'listenOriginal': 'Originaltext anhören'
            }
        },
        'it': {
            'auto': 'Rileva',
            'en': 'Inglese',
            'fr': 'Francese',
            'es': 'Spagnolo',
            'de': 'Tedesco',
            'it': 'Italiano',
            'pt': 'Portoghese',
            'ru': 'Russo',
            'zh-CN': 'Cinese (Semplificato)',
            'ja': 'Giapponese',
            'errors': {
                'noText': 'Nessun testo selezionato',
                'translation': 'Errore di traduzione',
                'connection': 'Errore di connessione'
            },
            'tooltips': {
                'listenTranslated': 'Ascolta il testo tradotto',
                'listenOriginal': 'Ascolta il testo originale'
            }
        },
        'pt': {
            'auto': 'Detectar',
            'en': 'Inglês',
            'fr': 'Francês',
            'es': 'Espanhol',
            'de': 'Alemão',
            'it': 'Italiano',
            'pt': 'Português',
            'ru': 'Russo',
            'zh-CN': 'Chinês (Simplificado)',
            'ja': 'Japonês',
            'errors': {
                'noText': 'Nenhum texto selecionado',
                'translation': 'Erro de tradução',
                'connection': 'Erro de conexão'
            },
            'tooltips': {
                'listenTranslated': 'Ouvir o texto traduzido',
                'listenOriginal': 'Ouvir o texto original'
            }
        },
        'ru': {
            'auto': 'Определить',
            'en': 'Английский',
            'fr': 'Французский',
            'es': 'Испанский',
            'de': 'Немецкий',
            'it': 'Итальянский',
            'pt': 'Португальский',
            'ru': 'Русский',
            'zh-CN': 'Китайский (упрощённый)',
            'ja': 'Японский',
            'errors': {
                'noText': 'Текст не выделен',
                'translation': 'Ошибка перевода',
                'connection': 'Ошибка соединения'
            },
            'tooltips': {
                'listenTranslated': 'Прослушать переведённый текст',
                'listenOriginal': 'Прослушать оригинальный текст'
            }
        },
        'zh-CN': {
            'auto': '检测',
            'en': '英语',
            'fr': '法语',
            'es': '西班牙语',
            'de': '德语',
            'it': '意大利语',
            'pt': '葡萄牙语',
            'ru': '俄语',
            'zh-CN': '中文(简体)',
            'ja': '日语',
            'errors': {
                'noText': '未选择文本',
                'translation': '翻译错误',
                'connection': '连接错误'
            },
            'tooltips': {
                'listenTranslated': '聆听翻译文本',
                'listenOriginal': '聆听原文'
            }
        },
        'ja': {
            'auto': '検出',
            'en': '英語',
            'fr': 'フランス語',
            'es': 'スペイン語',
            'de': 'ドイツ語',
            'it': 'イタリア語',
            'pt': 'ポルトガル語',
            'ru': 'ロシア語',
           'zh-CN': '中国語(簡体)',
            'ja': '日本語',
            'errors': {
                'noText': 'テキストが選択されていません',
                'translation': '翻訳エラー',
                'connection': '接続エラー'
            },
            'tooltips': {
                'listenTranslated': '翻訳されたテキストを聞く',
                'listenOriginal': '元のテキストを聞く'
            }
        }
    };

    const uiLang = languageNames[browserLang] ? browserLang : 'en';
    const langNames = languageNames[uiLang];
    const errors = langNames.errors;
    const tooltips = langNames.tooltips;

    const languages = [
        { code: 'auto', name: langNames.auto },
        { code: 'en', name: langNames.en },
        { code: 'fr', name: langNames.fr },
        { code: 'es', name: langNames.es },
        { code: 'de', name: langNames.de },
        { code: 'it', name: langNames.it },
        { code: 'pt', name: langNames.pt },
        { code: 'ru', name: langNames.ru },
        { code: 'zh-CN', name: langNames['zh-CN'] },
        { code: 'ja', name: langNames.ja }
    ];

    const defaultTargetLang = languages.some(lang => lang.code === browserLang && lang.code !== 'auto') ? browserLang : 'en';

    const translationBox = document.createElement('div');
    translationBox.style.cssText = `
        position: fixed;
        background: linear-gradient(135deg, #1e1e2f 0%, #2a2a4a 100%);
        color: #ffffff;
        padding: 20px;
        border-radius: 12px;
        z-index: 9999;
        display: none;
        max-width: 350px;
        min-width: 250px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        font-size: 14px;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        backdrop-filter: blur(10px);
        border: 1px solid rgba(255, 255, 255, 0.1);
    `;
    document.body.appendChild(translationBox);

    translationBox.innerHTML = `
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
            <select id="sourceLang" style="background: rgba(255, 255, 255, 0.1); color: #fff; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; padding: 6px; font-size: 13px; cursor: pointer;">
                ${languages.map(lang => `<option value="${lang.code}">${lang.name}</option>`).join('')}
            </select>
            <span style="color: #a0a0c0; margin: 0 8px;">→</span>
            <select id="targetLang" style="background: rgba(255, 255, 255, 0.1); color: #fff; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; padding: 6px; font-size: 13px; cursor: pointer;">
                ${languages.filter(lang => lang.code !== 'auto').map(lang => `<option value="${lang.code}">${lang.name}</option>`).join('')}
            </select>
        </div>
        <div id="translationText" style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 8px; min-height: 50px; line-height: 1.5; white-space: pre-wrap;"></div>
        <div style="display: flex; justify-content: flex-end; margin-top: 12px; gap: 10px;">
            <div id="speakButton" style="position: relative; cursor: pointer;">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M11 5L6 9H2v6h4l5 4V5z"></path>
                    <path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
                    <path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
                </svg>
                <div id="speakTooltip" style="display: none; position: absolute; bottom: 100%; right: 0; background: rgba(0, 0, 0, 0.8); color: #fff; padding: 8px; border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 10000;">
                    <div id="speakTranslated" style="padding: 4px 0; cursor: pointer;">${tooltips.listenTranslated}</div>
                    <div id="speakOriginal" style="padding: 4px 0; cursor: pointer;">${tooltips.listenOriginal}</div>
                </div>
            </div>
            <div id="copyButton" style="cursor: pointer;" title="Copy translation">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                    <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                </svg>
            </div>
        </div>
    `;

    const sourceLangSelect = translationBox.querySelector('#sourceLang');
    const targetLangSelect = translationBox.querySelector('#targetLang');
    const translationText = translationBox.querySelector('#translationText');
    const speakButton = translationBox.querySelector('#speakButton');
    const speakTooltip = translationBox.querySelector('#speakTooltip');
    const speakTranslated = translationBox.querySelector('#speakTranslated');
    const speakOriginal = translationBox.querySelector('#speakOriginal');
    const copyButton = translationBox.querySelector('#copyButton');

    sourceLangSelect.value = 'auto';
    targetLangSelect.value = defaultTargetLang;

    let currentSelectedText = '';
    let currentTranslatedText = '';

    function getSelectedText() {
        return window.getSelection().toString().trim();
    }

function splitSentences(text) {
    const regex = /(\.\s+|\.\n|\.)/;
    let parts = text.split(regex);
    let sentences = [];
    let currentSentence = '';

    for (let i = 0; i < parts.length; i++) {
        currentSentence += parts[i];
        if (parts[i].match(regex) || i === parts.length - 1) {
            if (currentSentence.trim()) {
                sentences.push(currentSentence.trim());
            }
            currentSentence = '';
        }
    }

    return sentences.length ? sentences : [text];
}


    function translateSentence(text, sourceLang, targetLang, callback) {
        if (!text.trim()) {
            callback(text);
            return;
        }

        const match = text.match(/^(.*?)(?:(\.\s+|\.\n|\.)|$)/);
        const textToTranslate = match[1] || text;
        const delimiter = match[2] || '';

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(textToTranslate.trim())}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    const translation = data[0][0][0] + delimiter;
                    callback(translation);
                } catch (e) {
                    callback(errors.translation + delimiter);
                }
            },
            onerror: function() {
                callback(errors.connection + delimiter);
            }
        });
    }

    function translateText(text, sourceLang, targetLang, callback, position) {
        if (!text) {
            callback(errors.noText, position);
            return;
        }

        const sentences = splitSentences(text);
        let translatedSentences = [];
        let completed = 0;

        sentences.forEach((sentence, index) => {
            translateSentence(sentence, sourceLang, targetLang, (translation) => {
                translatedSentences[index] = translation;
                completed++;

                if (completed === sentences.length) {
                    const fullTranslation = translatedSentences.join('');
                    callback(fullTranslation, position);
                }
            });
        });
    }

function speak(text, lang) {
    const utterance = new SpeechSynthesisUtterance(text);
    utterance.lang = lang;
    window.speechSynthesis.speak(utterance);
}

    document.addEventListener('keydown', (e) => {
        if (e.metaKey && e.key === 'l' && !e.altKey && !e.ctrlKey && !e.shiftKey) {
            e.preventDefault();
            const selectedText = getSelectedText();
            currentSelectedText = selectedText;
            const selection = window.getSelection();
            let position = { x: 0, y: 0 };

            if (selection.rangeCount > 0) {
                const range = selection.getRangeAt(0);
                const rect = range.getBoundingClientRect();
                position = {
                    x: rect.left + window.scrollX,
                    y: rect.bottom + window.scrollY
                };
            }

            translateText(selectedText, sourceLangSelect.value, targetLangSelect.value, (translation, pos) => {
                currentTranslatedText = translation;
                translationText.textContent = translation;
                translationBox.style.display = 'block';
                translationBox.style.left = `${pos.x + 10}px`;
                translationBox.style.top = `${pos.y + 10}px`;
                translationBox.style.opacity = '1';
                translationBox.style.transform = 'translateY(0)';
            }, position);
        }
    });

    function handleLanguageChange() {
        if (currentSelectedText) {
            translateText(currentSelectedText, sourceLangSelect.value, targetLangSelect.value, (translation, pos) => {
                currentTranslatedText = translation;
                translationText.textContent = translation;
            }, { x: parseFloat(translationBox.style.left), y: parseFloat(translationBox.style.top) });
        }
    }

    sourceLangSelect.addEventListener('change', handleLanguageChange);
    targetLangSelect.addEventListener('change', handleLanguageChange);

    speakButton.addEventListener('mouseenter', () => {
        speakTooltip.style.display = 'block';
    });
    speakButton.addEventListener('mouseleave', () => {
        speakTooltip.style.display = 'none';
    });

    speakTranslated.addEventListener('click', () => {
        if (currentTranslatedText) {
            speak(currentTranslatedText, targetLangSelect.value);
        }
    });

    speakOriginal.addEventListener('click', () => {
        if (currentSelectedText) {
            const sourceLang = sourceLangSelect.value === 'auto' ? 'en' : sourceLangSelect.value;
            speak(currentSelectedText, sourceLang);
        }
    });

    copyButton.addEventListener('click', () => {
        if (currentTranslatedText) {
            navigator.clipboard.writeText(currentTranslatedText);
            copyButton.querySelector('svg').style.stroke = '#00ff00';
            setTimeout(() => {
                copyButton.querySelector('svg').style.stroke = '#ffffff';
            }, 1000);
        }
    });

    document.addEventListener('mousedown', (e) => {
        if (!translationBox.contains(e.target)) {
            translationBox.style.display = 'none';
            translationBox.style.opacity = '0';
            translationBox.style.transform = 'translateY(10px)';
        }
    });

    function adjustBoxPosition() {
        const rect = translationBox.getBoundingClientRect();
        if (rect.right > window.innerWidth) {
            translationBox.style.left = `${window.innerWidth - rect.width - 10}px`;
        }
        if (rect.bottom > window.innerHeight) {
            translationBox.style.top = `${window.innerHeight - rect.height - 10}px`;
        }
    }

    translationBox.addEventListener('transitionend', adjustBoxPosition);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址