// ==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://gf.qytechs.cn/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);
});
})();