嗚呦翻譯機

即時翻譯嗚呦!

  1. // ==UserScript==
  2. // @name Woomy Translator
  3. // @name:es Traductor Woomy
  4. // @name:zh-TW 嗚呦翻譯機
  5. // @name:nl Woomy Vertaler
  6. // @name:ja ウーミー翻訳機
  7. // @name:ru Вуми Переводчик
  8. // @description Translates woomy in real time
  9. // @description:es ¡Traduce woomy en tiempo real!
  10. // @description:zh-TW 即時翻譯嗚呦!
  11. // @description:nl Vertaalt woomy in realtime!
  12. // @description:ja ウーミーをリアルタイムで翻訳!
  13. // @description:ru Переводит "Вуми" в режиме реального времени!
  14. // @version 1.2
  15. // @author PowfuArras // Discord: @xskt
  16. // @match https://woomy.app/
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=woomy.app
  18. // @grant none
  19. // @run-at document-start
  20. // @license FLORRIM DEVELOPER GROUP LICENSE (https://github.com/Florrim/license/blob/main/LICENSE.md)
  21. // @namespace https://greasyfork.org/users/951187
  22. // ==/UserScript==
  23.  
  24. // TODO:
  25. // Fix specific translation issues. For example, "x42" in stats becomes "x 42 ". Probably some smart trimming will do.
  26. // 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?
  27.  
  28. (function () {
  29. "use strict";
  30.  
  31. // Allowed languages supported by Google
  32. 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" } ];
  33. let currentLanguage = languages[languages.findIndex(language => language.code === "en")];
  34.  
  35. // A map to store translations, so we dont need to retranslate every time we need to draw text
  36. const cache = new Map();
  37.  
  38. // Native drawing functions, used to actually draw text later
  39. const natives = {
  40. fillText: CanvasRenderingContext2D.prototype.fillText,
  41. strokeText: CanvasRenderingContext2D.prototype.strokeText,
  42. measureText: CanvasRenderingContext2D.prototype.measureText,
  43. fillTextOffscreen: OffscreenCanvasRenderingContext2D.prototype.fillText,
  44. strokeTextOffscreen: OffscreenCanvasRenderingContext2D.prototype.strokeText,
  45. measureTextOffscreen: OffscreenCanvasRenderingContext2D.prototype.measureText
  46. };
  47.  
  48. // Regex stuff that helps us with identifying numbers and sentences
  49. const regex = {
  50. isNumber: /^\d+(?:\.\d+)?(?:[a-zA-Z]{1,2})?$/,
  51. chunks: /(\d+(?:\.\d+)?(?:[a-zA-Z]{1,2})?)/g
  52. };
  53. const util = {
  54. isNumber: text => regex.isNumber.test(text),
  55. chunkify: text => text.split(regex.chunks).filter(Boolean)
  56. };
  57.  
  58. // Hook into text drawing apply our own modifications
  59. CanvasRenderingContext2D.prototype.fillText = function (text, x, y, maxWidth) {
  60. natives.fillText.call(this, transmutateText(text), x, y, maxWidth);
  61. };
  62. CanvasRenderingContext2D.prototype.strokeText = function (text, x, y, maxWidth) {
  63. natives.strokeText.call(this, transmutateText(text), x, y, maxWidth);
  64. };
  65. CanvasRenderingContext2D.prototype.measureText = function (text) {
  66. return natives.measureText.call(this, transmutateText(text));
  67. };
  68. OffscreenCanvasRenderingContext2D.prototype.fillText = function (text, x, y, maxWidth) {
  69. natives.fillTextOffscreen.call(this, transmutateText(text), x, y, maxWidth);
  70. };
  71. OffscreenCanvasRenderingContext2D.prototype.strokeText = function (text, x, y, maxWidth) {
  72. natives.strokeTextOffscreen.call(this, transmutateText(text), x, y, maxWidth);
  73. };
  74. OffscreenCanvasRenderingContext2D.prototype.measureText = function (text) {
  75. return natives.measureTextOffscreen.call(this, transmutateText(text));
  76. };
  77.  
  78. // Translate a string into our desired language.
  79. // Stores it in cache after the fact
  80. function translate(text) {
  81. // If we have not came across this text yet...
  82. if (!cache.has(text)) {
  83. // Placeholder while we wait for Google.
  84. cache.set(text, "...");
  85.  
  86. // Finally, actually translate the text.
  87. // Send a post request to google translate api and then parse it into something we can use.
  88. fetch(`https://translate.googleapis.com/translate_a/single?${new URLSearchParams({
  89. client: "gtx",
  90. sl: "en",
  91. tl: currentLanguage.code,
  92. dt: "t",
  93. dj: "1",
  94. source: "input",
  95. q: text,
  96. })}`).then(function (data) {
  97. return data.json();
  98. }).then(function (json) {
  99. // Score!
  100. cache.set(text, json.sentences.reduce((acc, value) => `${acc}${value.trans}`, ""));
  101. });
  102. }
  103.  
  104. // Return text from cache
  105. return cache.get(text);
  106. }
  107.  
  108. // Apply transmutations to text for translation
  109. function transmutateText(text) {
  110. // We dont need to do anything with this :D
  111. if (text.length === 0) return text;
  112. if (util.isNumber(text)) return text;
  113.  
  114. // Split the text into multiple chunks of numbers and strings
  115. const chunks = util.chunkify(text);
  116. let output = "";
  117.  
  118. // For each chunk...
  119. for (let i = 0; i < chunks.length; i++) {
  120. const chunk = chunks[i];
  121.  
  122. // If it is a number, than dont do anything
  123. // else translate it and append it to our output
  124. if (util.isNumber(chunk)) output += ` ${chunks[i]} `;
  125. else output += translate(chunk);
  126. }
  127. return output;
  128. }
  129.  
  130. // Constantly try to hook into the settings menu, and once it does clear the interval
  131. window.addEventListener("load", function () {
  132. const interval = setInterval(function () {
  133. // Try, try try try!
  134. try {
  135. // Create our own little element for the settings menu
  136. const element = document.getElementById("Woomy_backgroundAnimation").parentElement.cloneNode(true);
  137.  
  138. // We got it now, clear that mf!
  139. clearInterval(interval);
  140.  
  141. // Make it fancy
  142. const select = element.children[0];
  143. element.childNodes[0].textContent = "Language: ";
  144. select.style.maxWidth = "140px";
  145. select.id = "PowfuArras_language";
  146.  
  147. // Apply the valid languages
  148. select.innerHTML = languages.map(language => `<option value=${language.code}>${language.language}</option>`);
  149.  
  150. // Listen in for the user trying to change it
  151. // When they do, clear the cache and update the current language
  152. select.addEventListener("change", function (event) {
  153. cache.clear();
  154. currentLanguage = languages[languages.findIndex(language => language.code === event.target.value)];
  155. });
  156.  
  157. // Set default
  158. element.children[0].selectedIndex = languages.findIndex(language => language.code === currentLanguage.code);
  159. element.dispatchEvent(new Event("change"));
  160.  
  161. // Insert it into the settings menu
  162. document.querySelectorAll(".optionsFlexHolder")[0].appendChild(element);
  163. } catch (error) {}
  164. }, 100);
  165. });
  166. })();