ChatGPT Translator

This script automatically translates chat messages to the user's local language and also translates user messages before sending them in English.

// ==UserScript==
// @name         ChatGPT Translator
// @description  This script automatically translates chat messages to the user's local language and also translates user messages before sending them in English.
// @namespace    ChatGPTTranslator
// @version      1.8.1
// @author       CriDos
// @match        https://chat.openai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chat.openai.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @license      MIT
// ==/UserScript==

'use strict';

console.log(`ChatGPT Translator initializing...`);

let debug = false;

setInterval(() => {
    try {
        addAutoTranslate();
    } catch (error) {
        console.error(error);
    }

    try {
        findAndHookTextareaElement();
    } catch (error) {
        console.error(error);
    }
}, 100);

function addAutoTranslate() {
    var messages = document.querySelectorAll(".markdown.prose");

    for (var i = 0; i < messages.length; i++) {
        const msgMarkdownNode = messages[i];
        const parentMsgMarkdown = msgMarkdownNode.parentElement;

        if (parentMsgMarkdown.isAutoTranslate) {
            continue;
        }
        parentMsgMarkdown.isAutoTranslate = true;

        setInterval(async () => {
            await translateNode(msgMarkdownNode);
        }, 500);
    }
}

function findAndHookTextareaElement() {
    const targetElement = document.querySelector("textarea");
    if (targetElement === null) {
        return;
    }

    if (targetElement.isAddHookKeydownEvent === true) {
        return;
    }

    targetElement.isAddHookKeydownEvent = true;

    console.log(`Textarea element found. Adding keydown event listener.`);
    targetElement.addEventListener("keydown", async event => await handleSubmit(event, targetElement), true);
}

async function handleSubmit(event, targetElement) {
    console.log(`Keydown event detected: type - ${event.type}, key - ${event.key}`);

    if (event.shiftKey && event.key === "Enter") {
        return;
    }

    if (window.isActiveOnSubmit === true) {
        return;
    }

    if (event.key === "Enter") {
        window.isActiveOnSubmit = true;
        event.stopImmediatePropagation();

        const request = targetElement.value;
        targetElement.value = "";

        const translatedText = await translate(request, navigator.language, "en", "text");

        targetElement.focus();
        targetElement.value = translatedText;
        const enterEvent = new KeyboardEvent("keydown", {
            bubbles: true,
            cancelable: true,
            key: "Enter",
            code: "Enter"
        });
        targetElement.dispatchEvent(enterEvent);

        window.isActiveOnSubmit = false;
    }
}

async function translateNode(node) {
    const translateClassName = "translate-markdown";
    const parentNode = node.parentElement;
    const nodeContent = $(node).html();

    if (node.storeContent == nodeContent) {
        return;
    }
    node.storeContent = nodeContent;

    var translateNode = parentNode.querySelector(`.${translateClassName}`);
    if (translateNode == null) {
        translateNode = node.cloneNode(true);
        translateNode.classList.add(translateClassName);
        parentNode.insertBefore(translateNode, parentNode.firstChild);
    }

    var translatedContent = await translateHTML(nodeContent, "auto", navigator.language)
    translatedContent = translatedContent.replace(/<\/div>$/, '');

    const endOfTranslationText = await translate("end of translation", "en", navigator.language, "text");

    $(translateNode).html(translatedContent + `<p>.......... ${endOfTranslationText} ..........</p></div>`);
}

async function translateHTML(html, sLang, tLang) {
    const excludeTagRegex = /<(pre|code)[^>]*>([\s\S]*?)<\/(pre|code)>/g;
    const excludeTags = [];
    const excludePlaceholder = 'e0x1c';

    let htmlContent = html;

    let excludeTagsMatch;
    while (excludeTagsMatch = excludeTagRegex.exec(html)) {
        excludeTags.push(excludeTagsMatch[0]);
        htmlContent = htmlContent.replace(excludeTagsMatch[0], `<${excludePlaceholder}${excludeTags.length - 1}>`);
    }

    if (debug) {
        console.log(`preTranslateHTML: ${html}`);
    }

    htmlContent = await translate(htmlContent, sLang, tLang);

    for (let i = 0; i < excludeTags.length; i++) {
        htmlContent = htmlContent.replace(`<${excludePlaceholder}${i}>`, excludeTags[i]);
    }

    if (debug) {
        console.log(`postTranslateHTML: ${htmlContent}`);
    }

    return htmlContent;
}

const cache = {};

async function translate(text, sLang, tLang, format, key) {
    try {
        if (debug) {
            console.log(`preTranslate: ${text}`);
        }

        if (format == null) {
            format = "html";
        }

        if (key == null) {
            key = "AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw";
        }

        const cacheKey = text + '_' + format;

        if (cache[cacheKey]) {
            return cache[cacheKey];
        }

        const response = await new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: `https://translate.googleapis.com/translate_a/t?client=gtx&format=${format}&sl=${sLang}&tl=${tLang}&key=${key}`,
                data: `q=${encodeURIComponent(text)}`,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded"
                },
                onload: response => {
                    const json = JSON.parse(response.responseText);
                    if (Array.isArray(json[0])) {
                        resolve(json[0][0]);
                    } else {
                        resolve(json[0]);
                    }
                },
                onerror: response => {
                    reject(response.statusText);
                }
            });
        });

        cache[cacheKey] = response;

        if (debug) {
            console.log(`postTranslate: ${response}`);
        }

        return response;
    } catch (error) {
        console.error(error);
    }
}