- // ==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.2
- // @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 (https://github.com/Florrim/license/blob/main/LICENSE.md)
- // @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 () {
- "use strict";
-
- // 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);
- });
- })();