Steam 文本内容翻译

在 Steam 网页上的常见文本内容如简介、评论、新闻、讨论、聊天消息等文本右上角加上了翻译按钮,一键翻译外文。可通过右键点击按钮可重新翻译或切换翻译引擎。支持 WattToolkit 工具箱。

// ==UserScript==
// @name         Steam 文本内容翻译
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  在 Steam 网页上的常见文本内容如简介、评论、新闻、讨论、聊天消息等文本右上角加上了翻译按钮,一键翻译外文。可通过右键点击按钮可重新翻译或切换翻译引擎。支持 WattToolkit 工具箱。
// @author       羽
// @match        *://*.steampowered.com/*
// @match        *://steamcommunity.com/*
// @grant        GM_xmlhttpRequest

// @connect      translate.google.com
// @connect      translate.googleapis.com
// @connect      fanyi.baidu.com
// @connect      ifanyi.iciba.com
// @connect      translate.alibaba.com
// @connect      m.youdao.com
// @connect      dict.youdao.com
// @connect      fanyi.youdao.com
// @connect      transmart.qq.com

// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/base64.min.js

// @license      MIT License

// ==/UserScript==

(function() {
    'use strict';

    // 添加目标语言和API的全局配置
    const TARGET_LANGUAGES = {
        "中文": "zh-CN",
        "英语": "en",
        "日语": "ja",
        "韩语": "ko",
        "法语": "fr",
        "德语": "de",
        "西班牙语": "es",
        "俄语": "ru"
    };

    const TRANSLATION_APIS = {
        "Google翻译": "google",
        "百度翻译": "baidu",
        "金山词霸": "iciba",
        "有道词典-mobile": "youdao_m",
        "腾讯AI": "tencentai"
    };

    // 需要添加翻译功能的控件类名列表
    const targetClasses = [
        'profile_summary noexpand', // 用户简介
        'game_description_snippet', // 游戏简介
        'curator_review', // 游戏简介(推荐流)
        'recommendation',    // 游戏简介(推荐流卡片)
        'game_area_description',    // 游戏介绍
        'devnotes',     // 开发者说明
        'review_area_content',  // 游戏评论详情
        'blotter_group_announcement_content',  // 公告
        'workshop_item_row', // 创意工坊mod简介
        'workshopItemDescription',  // 创意工坊mod介绍
        'commentthread_comment_text',   // 创意工坊回复/游戏评论回复/讨论回复
        'apphub_CardContentMain',   // 新闻卡片简讯
        'workshopItemCollectionContainer',  // 指南简讯
        'guideTopContent',  // 指南主题
        'guide subSections',    // 指南内容
        'A_A2B6fTn_MPLlGCmsLtd _3NW5vEM9HgfQrgR4W-Xy_s',    // 新闻内容
        'EventDetailsBody',  // 活动页面
        'achieveTxt', // 成就
    ];
    // 需要添加翻译功能的CSS选择器列表
    const targetSelectors = {
        // 选择class为rightcol内的class为content的元素
        'rightcol': ['content'],    // 游戏评论
        'shortcol': ['content'],    // 游戏短评
        'forum_op': ['content', 'topic'],   // 讨论会话
        'ChatMessageBlock': ['msg'],    // 聊天消息
        '_1h5cJPC1IYFGDEMbRAWSNy': ['_1M8-Pa3b3WboayCgd5VBJT','_2g3JjlrRkzgUWXF57w3leW'],   // 更新记录简讯
        'announcement detailBox': ['headline','bodytext'],  // 创意工坊主页
    };
    const ignoredClasses = [
        'AppSummaryWidgetCtn',  // 介绍中的游戏卡片

    ]

    // 存储原始内容的键
    const ORIGINAL_CONTENT_KEY = 'original-content';
    // 存储翻译内容的键
    const TRANSLATED_CONTENT_KEY = 'translated-content';
    // 标记控件是否已添加翻译按钮
    const BUTTON_ADDED_KEY = 'translation-button-added';
    // 标记控件当前状态
    const TRANSLATION_STATE_KEY = 'translation-state';

    // 获取保存的设置或使用默认值
    let targetLanguage = localStorage.getItem('translation-target-language') || 'zh-CN';
    let translationApi = localStorage.getItem('translation-api') || 'iciba';


    // 初始化函数
    function init() {
        // 定期检查页面上的目标控件
        setInterval(checkForTargetElements, 2000);
        // 立即执行一次检查
        checkForTargetElements();
    }

    // 检查页面上的目标控件
    function checkForTargetElements() {
        // 处理类名列表
        targetClasses.forEach(className => {
            const elements = document.getElementsByClassName(className);
            for (let i = 0; i < elements.length; i++) {
                const element = elements[i];
                if (!element.getAttribute(BUTTON_ADDED_KEY)) {
                    addTranslationButton(element);
                    element.setAttribute(BUTTON_ADDED_KEY, 'true');
                    element.setAttribute(TRANSLATION_STATE_KEY, 'original');
                }
            }
        });
        
        // 处理CSS选择器列表
        Object.entries(targetSelectors).forEach(([selector, includes]) => {
            const elements = document.getElementsByClassName(selector);
            for (let i = 0; i < elements.length; i++) {
                const element = elements[i];
                // 检查元素是否包含所有指定的子元素
                const hasAllIncludes = includes.every(className => 
                    Array.from(element.children).some(child => 
                        child.classList.contains(className)
                    )
                );
                
                if (!element.getAttribute(BUTTON_ADDED_KEY) && hasAllIncludes) {
                    addTranslationButton(element, includes);
                    element.setAttribute(BUTTON_ADDED_KEY, 'true');
                    element.setAttribute(TRANSLATION_STATE_KEY, 'original');
                }
            }
        });
    }

    // 为控件添加翻译按钮
    function addTranslationButton(element, includes=null) {
        // 检查控件内容是否为空
        if (!element.textContent.trim()) {
            return; 
        }
        // 保存原始内容的深拷贝
        const originalContent = element.cloneNode(true);
        element.setAttribute(ORIGINAL_CONTENT_KEY, 'saved');
        element._originalContent = originalContent;

        // 创建按钮容器,设置为绝对定位
        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'translation-button-container';

        // 创建翻译按钮
        const translateButton = document.createElement('button');
        translateButton.textContent = '翻译';
        translateButton.className = 'translation-toggle-button';
        translateButton.title = '左键点击翻译,右键点击可重新翻译/设置。';

        // 添加按钮点击事件
        translateButton.addEventListener('click', function(event) {
            event.stopPropagation(); // 阻止事件冒泡
            toggleTranslation(element, translateButton, includes);
        });
        // 添加右键菜单事件
        translateButton.addEventListener('contextmenu', function(event) {
            event.stopPropagation(); // 阻止事件冒泡
            showContextMenu(event, element, translateButton);
        });

        // 将按钮添加到容器中
        buttonContainer.appendChild(translateButton);

        // 确保元素有相对定位,以便正确放置按钮
        const originalPosition = window.getComputedStyle(element).position;
        if (originalPosition === 'static') {
            element.style.position = 'relative';
        }

        // 将按钮容器添加到元素中
        element.appendChild(buttonContainer);

        // 添加样式
        addStyles();
    }

    // 创建右键菜单
    function createContextMenu(element, button) {
        // 检查是否已存在菜单
        let menu = document.querySelector('.translation-context-menu');
        if (!menu) {
            menu = document.createElement('div');
            menu.className = 'translation-context-menu';
            document.body.appendChild(menu);
        }
        
        // 清空菜单内容
        menu.innerHTML = '';
        
        // 添加菜单项
        const retranslateItem = document.createElement('div');
        retranslateItem.className = 'translation-context-menu-item';
        retranslateItem.textContent = '重新翻译';
        retranslateItem.addEventListener('click', function() {
            retranslate(element, button);
            hideContextMenu();
        });
        menu.appendChild(retranslateItem);
        
        // 添加分隔线
        const separator = document.createElement('div');
        separator.className = 'translation-context-menu-separator';
        menu.appendChild(separator);
        
        // 添加设置菜单项
        const settingsItem = document.createElement('div');
        settingsItem.className = 'translation-context-menu-item';
        settingsItem.textContent = '翻译设置';
        settingsItem.addEventListener('click', function() {
            showTranslationSettings();
            hideContextMenu();
        });
        menu.appendChild(settingsItem);
        
        return menu;
    }
    
    // 显示右键菜单
    function showContextMenu(event, element, button) {
        event.preventDefault();
        
        const menu = createContextMenu(element, button);
        
        // 设置菜单位置
        menu.style.left = `${event.pageX}px`;
        menu.style.top = `${event.pageY}px`;
        menu.style.display = 'block';
        
        // 点击其他区域关闭菜单
        const closeMenuHandler = function(e) {
            if (!menu.contains(e.target)) {
                hideContextMenu();
                document.removeEventListener('click', closeMenuHandler);
            }
        };
        
        setTimeout(() => {
            document.addEventListener('click', closeMenuHandler);
        }, 0);
    }
    
    // 隐藏右键菜单
    function hideContextMenu() {
        const menu = document.querySelector('.translation-context-menu');
        if (menu) {
            menu.style.display = 'none';
        }
    }

    // 显示翻译设置
    function showTranslationSettings() {
        // 检查是否已存在设置面板
        let settingsPanel = document.getElementById('translation-settings-panel');
        if (settingsPanel) {
            settingsPanel.style.display = 'block';
            // 重置数据
            document.getElementById('translation-target-language').value = targetLanguage;
            document.getElementById('translation-api').value = translationApi;
            return;
        }
        
        // 创建设置面板
        settingsPanel = document.createElement('div');
        settingsPanel.id = 'translation-settings-panel';
        settingsPanel.className = 'translation-settings-panel';
        
        // 添加标题
        const title = document.createElement('h3');
        title.textContent = '翻译设置';
        title.className = 'translation-settings-title';
        settingsPanel.appendChild(title);
        
        // 添加关闭按钮
        const closeButton = document.createElement('button');
        closeButton.textContent = '×';
        closeButton.className = 'translation-settings-close-btn';
        closeButton.addEventListener('click', function() {
            settingsPanel.style.display = 'none';
        });
        settingsPanel.appendChild(closeButton);
        
        // 添加设置选项
        const settingsContent = document.createElement('div');
        settingsContent.className = 'translation-settings-content';
        settingsContent.innerHTML = `
            <div class="translation-settings-option">
                <label>目标语言:</label>
                <select id="translation-target-language">
                    ${Object.entries(TARGET_LANGUAGES).map(([name, value]) => 
                        `<option value="${value}"${value === targetLanguage ? ' selected' : ''}>${name}</option>`
                    ).join('')}
                </select>
            </div>
            <div class="translation-settings-option">
                <label>翻译API:</label>
                <select id="translation-api">
                    ${Object.entries(TRANSLATION_APIS)
                        .map(([name, value]) => 
                            `<option value="${value}"${value === translationApi ? ' selected' : ''}>${name}</option>`
                        ).join('')}
                </select>
            </div>
        `;
        settingsPanel.appendChild(settingsContent);
        
        // 添加保存按钮
        const saveButton = document.createElement('button');
        saveButton.textContent = '保存设置';
        saveButton.className = 'translation-settings-save-btn';
        saveButton.addEventListener('click', function() {
            // 保存设置逻辑
            targetLanguage = document.getElementById('translation-target-language').value;
            translationApi = document.getElementById('translation-api').value;
            
            // 保存到localStorage
            localStorage.setItem('translation-target-language', targetLanguage);
            localStorage.setItem('translation-api', translationApi);
            
            settingsPanel.style.display = 'none';
        });
        settingsPanel.appendChild(saveButton);
        
        // 添加到页面
        document.body.appendChild(settingsPanel);
        
        if (targetLanguage) {
            document.getElementById('translation-target-language').value = targetLanguage;
        }
        
        if (translationApi) {
            document.getElementById('translation-api').value = translationApi;
        }
        
        // 确保样式已添加
        addStyles();
    }

    // 切换翻译状态
    function toggleTranslation(element, button, includes=null) {
        const currentState = element.getAttribute(TRANSLATION_STATE_KEY);
        if (button.classList.contains('error')) {
            button.classList.remove('error'); 
            button.title = '左键点击翻译,右键点击可重新翻译/设置。';
            element._translatedContent = null;
        }

        if (currentState === 'original') {
            // 如果已经有翻译内容,直接显示
            if (element._translatedContent) {
                showTranslatedContent(element);
                button.textContent = '原文';
                element.setAttribute(TRANSLATION_STATE_KEY, 'translated');
            } else {
                // 否则进行翻译
                button.textContent = '翻译中...';
                button.disabled = true;

                // 先移除翻译按钮,避免其文本被收集
                const buttonContainer = element.querySelector('.translation-button-container');
                if (buttonContainer) {
                    element.removeChild(buttonContainer);
                }
                // 仅翻译目标元素
                const nonTargetElements = [];
                let targetElements = [];
                if (includes) {
                    // 保存目标元素和非目标元素
                    Array.from(element.children).forEach((child, index) => {
                        if (includes.some(className => child.classList.contains(className))) {
                            targetElements.push(child);
                        } else if (!child.classList.contains('translation-button-container')) {
                            nonTargetElements.push({
                                element: child,
                                parent: child.parentNode,
                                nextSibling: child.nextSibling,
                                index: index
                            });
                            child.parentNode.removeChild(child);
                        }
                    });
                } else if (ignoredClasses && ignoredClasses.length > 0){
                    // 当没有指定includes时,排除ignoredClasses
                    const ignoredElements = element.querySelectorAll(
                        ignoredClasses.map(className => '.' + className).join(',')
                    );
                    ignoredElements.forEach(el => {
                        el.setAttribute('data-skip-translation', 'true');
                    });
                }

                // 收集所有文本节点
                // const textNodeInfos = collectTextNodes(element);
                // const textsToTranslate = textNodeInfos.map(info => info.node.nodeValue.trim()).filter(text => text.length > 0);
                // 收集所有文本节点(仅从目标元素中收集)
                const textNodeInfos = includes ? 
                    targetElements.flatMap(el => collectTextNodes(el)) :
                    collectTextNodes(element);
                const textsToTranslate = textNodeInfos.map(info => info.node.nodeValue.trim()).filter(text => text.length > 0);

                // 按原始顺序恢复元素
                nonTargetElements.sort((a, b) => a.index - b.index).forEach(info => {
                    info.parent.insertBefore(info.element, info.nextSibling);
                });
                // 重新添加按钮
                if (buttonContainer) {
                    element.appendChild(buttonContainer);
                }

                if (textsToTranslate.length > 0) {
                    // 将所有文本分批翻译,避免超出API限制
                    translateTextBatches(textsToTranslate, function(translatedTexts, success) {
                        // 创建翻译内容的深拷贝
                        const translatedContent = element._originalContent.cloneNode(true);

                        // 再次移除翻译按钮

                        // 获取翻译后的文本节点信息
                        // const translatedNodeInfos = collectTextNodes(translatedContent);
                        // 获取翻译后的文本节点信息(仅从目标元素获取)
                        const translatedTargetElements = includes ?
                            Array.from(translatedContent.children)
                                .filter(child => includes.some(className => child.classList.contains(className))) :
                            [translatedContent];
                        
                        const translatedNodeInfos = translatedTargetElements.flatMap(el => collectTextNodes(el));

                        // 按照原始顺序替换文本
                        let translatedIndex = 0;
                        textNodeInfos.forEach((originalInfo, i) => {
                            if (i >= translatedNodeInfos.length) return;
                            
                            const originalText = originalInfo.node.nodeValue.trim();
                            if (originalText.length > 0 && translatedIndex < translatedTexts.length) {
                                translatedNodeInfos[i].node.nodeValue = translatedNodeInfos[i].node.nodeValue.replace(
                                    translatedNodeInfos[i].node.nodeValue.trim(),
                                    translatedTexts[translatedIndex]
                                );
                                translatedIndex++;
                            }
                        });

                        // 保存翻译内容
                        element._translatedContent = translatedContent;

                        // 显示翻译内容
                        showTranslatedContent(element);

                        button.textContent = '原文';
                        button.disabled = false;
                        element.setAttribute(TRANSLATION_STATE_KEY, 'translated');
                        if(!success){
                            button.textContent = '翻译错误';
                            button.classList.add('error');
                            button.title = '左键点击翻译,右键点击可重新翻译/设置。\n翻译错误可尝试重新翻译,或者切换翻译引擎。';
                        }
                    });
                } else {
                    button.textContent = '翻译';
                    button.disabled = false;
                }
            }
        } else {
            // 恢复原文
            showOriginalContent(element);
            button.textContent = '翻译';
            element.setAttribute(TRANSLATION_STATE_KEY, 'original');
        }
    }

    // 分批翻译文本,处理长文本
    function translateTextBatches(texts, callback) {
        const MAX_BATCH_SIZE = 1000; // 每批最大字符数
        const batches = [];
        let currentBatch = [];
        let currentSize = 0;

        // 将文本分成多个批次
        for (let i = 0; i < texts.length; i++) {
            const text = texts[i];
            if (currentSize + text.length > MAX_BATCH_SIZE && currentBatch.length > 0) {
                batches.push(currentBatch);
                currentBatch = [text];
                currentSize = text.length;
            } else {
                currentBatch.push(text);
                currentSize += text.length;
            }
        }

        if (currentBatch.length > 0) {
            batches.push(currentBatch);
        }

        const results = new Array(texts.length);
        let totalSuccess = true;
        let completedBatches = 0;

        // 翻译每个批次
        batches.forEach((batch, batchIndex) => {
            const batchText = batch.join('\n');

            translateText(batchText, function(translatedText, success) {
                let textIndex = 0;
                // 空值检查
                if (!translatedText) {
                    console.error('翻译返回空值:', {
                        batchText,
                        batchIndex,
                        translationApi
                    });
                    results[textIndex] = batchText; // 使用原文
                    success = false;
                } else {
                    // 分割翻译结果
                    const translatedParts = translatedText.split('\n');
                    
                    // 将结果放入正确的位置
                    for (let i = 0; i < batchIndex; i++) {
                        textIndex += batches[i].length;
                    }

                    for (let i = 0; i < Math.min(batch.length, translatedParts.length); i++) {
                        results[textIndex + i] = translatedParts[i];
                    }
                }
                
                totalSuccess = totalSuccess && success;
                completedBatches++;

                // 所有批次都完成后,返回结果
                if (completedBatches === batches.length) {
                    callback(results, totalSuccess);
                }
            });
        });
    }

    // 显示翻译内容
    function showTranslatedContent(element) {
        if (!element._translatedContent) return;

        // 保存当前的按钮容器
        const buttonContainer = element.querySelector('.translation-button-container');

        // 清空当前内容
        while (element.firstChild) {
            element.removeChild(element.firstChild);
        }

        // 添加翻译内容的所有子节点
        const fragment = document.createDocumentFragment();
        Array.from(element._translatedContent.childNodes).forEach(node => {
            fragment.appendChild(node.cloneNode(true));
        });

        element.appendChild(fragment);

        // 重新添加按钮容器
        if (buttonContainer) {
            element.appendChild(buttonContainer);
        }
    }

    // 显示原始内容
    function showOriginalContent(element) {
        if (!element._originalContent) return;

        // 保存当前的按钮容器
        const buttonContainer = element.querySelector('.translation-button-container');

        // 清空当前内容
        while (element.firstChild) {
            element.removeChild(element.firstChild);
        }

        // 添加原始内容的所有子节点
        const fragment = document.createDocumentFragment();
        Array.from(element._originalContent.childNodes).forEach(node => {
            fragment.appendChild(node.cloneNode(true));
        });

        element.appendChild(fragment);

        // 重新添加按钮容器
        if (buttonContainer) {
            element.appendChild(buttonContainer);
        }
    }

    // 重新翻译功能
    function retranslate(element, button) {
        // 如果当前是翻译状态,则重新触发翻译
        if (element.getAttribute(TRANSLATION_STATE_KEY) === 'translated') {
            // 先切换回原文
            toggleTranslation(element, button);
        }
        // 清除已有的翻译内容
        element._translatedContent = null;
        // 再触发翻译
        toggleTranslation(element, button);
    }

    // 收集元素中的所有文本节点
    function collectTextNodes(element) {
        const textNodes = [];
        const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);

        let node;
        while (node = walker.nextNode()) {
            const parent = node.parentNode;
            // 检查是否为需要跳过的元素或其子元素
            const shouldSkip = parent && (
                parent.tagName === 'SCRIPT' ||
                parent.tagName === 'STYLE' ||
                parent.closest('.translation-button-container') ||
                ignoredClasses.some(className => 
                    parent.classList.contains(className) || 
                    parent.closest('.' + className)
                )
            );
            
            if (parent && !shouldSkip) {
                const index = Array.from(parent.childNodes).indexOf(node);
                textNodes.push({
                    node: node,
                    parent: parent,
                    index: index
                });
            }
        }

        return textNodes;
    }
    

    // 翻译文本
    async function translateText(text, callback) {
        // console.log('翻译文本:', text);
        if (!text.trim()) {
            callback('',false);
            return;
        }
        
        try {
            let result;
            switch (translationApi) {
                case 'google':
                    result = await translate_gg(text, targetLanguage); break; 
                case 'baidu':
                    result = await translate_baidu(text, targetLanguage); break;
                case 'youdao_m':
                    result = await translate_youdao_mobile(text, targetLanguage); break;
                case 'iciba':
                    result = await translate_iciba(text, targetLanguage); break;
                case 'tencentai':
                    result = await translate_tencentai(text, targetLanguage); break;
                default:
                    result = await translate_gg(text, targetLanguage); break;
            }
            
            callback(result, true);
        } catch (error) {
            console.error('翻译出错:', error);
            callback(text, false); // 出错时返回原文
        }
    }

    //--谷歌翻译--start
    async function translate_gg(raw,targetLanguage='zh-CN') {
        const options = {
            method: "GET",
            url: `https://translate.google.com/translate_a/t?client=gtx&sl=auto&tl=zh-CN&q=` + encodeURIComponent(raw),
            anonymous: true,
            nocache: true,
        }
        return await BaseTranslate('谷歌翻译', raw, options, res => JSON.parse(res)[0][0])
    }
    //--百度翻译--start
    async function translate_baidu(raw, lang='zh') {
        if (!lang) {
            lang = await check_lang(raw);
        }
        if (lang == 'zh-CN') lang = 'zh';
        const options = {
            method: "POST",
            url: 'https://fanyi.baidu.com/ait/text/translate',
            data: JSON.stringify({ query: raw, from: lang, to: "zh" }),
            headers: {
                "referer": 'https://fanyi.baidu.com',
                'Content-Type': 'application/json',
                'Origin': 'https://fanyi.baidu.com',
                'accept': 'text/event-stream',
            },
        }
        return await BaseTranslate('百度翻译', raw, options, res => res.split('\n').filter(item => item.startsWith('data: ')).map(item => JSON.parse(item.slice(6))).find(item => item.data.list).data.list.map(item => item.dst).join('\n'))
    }
    async function check_lang(raw) {
        let t = Date.now();
        const options = {
            method: "POST",
            url: 'https://fanyi.baidu.com/langdetect',
            data: 'query=' + encodeURIComponent(raw.replace(/[\uD800-\uDBFF]$/, "").slice(0, 50)),
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            }
        }
        const res = await Request(options);
        //console.log(`语言加载完毕,耗时${Date.now()-t}ms`)
        try {
            return JSON.parse(res.responseText).lan
        } catch (err) {
            console.log(err);
            return
        }
    }
    //--腾讯AI翻译--start
    function guid() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }
    async function translate_tencentai(raw,targetLanguage='zh') {
        if(targetLanguage=='zh-CN') targetLanguage='zh';
        const data = {
            "header": {
                "fn": "auto_translation",
                "client_key": `browser-chrome-121.0.0-Windows_10-${guid()}-${Date.now()}`,
                "session": "",
                "user": ""
            },
            "type": "plain",
            "model_category": "normal",
            "text_domain": "",
            "source": {
                "lang": "auto",
                "text_list": [raw]
            },
            "target": {
                "lang": targetLanguage
            }
        }
        const options = {
            method: 'POST',
            url: 'https://transmart.qq.com/api/imt',
            data: JSON.stringify(data),
            headers: {
                'Content-Type': 'application/json',
                'Host': 'transmart.qq.com',
                'Origin': 'https://transmart.qq.com',
                'Referer': 'https://transmart.qq.com/'
            },
            anonymous: true,
            nocache: true,
        }
        return await BaseTranslate('腾讯AI翻译', raw, options, res => JSON.parse(res).auto_translation[0])
    }
    //--有道翻译m--start
    async function translate_youdao_mobile(raw, targetLanguage='') {
        const options = {
            method: "POST",
            url: 'http://m.youdao.com/translate',
            data: "inputtext=" + encodeURIComponent(raw) + "&type=AUTO",
            anonymous: true,
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            }
        }
        return await BaseTranslate('有道翻译mobile', raw, options, res => /id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(res)[1])
    }
    //--爱词霸翻译--start
    async function translate_iciba(raw, targetLanguage='zh') {
        if(targetLanguage=='zh-CN') targetLanguage='zh';
        const sign = CryptoJS.MD5("6key_web_fanyiifanyiweb8hc9s98e" + raw.replace(/(^\s*)|(\s*$)/g, "")).toString().substring(0, 16)
        const options = {
            method: "POST",
            url: `https://ifanyi.iciba.com/index.php?c=trans&m=fy&client=6&auth_user=key_web_fanyi&sign=${sign}`,
            data: `from=auto&t=${targetLanguage}&q=` + encodeURIComponent(raw),
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
        }
        return await BaseTranslate('爱词霸翻译', raw, options, res => JSON.parse(res).content.out)
    }
    

    // 基础请求
    async function BaseTranslate(name, raw, options, processer) {
        const toDo = async () => {
            try {
                const { responseText } = await Request(options);
                // 响应内容检查
                if (!responseText) {
                    throw new Error('翻译API返回空响应');
                }
                // 处理器结果检查
                const result = await processer(responseText);
                if (!result) {
                    throw new Error('翻译处理器返回空结果');
                }
                queueMicrotask(() => sessionStorage.setItem(`${name}-${raw}`, result));
                return result;
            } catch (err) {
                console.error('翻译请求详细错误:', {
                    name,
                    raw,
                    error: err,
                    responseText: err.responseText,
                    api: translationApi
                });
                throw err;
            }
        };
        
        return await PromiseRetryWrap(toDo, { 
            RetryTimes: 3, 
            ErrProcesser: (err) => {
                console.error('翻译重试失败:', err);
                throw err; // 抛出错误而不是返回原文
            }
        });
    }
    // 基础请求函数
    function Request(options) {
        return new Promise((resolve, reject) => {
            const timeout = options.timeout || 10000; // 默认10秒超时
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), timeout);
    
            GM_xmlhttpRequest({
                ...options,
                signal: controller.signal,
                onload: (response) => {
                    clearTimeout(timeoutId);
                    resolve(response);
                },
                onerror: (error) => {
                    clearTimeout(timeoutId);
                    reject(error);
                },
                ontimeout: () => {
                    clearTimeout(timeoutId);
                    reject(new Error('请求超时'));
                }
            });
        });
    }

    // 重试包装函数
    async function PromiseRetryWrap(func, { RetryTimes = 3, ErrProcesser = null, baseDelay = 1000 } = {}) {
        let lastError;
        
        for (let i = 0; i < RetryTimes; i++) {
            try {
                return await func();
            } catch (error) {
                lastError = error;
                if (i < RetryTimes - 1) {
                    // 使用指数退避策略,延迟时间随重试次数增加
                    const delay = baseDelay * Math.pow(2, i);
                    await new Promise(resolve => setTimeout(resolve, delay));
                }
            }
        }
        
        return ErrProcesser ? ErrProcesser(lastError) : Promise.reject(lastError);
    }

    // 添加全局样式
    function addStyles() {
        // 检查是否已添加样式
        if (document.getElementById('translation-button-styles')) {
            return;
        }

        const styleElement = document.createElement('style');
        styleElement.id = 'translation-button-styles';
        styleElement.textContent = `
            .translation-button-container {
                top: 0;
                right: 0;
                position: absolute;
                z-index: 9999;
                pointer-events: none; /* 容器本身不接收点击事件 */
            }

            .translation-toggle-button {
                padding: 1px 5px;
                font-size: 12px;
                cursor: pointer;
                background-color: rgba(40, 80, 107, 0.3); 
                color: rgba(96, 182, 231, 0.3);
                border: none;
                border-radius: 4px;
                transition: all 0.3s ease;
                box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
                pointer-events: auto; /* 按钮可以接收点击事件 */
                transform-origin: center;
            }
            .translation-toggle-button:hover {
                background-color: rgba(40, 80, 107, 1); /* 悬停时不透明 */
                box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
                color: rgba(96, 182, 231, 1);
            }
            .translation-toggle-button:active {
                transform: scale(0.95); /* 点击时缩小效果 */
                box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
                background-color: rgba(102, 192, 244, 0.7);
                color: white;
            }
            .translation-toggle-button:disabled {
                background-color: rgba(102, 192, 244, 0.5);
                cursor: not-allowed;
                color: rgba(27, 40, 56, 1);
            }
            .translation-toggle-button.error {
                background-color: rgba(200, 51, 51, 0.7);
                color: rgba(243, 236, 236, 0.7);
            }
            .translation-toggle-button.error:hover {
                background-color: rgba(200, 51, 51, 1);
                color: rgb(245, 234, 234);
            }

            /* 右键菜单样式 */
            .translation-context-menu {
                position: absolute;
                background-color: rgba(72, 101, 130, 0.9);
                border: 1px solid #2a4b62;
                border-radius: 4px;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
                color: #d6d8db;
                padding: 2px;
                z-index: 10000;
                min-width: 50px;
                display: none;
            }
            
            .translation-context-menu-item {
                padding: 1px 5px;
                cursor: pointer;
                font-size: 13px;
                transition: background-color 0.2s;
            }
            
            .translation-context-menu-item:hover {
                background-color: rgb(30, 47, 68);
            }
            
            .translation-context-menu-separator {
                height: 1px;
                background-color: #8b9fb4;
                margin: 2px;
            }

            /* 设置面板样式 */
            .translation-settings-panel {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: rgba( 33, 49, 68, 0.9);
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
                z-index: 10001;
                min-width: 300px;
            }
            
            .translation-settings-title {
                margin: 0 0 15px 0;
                border-bottom: 1px solid #eee;
                padding-bottom: 10px;
            }
            
            .translation-settings-close-btn {
                position: absolute;
                top: 10px;
                right: 10px;
                background: none;
                border: none;
                font-size: 20px;
                cursor: pointer;
                color: #999;
            }
            
            .translation-settings-content {
                margin-bottom: 15px;
            }
            
            .translation-settings-option {
                margin-bottom: 10px;
            }
            
            .translation-settings-option label {
                display: block;
                margin-bottom: 5px;
            }
            
            .translation-settings-option select {
                width: 100%;
                padding: 5px;
                background-color: #7d90a4;
            }
            
            .translation-settings-save-btn {
                padding: 8px 15px;
                background-color: #486582;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                margin-top: 15px;
            }
            
            .translation-settings-save-btn:hover {
                background-color: #6E8FAF;
            }

            /* 按钮定位调整 */
            .commentthread_comment_text {
                overflow-y: unset;
            }
            .blotter_group_announcement_content,
            .review_area_content {
                overflow: unset;
            }
            .game_area_description .translation-button-container {
                top: 0;
                bottom: unset;
            }
            .workshopItemDescription .translation-button-container,
            .commentthread_comment_content .translation-button-container {
                top: unset;
                right: 30px;
                bottom: 100%; 
            }
            .apphub_CardContentMain .translation-button-container {
                top: 5px;
                right: 5px; 
                bottom: unset;
            }
            .EventDetailsBody .translation-button-container,
            .workshopItemCollectionContainer .translation-button-container {
                top: 5px;
                right: 100px; 
                bottom: unset;
            }
            .announcement .translation-button-container,
            .ChatMessageBlock .translation-button-container{
                top: 5px;
                right: 15px;
                bottom: unset;
            }
            .subSections .translation-button-container {
                top: 15px;
                right: 5px;
                bottom: unset;
            }
            .guideTopContent .translation-button-container {
                top: 8px;
                right: 160px; 
                bottom: unset;
            }
            .forum_op .translation-button-container {
                top: 50px;
                right: 12px; 
                bottom: unset;
            }
            .rightcol .translation-button-container {
                top: 60px;
                right: 10px; 
                bottom: unset;
            }
            .profile_summary .translation-button-container,
            .devnotes .translation-button-container,
            .shortcol .translation-button-container {
                top: 0;
                right: 10px; 
                bottom: unset;
            }
            .game_description_snippet .translation-button-container{
                top: 0;
                right: 3px; 
                bottom: unset;
            }
            .recommendation .translation-button-container {
                top: 12px;
                right: 80px;
                bottom: unset; 
            }
            .curator_review .translation-button-container {
                top: 0;
                right: unset;
                bottom: unset; 
            }
            .blotter_group_announcement_content .translation-button-container{
                top: -30px;
                right: 10px;
                bottom: unset;
            }
            .review_area_content .translation-button-container,
            ._3NW5vEM9HgfQrgR4W-Xy_s .translation-button-container {
                top: -22px;
                right: 0;
                bottom: unset; 
            }

        `;

        document.head.appendChild(styleElement);
    }

    // 启动脚本
    init();
})();

QingJ © 2025

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