// ==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);
})();