[SNOLAB] [Mulango] myTyping Game Translator

[SNOLAB] [Mulango] Translate Japenese to the second language of your browser.

  1. // ==UserScript==
  2. // @name [SNOLAB] [Mulango] myTyping Game Translator
  3. // @namespace https://userscript.snomiao.com/
  4. // @author snomiao@gmail.com
  5. // @version 0.2.1
  6. // @description [SNOLAB] [Mulango] Translate Japenese to the second language of your browser.
  7. // @match https://typing.twi1.me/game/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=twi1.me
  9. // @grant none
  10. // ==/UserScript==
  11. /*
  12. Tested Pages:
  13. https://typing.twi1.me/game/79902
  14. */
  15.  
  16. (async function () {
  17. const translate = await useTranslator(navigator.languages[1]);
  18. questionsLoop().then();
  19. questionsLoopZh().then();
  20. typingLoop().then();
  21. speakingLoop().then();
  22.  
  23. async function questionsLoop() {
  24. while (1) {
  25. const cls = ".questions .kanji:not(.translated)";
  26. const e = document.querySelector(cls);
  27. if (e) {
  28. e.classList.add("translated");
  29. const transcript = await translate(e.textContent);
  30. e.innerHTML = e.innerHTML + "\t(" + transcript;
  31. continue;
  32. }
  33. await new Promise((r) => setTimeout(r, 32)); // TODO: upgrade this into Observer Object
  34. }
  35. }
  36. async function questionsLoopZh() {
  37. while (1) {
  38. const sel = ".questions .kanji.translated:not(.translatedzh)"; // translate users' lang first
  39. const e = document.querySelector(sel);
  40. if (e) {
  41. e.classList.add("translatedzh");
  42. const ts2 = await translate(e.textContent, "zh");
  43. e.setAttribute("title", ts2);
  44. continue;
  45. }
  46. await new Promise((r) => setTimeout(r, 100)); // TODO: upgrade this into Observer Object
  47. }
  48. }
  49. async function speakingLoop() {
  50. const changed = edger("");
  51. while (1) {
  52. const e = document.querySelector(".mtjGmSc-kana");
  53. if (e && changed(e.textContent)) {
  54. document.querySelector(".mtjGmSc-kana").style = "color: #DDD";
  55. await speak(e.textContent);
  56. }
  57. await new Promise((r) => setTimeout(r, 100)); // TODO: upgrade this into Observer Object
  58. }
  59. }
  60. async function typingLoop() {
  61. const changed = edger("");
  62. while (1) {
  63. const e = document.querySelector(".mtjGmSc-kanji");
  64. if (e && !translated(e) && changed(e.textContent)) {
  65. document.querySelector(".mtjGmSc-roma").style = "display: none";
  66. const textContent = e.textContent;
  67. await kanjiTranscriptReplace(e, textContent);
  68. const transcript = await translate(textContent);
  69. await kanjiTranscriptReplace(e, transcript);
  70. const transcriptZH = await translate(textContent, "zh");
  71. await kanjiTranscriptReplace(e, transcript, transcriptZH);
  72. }
  73. await new Promise((r) => setTimeout(r, 100)); // TODO: upgrade this into Observer Object
  74. }
  75.  
  76. function translated(e) {
  77. return e.querySelector(".kanji-transcript");
  78. }
  79. }
  80. async function speakAndTranslate(s) {
  81. return await translate(await speaked(s));
  82. k;
  83. }
  84. })();
  85.  
  86. async function kanjiTranscriptReplace(e, transcript, title) {
  87. const style =
  88. "width: 100%;text-align: center;background: white;position: relative;z-index: 1;";
  89. e.querySelector(".kanji-transcript")?.remove();
  90. const div = document.createElement("div");
  91. div.className = "kanji-transcript";
  92. div.innerHTML = transcript;
  93. div.style = style;
  94. title && div.setAttribute("title", title);
  95. e.appendChild(div);
  96. await new Promise((r) => setTimeout(r, 1));
  97. }
  98.  
  99. async function useTranslator(initLang = navigator.language) {
  100. const translateAPI = (
  101. await import(
  102. "https://cdn.skypack.dev/@snomiao/google-translate-api-browser"
  103. )
  104. ).setCORS("https://google-translate-cors.vercel.app/api?url=", {
  105. encode: true,
  106. });
  107. const translate = async (s, lang = initLang) => {
  108. if (!s) return;
  109. return await translateAPI(s, { to: lang.replace(/-.*/, "") })
  110. .then((e) => e.text)
  111. .catch(console.error);
  112. };
  113. return localforageCached(limiter(translate, 1e3));
  114. }
  115. function validPipor(fn) {
  116. // requires the first param is not undefined otherwise return the undefined
  117. return (s, ...args) => (s === undefined ? undefined : fn(s, ...args));
  118. }
  119. function limiter(fn, wait = 1e3, last = 0) {
  120. return async (...args) => {
  121. const remain = () => last + wait - +new Date();
  122. while (remain() > 0) await new Promise((r) => setTimeout(r, remain()));
  123. const r = await fn(...args);
  124. last = +new Date();
  125. return r;
  126. };
  127. }
  128. function edger(init) {
  129. return (e) => (e !== init ? (init = e) : undefined);
  130. }
  131. function watcher(fetcher, listener, interval = 16) {
  132. const changed = edger();
  133. return async () => {
  134. while (1) {
  135. await validPipor(listener)(changed(await fetcher()));
  136. await new Promise((r) => setTimeout(r, interval));
  137. }
  138. };
  139. }
  140.  
  141.  
  142. async function localforageCached(fn) {
  143. const hash = (s) => s.slice(0, 16) + s.slice(-16);
  144. const { default: cache } = await import(
  145. "https://cdn.skypack.dev/@luudjanssen/localforage-cache"
  146. );
  147. const in3day = 86400e3 * 3;
  148. const cacheName = hash(String(fn));
  149. const cacheInstance = cache.createInstance({
  150. name: cacheName,
  151. defaultExpiration: in3day,
  152. });
  153. return validPipor(cachedFn);
  154. async function cachedFn(...args) {
  155. const result =
  156. (await cacheInstance?.getItem(JSON.stringify(args))) ||
  157. (await fn(...args));
  158. await cacheInstance?.setItem(JSON.stringify(args), result); //refresh cache
  159. return result;
  160. }
  161. }
  162.  
  163. async function speaked(text) {
  164. return (
  165. speechSynthesis.speak(
  166. Object.assign(new SpeechSynthesisUtterance(), { text, lang: "ja" })
  167. ),
  168. text
  169. );
  170. }