WhatsApp 翻译器

将 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);

})();