// ==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 // @downloadURL https://update.greasyfork.icu/scripts/535748/%5BMWI%5DAuto%20Message%20Translator.user.js // @updateURL https://update.greasyfork.icu/scripts/535748/%5BMWI%5DAuto%20Message%20Translator.meta.js // ==/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>/s; const match1 = html.match(regex1); if (match1 && match1[1]) { translatedText = match1[1].trim(); } // 模式2: 另一种可能的结果容器 if (!translatedText) { const regex2 = /
(.*?)<\/div>/s; const match2 = html.match(regex2); if (match2 && match2[1]) { translatedText = match2[1].trim(); } } // 模式3: 另一种可能的结果容器 if (!translatedText) { const regex3 = /
(.*?)<\/div>/s; const match3 = html.match(regex3); if (match3 && match3[1]) { translatedText = match3[1].trim(); } } // 如果所有模式都失败,尝试更通用的方法 if (!translatedText) { // 查找任何可能包含翻译结果的div const resultDivRegex = /]*>(.*?)<\/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 } }; })();