Greasy Fork

Greasy Fork is available in English.

网页文本翻译工具

选中文本后右键翻译成中文,支持多个翻译API

当前为 2025-06-03 提交的版本,查看 最新版本

// ==UserScript==
// @name         网页文本翻译工具
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  选中文本后右键翻译成中文,支持多个翻译API
// @author       Your name
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // API 配置
    const API_CONFIG = {
        BAIDU: {
            name: '百度翻译',
            url: 'https://fanyi-api.baidu.com/api/trans/vip/translate',
            needKey: true
        },
        YOUDAO: {
            name: '有道翻译',
            url: 'https://openapi.youdao.com/api',
            needKey: true
        },
        MYMEMORY: {
            name: 'MyMemory',
            url: 'https://api.mymemory.translated.net/get',
            needKey: false
        }
    };

    // 获取当前使用的API
    let currentAPI = GM_getValue('currentAPI', 'MYMEMORY');
    let baiduAppId = GM_getValue('baiduAppId', '');
    let baiduKey = GM_getValue('baiduKey', '');
    let youdaoAppKey = GM_getValue('youdaoAppKey', '');
    let youdaoSecretKey = GM_getValue('youdaoSecretKey', '');

    // 添加样式
    GM_addStyle(`
        .translation-popup {
            position: fixed;
            top: 10px;
            right: 10px;
            padding: 10px;
            background: white;
            border: 1px solid #ccc;
            border-radius: 4px;
            z-index: 10000000;
            max-width: 300px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            font-family: Arial, sans-serif;
            font-size: 14px;
            line-height: 1.4;
        }
        .translation-close {
            position: absolute;
            right: 5px;
            top: 5px;
            border: none;
            background: none;
            cursor: pointer;
            font-size: 16px;
            color: #666;
            padding: 0 5px;
        }
        .translation-close:hover {
            color: #333;
        }
        .context-menu {
            position: fixed;
            background: white;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 5px 0;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 10000000;
        }
        .translation-menu-item {
            padding: 8px 20px;
            cursor: pointer;
            background: none;
            border: none;
            width: 100%;
            text-align: left;
            font-size: 14px;
        }
        .translation-menu-item:hover {
            background-color: #f0f0f0;
        }
        .api-settings {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            z-index: 10000001;
        }
        .api-settings input {
            margin: 5px 0;
            padding: 5px;
            width: 100%;
        }
        .api-settings button {
            margin: 5px;
            padding: 5px 10px;
        }
    `);

    // 注册设置菜单
    GM_registerMenuCommand('设置翻译API', showSettings);

    function showSettings() {
        const div = document.createElement('div');
        div.className = 'api-settings';
        div.innerHTML = `
            <h3>翻译API设置</h3>
            <select id="apiSelect">
                ${Object.entries(API_CONFIG).map(([key, api]) => 
                    `<option value="${key}" ${currentAPI === key ? 'selected' : ''}>${api.name}</option>`
                ).join('')}
            </select>
            <div id="baiduSettings" style="display: ${currentAPI === 'BAIDU' ? 'block' : 'none'}">
                <input type="text" id="baiduAppId" placeholder="百度翻译APP ID" value="${baiduAppId}">
                <input type="password" id="baiduKey" placeholder="百度翻译密钥" value="${baiduKey}">
            </div>
            <div id="youdaoSettings" style="display: ${currentAPI === 'YOUDAO' ? 'block' : 'none'}">
                <input type="text" id="youdaoAppKey" placeholder="有道翻译应用ID" value="${youdaoAppKey}">
                <input type="password" id="youdaoSecretKey" placeholder="有道翻译密钥" value="${youdaoSecretKey}">
            </div>
            <button onclick="this.parentElement.remove()">取消</button>
            <button onclick="window.saveAPISettings(this)">保存</button>
        `;
        document.body.appendChild(div);

        // API选择切换事件
        const apiSelect = div.querySelector('#apiSelect');
        apiSelect.addEventListener('change', function() {
            div.querySelector('#baiduSettings').style.display = this.value === 'BAIDU' ? 'block' : 'none';
            div.querySelector('#youdaoSettings').style.display = this.value === 'YOUDAO' ? 'block' : 'none';
        });

        // 保存设置
        window.saveAPISettings = function(button) {
            const parent = button.parentElement;
            currentAPI = parent.querySelector('#apiSelect').value;
            baiduAppId = parent.querySelector('#baiduAppId').value;
            baiduKey = parent.querySelector('#baiduKey').value;
            youdaoAppKey = parent.querySelector('#youdaoAppKey').value;
            youdaoSecretKey = parent.querySelector('#youdaoSecretKey').value;

            GM_setValue('currentAPI', currentAPI);
            GM_setValue('baiduAppId', baiduAppId);
            GM_setValue('baiduKey', baiduKey);
            GM_setValue('youdaoAppKey', youdaoAppKey);
            GM_setValue('youdaoSecretKey', youdaoSecretKey);

            parent.remove();
            showTranslation('设置已保存');
        };
    }

    // 生成百度翻译签名
    function generateBaiduSign(text, salt) {
        const str = baiduAppId + text + salt + baiduKey;
        return CryptoJS.MD5(str).toString();
    }

    // 生成有道翻译签名
    function generateYoudaoSign(text, salt, curtime) {
        const str = youdaoAppKey + truncate(text) + salt + curtime + youdaoSecretKey;
        return CryptoJS.SHA256(str).toString();
    }

    // 截取文本
    function truncate(text) {
        if (text.length <= 20) return text;
        return text.substring(0, 10) + text.length + text.substring(text.length - 10);
    }

    let contextMenu = null;

    // 创建右键菜单
    document.addEventListener('contextmenu', function(event) {
        const selectedText = window.getSelection().toString().trim();
        if (selectedText) {
            event.preventDefault();
            
            if (contextMenu) {
                contextMenu.remove();
            }

            contextMenu = document.createElement('div');
            contextMenu.className = 'context-menu';
            
            let x = event.pageX;
            let y = event.pageY;
            
            contextMenu.style.top = y + 'px';
            contextMenu.style.left = x + 'px';
            
            const menuItem = document.createElement('button');
            menuItem.className = 'translation-menu-item';
            menuItem.textContent = '翻译选中文本';
            menuItem.onclick = () => {
                translateText(selectedText);
                contextMenu.remove();
                contextMenu = null;
            };
            
            contextMenu.appendChild(menuItem);
            document.body.appendChild(contextMenu);
            
            const menuRect = contextMenu.getBoundingClientRect();
            if (menuRect.right > window.innerWidth) {
                contextMenu.style.left = (x - menuRect.width) + 'px';
            }
            if (menuRect.bottom > window.innerHeight) {
                contextMenu.style.top = (y - menuRect.height) + 'px';
            }
        }
    });

    document.addEventListener('click', function(event) {
        if (contextMenu && !contextMenu.contains(event.target)) {
            contextMenu.remove();
            contextMenu = null;
        }
    });

    document.addEventListener('keydown', function(event) {
        if (event.key === 'Escape' && contextMenu) {
            contextMenu.remove();
            contextMenu = null;
        }
    });

    function showTranslation(translatedText) {
        const existingPopup = document.querySelector('.translation-popup');
        if (existingPopup) {
            existingPopup.remove();
        }

        const div = document.createElement('div');
        div.className = 'translation-popup';
        
        const content = document.createElement('div');
        content.style.marginRight = '20px';
        content.textContent = translatedText;
        div.appendChild(content);

        const closeButton = document.createElement('button');
        closeButton.className = 'translation-close';
        closeButton.textContent = '×';
        closeButton.onclick = () => div.remove();
        div.appendChild(closeButton);

        document.body.appendChild(div);
        
        div.style.opacity = '0';
        div.style.transition = 'opacity 0.3s ease-in-out';
        setTimeout(() => div.style.opacity = '1', 10);

        setTimeout(() => {
            div.style.opacity = '0';
            setTimeout(() => div.remove(), 300);
        }, 5000);
    }

    async function translateText(text) {
        showTranslation('正在翻译...');
        
        switch(currentAPI) {
            case 'BAIDU':
                if (!baiduAppId || !baiduKey) {
                    showTranslation('请先设置百度翻译API密钥');
                    return;
                }
                translateWithBaidu(text);
                break;
            case 'YOUDAO':
                if (!youdaoAppKey || !youdaoSecretKey) {
                    showTranslation('请先设置有道翻译API密钥');
                    return;
                }
                translateWithYoudao(text);
                break;
            case 'MYMEMORY':
                translateWithMyMemory(text);
                break;
        }
    }

    function translateWithBaidu(text) {
        const salt = Date.now();
        const sign = generateBaiduSign(text, salt);
        const url = `${API_CONFIG.BAIDU.url}?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${baiduAppId}&salt=${salt}&sign=${sign}`;

        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.trans_result) {
                        showTranslation(data.trans_result[0].dst);
                    } else {
                        showTranslation('翻译失败:' + (data.error_msg || '未知错误'));
                    }
                } catch (error) {
                    console.error('Translation error:', error);
                    showTranslation('翻译出错,请稍后重试');
                }
            },
            onerror: function(error) {
                console.error('Request error:', error);
                showTranslation('网络错误,请稍后重试');
            }
        });
    }

    function translateWithYoudao(text) {
        const salt = Date.now();
        const curtime = Math.round(new Date().getTime() / 1000);
        const sign = generateYoudaoSign(text, salt, curtime);
        const url = `${API_CONFIG.YOUDAO.url}?q=${encodeURIComponent(text)}&from=auto&to=zh-CHS&appKey=${youdaoAppKey}&salt=${salt}&sign=${sign}&signType=v3&curtime=${curtime}`;

        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.translation) {
                        showTranslation(data.translation[0]);
                    } else {
                        showTranslation('翻译失败:' + (data.errorMessage || '未知错误'));
                    }
                } catch (error) {
                    console.error('Translation error:', error);
                    showTranslation('翻译出错,请稍后重试');
                }
            },
            onerror: function(error) {
                console.error('Request error:', error);
                showTranslation('网络错误,请稍后重试');
            }
        });
    }

    function translateWithMyMemory(text) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: `${API_CONFIG.MYMEMORY.url}?q=${encodeURIComponent(text)}&langpair=auto|zh`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.responseStatus === 200) {
                        showTranslation(data.responseData.translatedText);
                    } else {
                        showTranslation('翻译失败,请稍后重试');
                    }
                } catch (error) {
                    console.error('Translation error:', error);
                    showTranslation('翻译出错,请稍后重试');
                }
            },
            onerror: function(error) {
                console.error('Request error:', error);
                showTranslation('网络错误,请稍后重试');
            }
        });
    }
})();