您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将 WhatsApp 选中的文本翻译成中文
// ==UserScript== // @name WhatsApp Translator // @name:zh-CN WhatsApp 翻译器 // @namespace http://tampermonkey.net/ // @version 1.3.0 // @description Translate selected WhatsApp messages // @description:zh-CN 将 WhatsApp 选中的文本翻译成中文 // @author HeT // @match https://web.whatsapp.com/* // @grant GM_xmlhttpRequest // @connect translator-api-lovat.vercel.app // @license MIT // ==/UserScript== (function() { 'use strict'; const targetLang = 'en'; // 默认翻译成英文 const serverUrl = 'https://translator-api-lovat.vercel.app/api/translate'; document.body.addEventListener('click', function (e) { const bubble = e.target.closest('.message-in, .message-out'); if (!bubble) return; const messageTextElement = bubble.querySelector('span.selectable-text'); if (!messageTextElement) return; const originalText = messageTextElement.innerText.trim(); if (!originalText) return; translate(originalText, false, (translated) => { if (translated) { messageTextElement.innerText = translated; } }); }); // ========== 1. 添加翻译输入框 ========== function addTranslateBox() { const chatFooter = document.querySelector('footer'); if (!chatFooter || document.getElementById('translator-input')) return; const translateBox = document.createElement('textarea'); translateBox.id = 'translator-input'; translateBox.placeholder = '在这里输入中文,然后猛敲回车键会翻译成英文并填充上方输入框...'; translateBox.style.width = '100%'; translateBox.style.height = '40px'; translateBox.style.marginTop = '5px'; translateBox.style.padding = '5px'; translateBox.style.border = '1px solid #ccc'; translateBox.style.borderRadius = '6px'; translateBox.style.resize = 'none'; chatFooter.appendChild(translateBox); // ========== 3. 回车事件 ========== translateBox.addEventListener('keydown', (e) => { if (e.key === 'Enter') { if (e.ctrlKey) { // Ctrl+Enter 换行 e.preventDefault(); const start = translateBox.selectionStart; const end = translateBox.selectionEnd; translateBox.value = translateBox.value.substring(0, start) + "\n" + translateBox.value.substring(end); translateBox.selectionStart = translateBox.selectionEnd = start + 1; } else { e.preventDefault(); const text = translateBox.value.trim(); if (!text) return; translate(text, true, (translated) => { if (translated) { fillWhatsAppInput(translated); translateBox.value = ''; // 清空下方输入框 } }); } } }); } function getWAInput() { return document.querySelector('[data-testid="conversation-compose-box-input"]') || document.querySelector('footer div[contenteditable="true"][role="textbox"]') || document.querySelector('footer [contenteditable="true"][data-tab]'); } // ========== 4. 填充 WhatsApp 输入框 ========== function fillWhatsAppInput(text, keepFocusBelow = true) { const waInput = getWAInput(); const lowerBox = document.getElementById('translator-input'); if (!waInput) return; // 先聚焦到上方输入框,才能进行选择/替换 //waInput.focus(); // 方式一:选中全部 → insertText(大多数版本最稳) const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(waInput); sel.removeAllRanges(); sel.addRange(range); const ok = document.execCommand('insertText', false, text); // 触发 React/Lexical 的 input 监听 waInput.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: text })); // 如果 insertText 不可靠(或内容未被替换),执行兜底:composition + DOM 覆盖 const notReplaced = waInput.innerText.trim() !== text.trim(); if (!ok || notReplaced) { // 启动一次“输入法”合成事件,许多编辑器会借此刷新内部状态 waInput.dispatchEvent(new CompositionEvent('compositionstart', { bubbles: true, cancelable: true, data: '' })); // 直接覆盖 DOM(按行生成节点,避免纯 textContent 被还原) waInput.innerHTML = ''; text.split('\n').forEach((line, i) => { if (i > 0) waInput.appendChild(document.createElement('br')); waInput.appendChild(document.createTextNode(line)); }); // 结束“输入法”并再次触发 input waInput.dispatchEvent(new CompositionEvent('compositionend', { bubbles: true, cancelable: true, data: text })); waInput.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: text })); } // 默认把焦点抢回到下方自建输入框 if (keepFocusBelow && lowerBox) { setTimeout(() => lowerBox.focus(), 0); } } function translate(text, isInput, callback) { GM_xmlhttpRequest({ method: 'POST', url: serverUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ q: text, to: isInput ? targetLang : 'zh-CHS' }), onload: function(response) { try { const data = JSON.parse(response.responseText); console.log(data); if (data.translation && data.translation.length) { console.log(data.translation); callback(data.translation[0]); } else { console.error("翻译API返回异常", data); callback(null); } } catch (e) { console.error("解析返回数据失败", e, response.responseText); callback(null); } }, onerror: function(err) { console.error("请求失败", err); callback(null); } }); } // ========== 入口 ========== setInterval(() => { addTranslateBox(); }, 2000); // ============================== // 🔥 预热 API,避免第一次延迟 // ============================== setTimeout(() => { translate("ping", () => { }); }, 2000); })();