[SNOLAB] [Mulango] myTyping Game Translator

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

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         [SNOLAB] [Mulango] myTyping Game Translator
// @namespace    https://userscript.snomiao.com/
// @author       [email protected]
// @version      0.2.1
// @description  [SNOLAB] [Mulango] Translate Japenese to the second language of your browser.
// @match        https://typing.twi1.me/game/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twi1.me
// @grant        none
// ==/UserScript==
/*
    Tested Pages:
        https://typing.twi1.me/game/79902
 */

(async function () {
    const translate = await useTranslator(navigator.languages[1]);
    questionsLoop().then();
    questionsLoopZh().then();
    typingLoop().then();
    speakingLoop().then();

    async function questionsLoop() {
        while (1) {
            const cls = ".questions .kanji:not(.translated)";
            const e = document.querySelector(cls);
            if (e) {
                e.classList.add("translated");
                const transcript = await translate(e.textContent);
                e.innerHTML = e.innerHTML + "\t(" + transcript;
                continue;
            }
            await new Promise((r) => setTimeout(r, 32)); // TODO: upgrade this into Observer Object
        }
    }
    async function questionsLoopZh() {
        while (1) {
            const sel = ".questions .kanji.translated:not(.translatedzh)"; // translate users' lang first
            const e = document.querySelector(sel);
            if (e) {
                e.classList.add("translatedzh");
                const ts2 = await translate(e.textContent, "zh");
                e.setAttribute("title", ts2);
                continue;
            }
            await new Promise((r) => setTimeout(r, 100)); // TODO: upgrade this into Observer Object
        }
    }
    async function speakingLoop() {
        const changed = edger("");
        while (1) {
            const e = document.querySelector(".mtjGmSc-kana");
            if (e && changed(e.textContent)) {
                document.querySelector(".mtjGmSc-kana").style = "color: #DDD";
                await speak(e.textContent);
            }
            await new Promise((r) => setTimeout(r, 100)); // TODO: upgrade this into Observer Object
        }
    }
    async function typingLoop() {
        const changed = edger("");
        while (1) {
            const e = document.querySelector(".mtjGmSc-kanji");
            if (e && !translated(e) && changed(e.textContent)) {
                document.querySelector(".mtjGmSc-roma").style = "display: none";
                const textContent = e.textContent;
                await kanjiTranscriptReplace(e, textContent);
                const transcript = await translate(textContent);
                await kanjiTranscriptReplace(e, transcript);
                const transcriptZH = await translate(textContent, "zh");
                await kanjiTranscriptReplace(e, transcript, transcriptZH);
            }
            await new Promise((r) => setTimeout(r, 100)); // TODO: upgrade this into Observer Object
        }

        function translated(e) {
            return e.querySelector(".kanji-transcript");
        }
    }
    async function speakAndTranslate(s) {
        return await translate(await speaked(s));
        k;
    }
})();

async function kanjiTranscriptReplace(e, transcript, title) {
    const style =
        "width: 100%;text-align: center;background: white;position: relative;z-index: 1;";
    e.querySelector(".kanji-transcript")?.remove();
    const div = document.createElement("div");
    div.className = "kanji-transcript";
    div.innerHTML = transcript;
    div.style = style;
    title && div.setAttribute("title", title);
    e.appendChild(div);
    await new Promise((r) => setTimeout(r, 1));
}

async function useTranslator(initLang = navigator.language) {
    const translateAPI = (
        await import(
            "https://cdn.skypack.dev/@snomiao/google-translate-api-browser"
        )
    ).setCORS("https://google-translate-cors.vercel.app/api?url=", {
        encode: true,
    });
    const translate = async (s, lang = initLang) => {
        if (!s) return;
        return await translateAPI(s, { to: lang.replace(/-.*/, "") })
            .then((e) => e.text)
            .catch(console.error);
    };
    return localforageCached(limiter(translate, 1e3));
}
function validPipor(fn) {
    // requires the first param is not undefined otherwise return the undefined
    return (s, ...args) => (s === undefined ? undefined : fn(s, ...args));
}
function limiter(fn, wait = 1e3, last = 0) {
    return async (...args) => {
        const remain = () => last + wait - +new Date();
        while (remain() > 0) await new Promise((r) => setTimeout(r, remain()));
        const r = await fn(...args);
        last = +new Date();
        return r;
    };
}
function edger(init) {
    return (e) => (e !== init ? (init = e) : undefined);
}
function watcher(fetcher, listener, interval = 16) {
    const changed = edger();
    return async () => {
        while (1) {
            await validPipor(listener)(changed(await fetcher()));
            await new Promise((r) => setTimeout(r, interval));
        }
    };
}


async function localforageCached(fn) {
    const hash = (s) => s.slice(0, 16) + s.slice(-16);
    const { default: cache } = await import(
        "https://cdn.skypack.dev/@luudjanssen/localforage-cache"
    );
    const in3day = 86400e3 * 3;
    const cacheName = hash(String(fn));
    const cacheInstance = cache.createInstance({
        name: cacheName,
        defaultExpiration: in3day,
    });
    return validPipor(cachedFn);
    async function cachedFn(...args) {
        const result =
            (await cacheInstance?.getItem(JSON.stringify(args))) ||
            (await fn(...args));
        await cacheInstance?.setItem(JSON.stringify(args), result); //refresh cache
        return result;
    }
}

async function speaked(text) {
    return (
        speechSynthesis.speak(
            Object.assign(new SpeechSynthesisUtterance(), { text, lang: "ja" })
        ),
        text
    );
}