[MWI]Auto Message Translator

Automatically translate English messages to your language in Milky Way Idle game

// ==UserScript==
// @name         [MWI]Auto Message Translator
// @name:zh-CN   [银河奶牛]自动消息翻译器(zh-CN)
// @name:ja      [MWI]自動メッセージ翻訳ツール
// @name:ko      [MWI]자동 메시지 번역기
// @namespace    https://cnb.cool/shenhuanjie/skyner-cn/tamper-monkey-script/mwi-message-translator-zh_cn
// @version      1.0.1
// @description  Automatically translate English messages to your language in Milky Way Idle game
// @description:zh-CN  在银河奶牛游戏中自动将英文消息翻译为中文
// @description:ja  Milky Way Idleゲームで英語のメッセージを日本語に自動翻訳します
// @description:ko  Milky Way Idle 게임에서 영어 메시지를 한국어로 자동 번역합니다
// @author       shenhuanjie
// @license      MIT
// @match        https://www.milkywayidle.com/game*
// @match        https://milkywayidle.com/game*
// @icon         https://www.milkywayidle.com/favicon.svg
// @homepage     https://cnb.cool/shenhuanjie/skyner-cn/tamper-monkey-script/mwi-message-translator-zh_cn
// @supportURL   https://cnb.cool/shenhuanjie/skyner-cn/tamper-monkey-script/mwi-message-translator-zh_cn
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      translate.googleapis.com
// @connect      translate.google.com
// @run-at       document-start
// @noframes
//
// @history      1.0.1 - Added support for more languages, improved translation reliability
// @history      1.0.0 - Initial release
// ==/UserScript==

(function() {
    'use strict';

    // ========== 全局配置 ==========
    const CONFIG = {
        enableConsoleLog: false,      // 控制台日志开关
        sourceLanguage: 'en',        // 源语言
        targetLanguage: 'zh-CN',     // 目标语言
        logLevel: 'INFO',            // 日志级别: DEBUG, INFO, WARNING, ERROR
        initialScanDelay: 1000,      // 初始扫描延迟(ms)
        rescanInterval: 5000,        // 重新扫描间隔(ms)
        maxTranslationDepth: 5,      // 最大翻译深度
        translationCacheSize: 100,   // 翻译缓存大小
        messageFormat: /【(.+?)】\s*[::]\s*(.+)/,  // 消息格式正则表达式

        // 类名前缀配置(用于模糊匹配)
        classNamePrefixes: {
            chatMessage: 'ChatMessage_chatMessage__',
            timestamp: 'ChatMessage_timestamp__',
            name: 'ChatMessage_name__',
            chatHistory: 'ChatHistory_chatHistory__',
            systemMessage: 'ChatMessage_systemMessage__'
        },

        // 翻译请求间隔(ms),避免请求过于频繁被封IP
        translationRequestDelay: 300,

        // 翻译选项
        translateSystemMessages: false, // 是否翻译系统消息
        skipEmojiMessages: true,       // 是否跳过包含表情符号的消息
        minTextLength: 2,               // 最小翻译文本长度
        skipPatterns: [                 // 跳过匹配这些模式的消息
            /^[::]$/,                  // 冒号
            /^[@#]\w+$/,                // @用户名或#标签
            /^[^\w\s]{2,}$/,            // 纯特殊字符
            /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/ // 表情符号
        ]
    };
    // =============================

    // 工具函数:根据类名前缀生成选择器
    function getClassSelector(prefix) {
        return `[class^="${prefix}"], [class*=" ${prefix}"]`;
    }

    // 翻译缓存,避免重复翻译相同内容
    const translationCache = new Map();

    // 上次翻译请求的时间戳
    let lastTranslationTime = 0;

    // 延迟函数,返回一个Promise,在指定的毫秒数后解析
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // 工具函数:日志记录
    function log(message, level = 'INFO') {
        if (!CONFIG.enableConsoleLog) return;

        if (!['DEBUG', 'INFO', 'WARNING', 'ERROR'].includes(level)) {
            level = 'INFO';
        }

        const logLevels = {
            'DEBUG': 0,
            'INFO': 1,
            'WARNING': 2,
            'ERROR': 3
        };

        if (logLevels[level] < logLevels[CONFIG.logLevel]) {
            return;
        }

        const logColor = {
            DEBUG: '#888',
            INFO: '#2196F3',
            WARNING: '#FFC107',
            ERROR: '#F44336'
        };

        console.log(`%c[Translator][${level}] ${message}`, `color: ${logColor[level]}`);
    }

    // 使用Google翻译移动版网页接口进行翻译(无需API KEY)
    async function translateText(text, sourceLang = CONFIG.sourceLanguage, targetLang = CONFIG.targetLanguage) {
        // 检查缓存
        if (translationCache.has(text)) {
            log(`使用缓存翻译: ${text.substring(0, 30)}...`, 'DEBUG');
            return translationCache.get(text);
        }

        // 如果文本为空或只包含空白字符,直接返回
        if (!text || text.trim() === '') {
            return text;
        }

        log(`翻译文本: ${text.substring(0, 30)}...`, 'DEBUG');

        try {
            // 实现请求延迟,避免请求过于频繁
            const now = Date.now();
            const timeSinceLastRequest = now - lastTranslationTime;
            if (timeSinceLastRequest < CONFIG.translationRequestDelay) {
                const waitTime = CONFIG.translationRequestDelay - timeSinceLastRequest;
                log(`等待 ${waitTime}ms 后发送翻译请求...`, 'DEBUG');
                await delay(waitTime);
            }

            // 更新最后请求时间
            lastTranslationTime = Date.now();

            // 构建Google翻译移动版网页请求URL
            const encodedText = encodeURIComponent(text);
            const url = `https://translate.google.com/m?sl=${sourceLang}&tl=${targetLang}&q=${encodedText}`;

            // 使用Promise包装GM_xmlhttpRequest
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    headers: {
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                    },
                    onload: function(response) {
                        if (response.status >= 200 && response.status < 300) {
                            resolve(response);
                        } else {
                            reject(new Error(`请求失败: ${response.status} ${response.statusText}`));
                        }
                    },
                    onerror: function(error) {
                        reject(error);
                    }
                });
            });

            // 从HTML中提取翻译结果
            const html = response.responseText;

            // 尝试多种可能的正则表达式模式来匹配翻译结果
            let translatedText = null;

            // 模式1: 移动版翻译结果容器
            const regex1 = /<div class="result-container">(.*?)<\/div>/s;
            const match1 = html.match(regex1);
            if (match1 && match1[1]) {
                translatedText = match1[1].trim();
            }

            // 模式2: 另一种可能的结果容器
            if (!translatedText) {
                const regex2 = /<div class="t0">(.*?)<\/div>/s;
                const match2 = html.match(regex2);
                if (match2 && match2[1]) {
                    translatedText = match2[1].trim();
                }
            }

            // 模式3: 另一种可能的结果容器
            if (!translatedText) {
                const regex3 = /<div class="translation">(.*?)<\/div>/s;
                const match3 = html.match(regex3);
                if (match3 && match3[1]) {
                    translatedText = match3[1].trim();
                }
            }

            // 如果所有模式都失败,尝试更通用的方法
            if (!translatedText) {
                // 查找任何可能包含翻译结果的div
                const resultDivRegex = /<div[^>]*>(.*?)<\/div>/gs;
                const allDivs = [...html.matchAll(resultDivRegex)];

                // 查找包含原文长度相似的div(可能是翻译结果)
                for (const divMatch of allDivs) {
                    const divContent = divMatch[1].trim();
                    // 排除太短或太长的内容
                    if (divContent && divContent.length > text.length * 0.5 && divContent.length < text.length * 2) {
                        translatedText = divContent;
                        break;
                    }
                }
            }

            // 如果仍然没有找到翻译结果,返回原文
            if (!translatedText) {
                log('无法从HTML中提取翻译结果', 'WARNING');
                return text;
            }

            // 清理HTML标签
            translatedText = translatedText.replace(/<[^>]*>/g, '');

            // 更新缓存
            if (translationCache.size >= CONFIG.translationCacheSize) {
                // 如果缓存已满,删除最早添加的项
                const firstKey = translationCache.keys().next().value;
                translationCache.delete(firstKey);
            }
            translationCache.set(text, translatedText);

            log(`翻译完成: ${text.substring(0, 20)}... -> ${translatedText.substring(0, 20)}...`, 'INFO');
            return translatedText;
        } catch (error) {
            log(`翻译失败: ${error.message}`, 'ERROR');
            return text; // 翻译失败时返回原文
        }
    }

    // 从聊天消息元素中提取用户名和消息内容
    function extractMessageInfo(chatMessageElement) {
        if (!chatMessageElement) return null;

        try {
            // 检查是否是系统消息
            if (chatMessageElement.classList.contains('ChatMessage_systemMessage__3Jz9e')) {
                // 系统消息通常只有一个span,直接包含在消息元素中
                const systemSpan = chatMessageElement.querySelector('span:not(.ChatMessage_timestamp__1iRZO)');
                if (systemSpan && systemSpan.textContent) {
                    return {
                        isSystemMessage: true,
                        username: 'System',
                        contentSpan: systemSpan,
                        originalContent: systemSpan.textContent
                    };
                }
                return null;
            }

            // 查找用户名元素
            const nameSelector = getClassSelector(CONFIG.classNamePrefixes.name);
            const nameElement = chatMessageElement.querySelector(nameSelector);

            if (!nameElement) {
                log('未找到用户名元素', 'DEBUG');
                return null;
            }

            // 提取用户名 - 用户名不需要翻译,所以只是提取文本
            const username = nameElement.textContent.trim();

            // 查找最后一个span元素,通常是消息内容
            // 这个方法更可靠,因为消息内容总是在最后
            const allSpans = Array.from(chatMessageElement.querySelectorAll('span'));

            // 过滤掉时间戳、用户名相关的span和已翻译的span
            const contentSpans = allSpans.filter(span => {
                // 跳过时间戳
                if (span.classList.toString().includes(CONFIG.classNamePrefixes.timestamp)) {
                    return false;
                }

                // 跳过包含用户名的span或其父元素
                if (span.contains(nameElement) || nameElement.contains(span)) {
                    return false;
                }

                // 跳过冒号span (通常紧跟用户名)
                if (span.textContent.trim() === ':' || span.textContent.trim() === ':') {
                    return false;
                }

                // 跳过已经被标记为翻译过的span
                if (span.hasAttribute('data-translated')) {
                    return false;
                }

                // 跳过包含在链接容器中的span
                const linkContainer = span.closest('.ChatMessage_linkContainer__18Kv3');
                if (linkContainer) {
                    return false;
                }

                // 跳过物品元素
                const itemContainer = span.closest('.Item_itemContainer__x7kH1');
                if (itemContainer) {
                    return false;
                }

                return span.textContent.trim() !== '';
            });

            // 如果找不到合适的内容span,返回null
            if (contentSpans.length === 0) {
                log('未找到合适的消息内容span', 'DEBUG');
                return null;
            }

            // 使用最后一个符合条件的span作为消息内容
            const contentSpan = contentSpans[contentSpans.length - 1];

            return {
                isSystemMessage: false,
                username,  // 用户名不翻译
                contentSpan,  // 只翻译消息内容span
                originalContent: contentSpan.textContent
            };
        } catch (error) {
            log(`提取消息信息时出错: ${error.message}`, 'ERROR');
            return null;
        }
    }

    // 处理单个聊天消息
    async function processChatMessage(chatMessageElement) {
        if (!chatMessageElement) return false;

        try {
            // 在DEBUG级别下显示消息结构
            if (CONFIG.logLevel === 'DEBUG') {
                debugMessageStructure(chatMessageElement);
            }

            const messageInfo = extractMessageInfo(chatMessageElement);
            if (!messageInfo) {
                log('无法提取消息信息', 'DEBUG');
                return false;
            }

            const { isSystemMessage, username, contentSpan, originalContent } = messageInfo;

            // 确保我们找到的是消息内容而不是用户名
            if (!contentSpan || !originalContent) {
                log('未找到有效的消息内容', 'WARNING');
                return false;
            }

            // 检查是否已翻译(添加一个标记属性)
            if (contentSpan.hasAttribute('data-translated')) {
                log(`跳过已翻译的消息: ${originalContent.substring(0, 20)}...`, 'DEBUG');
                return false;
            }

            // 跳过只包含表情符号、特殊字符或非英文内容的消息
            if (!/[a-zA-Z]{2,}/.test(originalContent)) {
                log(`跳过非英文内容: ${originalContent}`, 'DEBUG');
                contentSpan.setAttribute('data-translated', 'non-english');
                return false;
            }

            // 跳过系统消息(可选,根据需要配置)
            if (isSystemMessage && !CONFIG.translateSystemMessages) {
                log(`跳过系统消息: ${originalContent.substring(0, 20)}...`, 'DEBUG');
                contentSpan.setAttribute('data-translated', 'system');
                return false;
            }

            log(`准备翻译 ${username} 的消息内容: ${originalContent.substring(0, 30)}...`, 'DEBUG');

            // 只翻译消息内容,不翻译用户名
            const translatedContent = await translateText(originalContent);

            // 如果翻译结果与原文相同,则不做更改
            if (translatedContent === originalContent) {
                contentSpan.setAttribute('data-translated', 'same');
                log(`翻译结果与原文相同: ${originalContent.substring(0, 20)}...`, 'DEBUG');
                return false;
            }

            // 更新span内容
            contentSpan.textContent = translatedContent;
            contentSpan.setAttribute('data-translated', 'true');
            contentSpan.setAttribute('title', `原文: ${originalContent}`); // 添加原文作为提示

            log(`已翻译 ${username} 的消息内容: ${originalContent.substring(0, 20)}... -> ${translatedContent.substring(0, 20)}...`, 'INFO');
            return true;
        } catch (error) {
            log(`处理聊天消息时出错: ${error.message}`, 'ERROR');
            return false;
        }
    }

    // 扫描并翻译聊天消息
    async function scanAndTranslate() {
        log('开始扫描聊天消息...', 'INFO');
        const startTime = performance.now();

        try {
            // 使用类名前缀查找所有聊天消息元素
            const chatMessageSelector = getClassSelector(CONFIG.classNamePrefixes.chatMessage);
            const chatMessages = document.querySelectorAll(chatMessageSelector);

            log(`找到 ${chatMessages.length} 条聊天消息`, 'INFO');

            let totalTranslations = 0;
            for (const message of chatMessages) {
                const translated = await processChatMessage(message);
                if (translated) {
                    totalTranslations++;
                }
            }

            const elapsedTime = performance.now() - startTime;
            log(`扫描完成: ${totalTranslations} 处翻译,耗时 ${elapsedTime.toFixed(2)}ms`, 'INFO');
            return totalTranslations;
        } catch (error) {
            log(`扫描过程中出错: ${error.message}`, 'ERROR');
            return 0;
        }
    }

    // 处理新添加的节点
    async function processAddedNode(node) {
        if (!node || node.nodeType !== Node.ELEMENT_NODE) return;

        try {
            // 检查节点是否是聊天消息
            const chatMessageSelector = getClassSelector(CONFIG.classNamePrefixes.chatMessage);
            if (node.matches && node.matches(chatMessageSelector)) {
                await processChatMessage(node);
                return;
            }

            // 查找节点内的所有聊天消息
            const chatMessages = node.querySelectorAll(chatMessageSelector);
            for (const message of chatMessages) {
                await processChatMessage(message);
            }
        } catch (error) {
            log(`处理新添加节点时出错: ${error.message}`, 'ERROR');
        }
    }

    // 检查元素是否在聊天历史区域内
    function isInChatHistory(element) {
        if (!element) return false;

        // 向上查找聊天历史容器
        let current = element;
        const chatHistorySelector = getClassSelector(CONFIG.classNamePrefixes.chatHistory);

        while (current && current !== document.body) {
            if (current.matches && current.matches(chatHistorySelector)) {
                return true;
            }
            current = current.parentElement;
        }

        return false;
    }

    // 防抖函数,避免频繁处理
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            const context = this;
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(context, args), wait);
        };
    }

    // 处理DOM变化的防抖函数
    const debouncedProcessMutations = debounce(async (mutations) => {
        let hasNewMessages = false;

        for (const mutation of mutations) {
            if (mutation.type === 'childList') {
                // 处理新增节点
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE && isInChatHistory(node)) {
                        await processAddedNode(node);
                        hasNewMessages = true;
                    }
                }
            } else if (mutation.type === 'characterData') {
                // 处理文本内容变更
                const textNode = mutation.target;
                if (textNode && textNode.nodeType === Node.TEXT_NODE && isInChatHistory(textNode)) {
                    // 查找包含此文本节点的聊天消息元素
                    let current = textNode.parentNode;
                    const chatMessageSelector = getClassSelector(CONFIG.classNamePrefixes.chatMessage);

                    while (current && current !== document.body) {
                        if (current.matches && current.matches(chatMessageSelector)) {
                            await processChatMessage(current);
                            hasNewMessages = true;
                            break;
                        }
                        current = current.parentElement;
                    }
                }
            }
        }

        if (hasNewMessages) {
            log('处理了新的聊天消息', 'DEBUG');
        }
    }, 300); // 300ms防抖延迟

    // 初始化函数
    function init() {
        log('翻译器初始化中...', 'INFO');

        try {
            // 初始延迟扫描,等待页面完全加载
            setTimeout(async () => {
                log('执行初始聊天消息扫描...', 'INFO');
                const initialTranslations = await scanAndTranslate();
                log(`初始扫描完成,翻译了 ${initialTranslations} 条消息`, 'INFO');

                // 动态监听DOM变化
                const observer = new MutationObserver(mutations => {
                    debouncedProcessMutations(mutations);
                });

                // 查找聊天历史容器并监听其变化
                const chatHistorySelector = getClassSelector(CONFIG.classNamePrefixes.chatHistory);
                const chatHistoryElements = document.querySelectorAll(chatHistorySelector);

                if (chatHistoryElements.length > 0) {
                    for (const element of chatHistoryElements) {
                        observer.observe(element, {
                            childList: true,
                            subtree: true,
                            characterData: true
                        });
                        log(`开始监听聊天历史容器: ${element.className}`, 'INFO');
                    }
                } else {
                    // 如果找不到聊天历史容器,则监听整个body
                    observer.observe(document.body, {
                        childList: true,
                        subtree: true,
                        characterData: true
                    });
                    log('未找到聊天历史容器,监听整个页面', 'WARNING');
                }

                log('翻译器已启动并监听DOM变化', 'INFO');
            }, CONFIG.initialScanDelay);

            // 定期重新扫描整个DOM
            setInterval(async () => {
                await scanAndTranslate();
            }, CONFIG.rescanInterval);

            log(`翻译器配置: 源语言=${CONFIG.sourceLanguage}, 目标语言=${CONFIG.targetLanguage}, 扫描间隔=${CONFIG.rescanInterval/1000}s`, 'INFO');
        } catch (error) {
            log(`初始化失败: ${error.message}`, 'ERROR');
        }
    }

    // 添加翻译样式
    function addTranslationStyles() {
        const styleElement = document.createElement('style');
        styleElement.textContent = `
            span[data-translated="true"] {
                color: #4CAF50 !important;
                text-decoration: underline dotted #4CAF50;
                position: relative;
            }

            span[data-translated="true"]:hover::after {
                content: attr(title);
                position: absolute;
                bottom: 100%;
                left: 0;
                background: rgba(0, 0, 0, 0.8);
                color: white;
                padding: 4px 8px;
                border-radius: 4px;
                font-size: 12px;
                white-space: pre-wrap;
                max-width: 300px;
                z-index: 1000;
            }

            .translator-status {
                position: fixed;
                bottom: 10px;
                right: 10px;
                background: rgba(33, 150, 243, 0.8);
                color: white;
                padding: 5px 10px;
                border-radius: 4px;
                font-size: 12px;
                z-index: 10000;
                display: none;
                transition: opacity 0.3s;
            }
        `;
        document.head.appendChild(styleElement);
    }

    // 添加状态指示器
    function addStatusIndicator() {
        const statusDiv = document.createElement('div');
        statusDiv.className = 'translator-status';
        statusDiv.textContent = '翻译器已启动';
        document.body.appendChild(statusDiv);

        // 显示状态指示器几秒钟,然后淡出
        setTimeout(() => {
            statusDiv.style.display = 'block';
            setTimeout(() => {
                statusDiv.style.opacity = '0';
                setTimeout(() => {
                    statusDiv.style.display = 'none';
                }, 1000);
            }, 3000);
        }, 1000);

        return statusDiv;
    }

    // 更新状态指示器
    function updateStatus(message, duration = 3000) {
        const statusDiv = document.querySelector('.translator-status') || addStatusIndicator();
        statusDiv.textContent = message;
        statusDiv.style.display = 'block';
        statusDiv.style.opacity = '1';

        setTimeout(() => {
            statusDiv.style.opacity = '0';
            setTimeout(() => {
                statusDiv.style.display = 'none';
            }, 1000);
        }, duration);
    }

    // 启动脚本
    function startScript() {
        // 添加样式
        addTranslationStyles();

        // 初始化翻译器
        init();

        // 添加状态指示器
        addStatusIndicator();

        // 打印初始状态信息
        if (CONFIG.enableConsoleLog) {
            console.log('%c[Translator] 翻译器已加载,控制台日志已开启', 'color: #2196F3');
        } else {
            console.log('%c[Translator] 翻译器已加载,控制台日志已关闭', 'color: #888');
        }
    }

    // 根据页面加载状态启动脚本
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', startScript);
    } else {
        startScript();
    }

    // 调试函数:显示消息结构
    function debugMessageStructure(chatMessageElement) {
        if (!CONFIG.enableConsoleLog || CONFIG.logLevel !== 'DEBUG') return;

        try {
            console.group('消息结构调试');
            console.log('消息元素:', chatMessageElement);

            // 查找用户名元素
            const nameSelector = getClassSelector(CONFIG.classNamePrefixes.name);
            const nameElement = chatMessageElement.querySelector(nameSelector);
            console.log('用户名元素:', nameElement);

            if (nameElement) {
                console.log('用户名文本:', nameElement.textContent.trim());
            }

            // 查找所有span元素
            const spans = chatMessageElement.querySelectorAll('span');
            console.log('所有span元素:', spans);

            // 查找可能的消息内容span
            for (let i = 0; i < spans.length; i++) {
                const span = spans[i];
                console.log(`Span ${i}:`, {
                    element: span,
                    text: span.textContent.trim(),
                    classes: span.className,
                    containsUserName: nameElement && (span.contains(nameElement) || nameElement.contains(span))
                });
            }

            console.groupEnd();
        } catch (error) {
            console.error('调试消息结构时出错:', error);
        }
    }

    // 导出一些函数到全局作用域,方便调试
    window.messageTranslator = {
        translate: translateText,
        scan: scanAndTranslate,
        updateStatus: updateStatus,
        config: CONFIG,
        debug: {
            messageStructure: debugMessageStructure,
            extractMessageInfo: extractMessageInfo
        }
    };
})();

QingJ © 2025

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