嗚呦翻譯機

即時翻譯嗚呦!

目前為 2023-07-17 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
    });
})();