Woomy Translator

Translates woomy in real time

当前为 2023-07-17 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Woomy Translator
// @name:es      Traductor Woomy
// @name:zh-TW   嗚呦翻譯機
// @name:nl      Woomy Vertaler
// @name:ja      ウーミー翻訳機
// @name:ru      Вуми Переводчик
// @description        Translates woomy in real time
// @description:es     ¡Traduce woomy en tiempo real!
// @description:zh-TW  即時翻譯嗚呦!
// @description:nl     Vertaalt woomy in realtime!
// @description:ja     ウーミーをリアルタイムで翻訳!
// @description:ru     Переводит "Вуми" в режиме реального времени!
// @version      1.0
// @author       PowfuArras // Discord: @xskt
// @match        https://woomy.app/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=woomy.app
// @grant        none
// @run-at       document-start
// @license      FLORRIM DEVELOPER GROUP LICENSE
// @namespace https://greasyfork.org/users/951187
// ==/UserScript==

// TODO:
// Fix specific translation issues. For example, "x42" in stats becomes "x 42 ". Probably some smart trimming will do.
// Make chat messages not translate, or atleast make it an option to disable. Not sure how I would do this. Maybe hook into color mixing and work my way backwards?

(function () {
    // Allowed languages supported by Google
    const languages = [ { "language": "Afrikaans", "code": "af" }, { "language": "Albanian", "code": "sq" }, { "language": "Amharic", "code": "am" }, { "language": "Arabic", "code": "ar" }, { "language": "Armenian", "code": "hy" }, { "language": "Assamese", "code": "as" }, { "language": "Aymara", "code": "ay" }, { "language": "Azerbaijani", "code": "az" }, { "language": "Bambara", "code": "bm" }, { "language": "Basque", "code": "eu" }, { "language": "Belarusian", "code": "be" }, { "language": "Bengali", "code": "bn" }, { "language": "Bhojpuri", "code": "bho" }, { "language": "Bosnian", "code": "bs" }, { "language": "Bulgarian", "code": "bg" }, { "language": "Catalan", "code": "ca" }, { "language": "Cebuano", "code": "ceb" }, { "language": "Chinese (Simplified)", "code": "zh" }, { "language": "Chinese (Traditional)", "code": "zh-TW" }, { "language": "Corsican", "code": "co" }, { "language": "Croatian", "code": "hr" }, { "language": "Czech", "code": "cs" }, { "language": "Danish", "code": "da" }, { "language": "Dhivehi", "code": "dv" }, { "language": "Dogri", "code": "doi" }, { "language": "Dutch", "code": "nl" }, { "language": "English", "code": "en" }, { "language": "Esperanto", "code": "eo" }, { "language": "Estonian", "code": "et" }, { "language": "Ewe", "code": "ee" }, { "language": "Filipino (Tagalog)", "code": "fil" }, { "language": "Finnish", "code": "fi" }, { "language": "French", "code": "fr" }, { "language": "Frisian", "code": "fy" }, { "language": "Galician", "code": "gl" }, { "language": "Georgian", "code": "ka" }, { "language": "German", "code": "de" }, { "language": "Greek", "code": "el" }, { "language": "Guarani", "code": "gn" }, { "language": "Gujarati", "code": "gu" }, { "language": "Haitian Creole", "code": "ht" }, { "language": "Hausa", "code": "ha" }, { "language": "Hawaiian", "code": "haw" }, { "language": "Hebrew", "code": "he" }, { "language": "Hindi", "code": "hi" }, { "language": "Hmong", "code": "hmn" }, { "language": "Hungarian", "code": "hu" }, { "language": "Icelandic", "code": "is" }, { "language": "Igbo", "code": "ig" }, { "language": "Ilocano", "code": "ilo" }, { "language": "Indonesian", "code": "id" }, { "language": "Irish", "code": "ga" }, { "language": "Italian", "code": "it" }, { "language": "Japanese", "code": "ja" }, { "language": "Javanese", "code": "jv" }, { "language": "Kannada", "code": "kn" }, { "language": "Kazakh", "code": "kk" }, { "language": "Khmer", "code": "km" }, { "language": "Kinyarwanda", "code": "rw" }, { "language": "Konkani", "code": "gom" }, { "language": "Korean", "code": "ko" }, { "language": "Krio", "code": "kri" }, { "language": "Kurdish", "code": "ku" }, { "language": "Kurdish (Sorani)", "code": "ckb" }, { "language": "Kyrgyz", "code": "ky" }, { "language": "Lao", "code": "lo" }, { "language": "Latin", "code": "la" }, { "language": "Latvian", "code": "lv" }, { "language": "Lingala", "code": "ln" }, { "language": "Lithuanian", "code": "lt" }, { "language": "Luganda", "code": "lg" }, { "language": "Luxembourgish", "code": "lb" }, { "language": "Macedonian", "code": "mk" }, { "language": "Maithili", "code": "mai" }, { "language": "Malagasy", "code": "mg" }, { "language": "Malay", "code": "ms" }, { "language": "Malayalam", "code": "ml" }, { "language": "Maltese", "code": "mt" }, { "language": "Maori", "code": "mi" }, { "language": "Marathi", "code": "mr" }, { "language": "Meiteilon (Manipuri)", "code": "mni-Mtei" }, { "language": "Mizo", "code": "lus" }, { "language": "Mongolian", "code": "mn" }, { "language": "Myanmar (Burmese)", "code": "my" }, { "language": "Nepali", "code": "ne" }, { "language": "Norwegian", "code": "no" }, { "language": "Nyanja (Chichewa)", "code": "ny" }, { "language": "Odia (Oriya)", "code": "or" }, { "language": "Oromo", "code": "om" }, { "language": "Pashto", "code": "ps" }, { "language": "Persian", "code": "fa" }, { "language": "Polish", "code": "pl" }, { "language": "Portuguese (Portugal, Brazil)", "code": "pt" }, { "language": "Punjabi", "code": "pa" }, { "language": "Quechua", "code": "qu" }, { "language": "Romanian", "code": "ro" }, { "language": "Russian", "code": "ru" }, { "language": "Samoan", "code": "sm" }, { "language": "Sanskrit", "code": "sa" }, { "language": "Scots Gaelic", "code": "gd" }, { "language": "Sepedi", "code": "nso" }, { "language": "Serbian", "code": "sr" }, { "language": "Sesotho", "code": "st" }, { "language": "Shona", "code": "sn" }, { "language": "Sindhi", "code": "sd" }, { "language": "Sinhala (Sinhalese)", "code": "si" }, { "language": "Slovak", "code": "sk" }, { "language": "Slovenian", "code": "sl" }, { "language": "Somali", "code": "so" }, { "language": "Spanish", "code": "es" }, { "language": "Sundanese", "code": "su" }, { "language": "Swahili", "code": "sw" }, { "language": "Swedish", "code": "sv" }, { "language": "Tagalog (Filipino)", "code": "tl" }, { "language": "Tajik", "code": "tg" }, { "language": "Tamil", "code": "ta" }, { "language": "Tatar", "code": "tt" }, { "language": "Telugu", "code": "te" }, { "language": "Thai", "code": "th" }, { "language": "Tigrinya", "code": "ti" }, { "language": "Tsonga", "code": "ts" }, { "language": "Turkish", "code": "tr" }, { "language": "Turkmen", "code": "tk" }, { "language": "Twi (Akan)", "code": "ak" }, { "language": "Ukrainian", "code": "uk" }, { "language": "Urdu", "code": "ur" }, { "language": "Uyghur", "code": "ug" }, { "language": "Uzbek", "code": "uz" }, { "language": "Vietnamese", "code": "vi" }, { "language": "Welsh", "code": "cy" }, { "language": "Xhosa", "code": "xh" }, { "language": "Yiddish", "code": "yi" }, { "language": "Yoruba", "code": "yo" }, { "language": "Zulu", "code": "zu" } ];
    let currentLanguage = languages[languages.findIndex(language => language.code === "en")];

    // A map to store translations, so we dont need to retranslate every time we need to draw text
    const cache = new Map();

    // Native drawing functions, used to actually draw text later
    const natives = {
        fillText: CanvasRenderingContext2D.prototype.fillText,
        strokeText: CanvasRenderingContext2D.prototype.strokeText,
        measureText: CanvasRenderingContext2D.prototype.measureText,
        fillTextOffscreen: OffscreenCanvasRenderingContext2D.prototype.fillText,
        strokeTextOffscreen: OffscreenCanvasRenderingContext2D.prototype.strokeText,
        measureTextOffscreen: OffscreenCanvasRenderingContext2D.prototype.measureText
    };

    // Regex stuff that helps us with identifying numbers and sentences
    const regex = {
        isNumber: /^\d+(?:\.\d+)?(?:[a-zA-Z]{1,2})?$/,
        chunks: /(\d+(?:\.\d+)?(?:[a-zA-Z]{1,2})?)/g
    };
    const util = {
        isNumber: text => regex.isNumber.test(text),
        chunkify: text => text.split(regex.chunks).filter(Boolean)
    };

    // Hook into text drawing apply our own modifications
    CanvasRenderingContext2D.prototype.fillText = function (text, x, y, maxWidth) {
        natives.fillText.call(this, transmutateText(text), x, y, maxWidth);
    };
    CanvasRenderingContext2D.prototype.strokeText = function (text, x, y, maxWidth) {
        natives.strokeText.call(this, transmutateText(text), x, y, maxWidth);
    };
    CanvasRenderingContext2D.prototype.measureText = function (text) {
        return natives.measureText.call(this, transmutateText(text));
    };
    OffscreenCanvasRenderingContext2D.prototype.fillText = function (text, x, y, maxWidth) {
        natives.fillTextOffscreen.call(this, transmutateText(text), x, y, maxWidth);
    };
    OffscreenCanvasRenderingContext2D.prototype.strokeText = function (text, x, y, maxWidth) {
        natives.strokeTextOffscreen.call(this, transmutateText(text), x, y, maxWidth);
    };
    OffscreenCanvasRenderingContext2D.prototype.measureText = function (text) {
        return natives.measureTextOffscreen.call(this, transmutateText(text));
    };

    // Translate a string into our desired language.
    // Stores it in cache after the fact
    function translate(text) {
        // If we have not came across this text yet...
        if (!cache.has(text)) {
            // Placeholder while we wait for Google.
            cache.set(text, "...");

            // Finally, actually translate the text.
            // Send a post request to google translate api and then parse it into something we can use.
            fetch(`https://translate.googleapis.com/translate_a/single?${new URLSearchParams({
                client: "gtx",
                sl: "en",
                tl: currentLanguage.code,
                dt: "t",
                dj: "1",
                source: "input",
                q: text,
            })}`).then(function (data) {
                return data.json();
            }).then(function (json) {
                // Score!
                cache.set(text, json.sentences.reduce((acc, value) => `${acc}${value.trans}`, ""));
            });
        }

        // Return text from cache
        return cache.get(text);
    }

    // Apply transmutations to text for translation
    function transmutateText(text) {
        // We dont need to do anything with this :D
        if (text.length === 0) return text;
        if (util.isNumber(text)) return text;

        // Split the text into multiple chunks of numbers and strings
        const chunks = util.chunkify(text);
        let output = "";

        // For each chunk...
        for (let i = 0; i < chunks.length; i++) {
            const chunk = chunks[i];

            // If it is a number, than dont do anything
            // else translate it and append it to our output
            if (util.isNumber(chunk)) output += ` ${chunks[i]} `;
            else output += translate(chunk);
        }
        return output;
    }

    // Constantly try to hook into the settings menu, and once it does clear the interval
    window.addEventListener("load", function () {
        const interval = setInterval(function () {
            // Try, try try try!
            try {
                // Create our own little element for the settings menu
                const element = document.getElementById("Woomy_backgroundAnimation").parentElement.cloneNode(true);

                // We got it now, clear that mf!
                clearInterval(interval);

                // Make it fancy
                const select = element.children[0];
                element.childNodes[0].textContent = "Language: ";
                select.style.maxWidth = "140px";
                select.id = "PowfuArras_language";

                // Apply the valid languages
                select.innerHTML = languages.map(language => `<option value=${language.code}>${language.language}</option>`);

                // Listen in for the user trying to change it
                // When they do, clear the cache and update the current language
                select.addEventListener("change", function (event) {
                    cache.clear();
                    currentLanguage = languages[languages.findIndex(language => language.code === event.target.value)];
                });

                // Set default
                element.children[0].selectedIndex = languages.findIndex(language => language.code === currentLanguage.code);
                element.dispatchEvent(new Event("change"));

                // Insert it into the settings menu
                document.querySelectorAll(".optionsFlexHolder")[0].appendChild(element);
            } catch (error) {}
        }, 100);
    });
})();