Japanese WaniKani dashboard

Translate the WaniKani dashboard into 日本語

// ==UserScript==
// @name         Japanese WaniKani dashboard
// @namespace    ddonovan
// @version      0.2
// @description  Translate the WaniKani dashboard into 日本語
// @author       Djack Donovan
// @copyright    2021 Djack Donovan
// @include      /^https://(www|preview).wanikani.com/*
// @icon         https://www.wanikani.com/favicon.ico
// @run-at       document-end
// @grant       none
// ==/UserScript==

if (!window.wkof) {
    alert('Japanese WaniKani dashboard script requires Wanikani Open Framework.\nYou will now be forwarded to installation instructions.');
    window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
    return;
}

function replaceInText(element, pattern, replacement) {
    for (let node of element.childNodes) {
        switch (node.nodeType) {
            case Node.ELEMENT_NODE:
            case Node.DOCUMENT_NODE:
                replaceInText(node, pattern, replacement);
                break;
            case Node.TEXT_NODE:
                node.textContent = node.textContent.replace(pattern, replacement);
                break;
        }
    }
}

var substitutionGroups = [
    // main buttons
    { class: 'lessons-and-reviews__lessons-button', substitutions: [
        { level: 26, source: 'Lessons', target: '授業' },
    ] },
    { class: 'lessons-and-reviews__reviews-button', substitutions: [
        { level: 26, source: 'Reviews', target: '復習' },
    ] },

    // review forecast
    { class: 'forecast', substitutions: [
        { level: 26, source: 'Review Forecast', target: '復習予報' },
        { level: 18, source: 'Review Forecast', target: 'Review 予報' },
    ] },
    { class: 'review-forecast__day-label', substitutions: [
        { level: 18, source: 'Monday', target: '月曜日' },
        { level: 18, source: 'Tuesday', target: '火曜日' },
        { level: 18, source: 'Wednesday', target: '水曜日' },
        { level: 18, source: 'Thursday', target: '木曜日' },
        { level: 18, source: 'Friday', target: '金曜日' },
        { level: 18, source: 'Saturday', target: '土曜日' },
        { level: 18, source: 'Sunday', target: '日曜日' },
        { level: 3, source: 'Today', target: '今日' },
    ] },
    { class: 'review-forecast__hour', substitutions: [
        { level: 6, source: '12 am', target: '午前0時' },
        { level: 6, source: '12 pm', target: '午後0時' },
        { level: 6, source: /(\d+) am/g, target: '午前$1時' },
        { level: 6, source: /(\d+) pm/g, target: '午後$1時' },
    ] },

    // progress panel
    { class: 'progress-component', substitutions: [
        { level: 11, source: /Level (\d+) Progress/g, target: '$1級進行' },
        { level: 0, source: /(\d+) of (\d+)/g, target: '$2つに$1つ' },
        { level: 23, source: 'kanji passed', target: '漢字を心得る' },
        { level: 10, source: 'kanji passed', target: '漢字 passed' },
        { level: 9, source: 'Radicals', target: '部首' },
        { level: 10, source: 'Kanji', target: '漢字' },
        { level: 15, source: 'Vocabulary', target: '単語' },
    ] },

    // nav bar
    { class: 'navigation-shortcut--lessons', substitutions: [
        { level: 26, source: 'Lessons', target: '授業' },
    ] },
    { class: 'navigation-shortcut--reviews', substitutions: [
        { level: 26, source: 'Reviews', target: '復習' },
    ] },
    { class: 'sitemap__section-header', substitutions: [
        { level: 0, source: 'Levels', target: 'レベル' },
        { level: 9, source: 'Radicals', target: '部首' },
        { level: 10, source: 'Kanji', target: '漢字' },
        { level: 15, source: 'Vocabulary', target: '単語' },
        { level: 0, source: 'Help', target: 'ヘルプ' },
    ] },

    // lesson page
    { id: 'lessons-summary', substitutions: [
        { level: 9, source: 'Radicals', target: '部首' },
        { level: 10, source: 'Kanji', target: '漢字' },
        { level: 15, source: 'Vocabulary', target: '単語' },
        { level: 29, source: 'Lessons Summary', target: '授業の大略' },
        { level: 26, source: 'Lessons Summary', target: '授業 Summary' },
        { level: 10, source: 'Start Session', target: 'セッションを始めます' },
    ] },
    { id: 'supplement-nav', substitutions: [
        { level: 8, source: 'name', target: '名前' },
        { level: 10, source: 'found in kanji', target: '漢字に見付ける' },
        { level: 9, source: 'radicals', target: '部首' },
        { level: 11, source: 'meaning', target: '意味' },
        { level: 10, source: 'readings', target: '読み方' },
        { level: 14, source: 'examples', target: '例え' },
        { level: 10, source: 'reading', target: '読み方' },
        { level: 21, source: 'kanji composition', target: '漢字の分解' },
        { level: 10, source: 'kanji composition', target: '漢字 composition' },
        { level: 32, source: 'context', target: '文脈' }, // not WaniKani vocabulary
    ] },

    // review page
    { id: 'reviews-summary', substitutions: [
        { level: 9, source: 'Radicals', target: '部首' },
        { level: 10, source: 'Kanji', target: '漢字' },
        { level: 15, source: 'Vocabulary', target: '単語' },
        { level: 29, source: 'Reviews Summary', target: '復習の大略' },
        { level: 26, source: 'Reviews Summary', target: '復習 Summary' },
        { level: 10, source: 'Start Session', target: 'セッションを始めます' },
        { level: 21, source: 'Answered Correctly', target: '正解' },
        { level: 21, source: 'Answered Incorrectly', target: '不正解' },
    ] },
];

var level;

try {
    window.wkof.include('Apiv2');
    window.wkof.ready('Apiv2').then(fetch_data);
    function fetch_data() {
        window.wkof.Apiv2.fetch_endpoint('user').then(process_response);
    }
    function process_response(response) {
        level = response.data.level;
        runSubstitutions();
    }

    //let progressElement = document.getElementsByClassName('progress-component')[0];
    //let match = progressElement.innerHTML.match(/Level (\d+) Progress/);
    //level = parseInt(match[1]);
} catch {
    level = 50;
    window.addEventListener('load', function() {
        runSubstitutions();
    }, false);
}

function runSubstitutions()
{
    for (let substitutionGroup of substitutionGroups) {
        if (substitutionGroup.class)
        {
            let elements = document.getElementsByClassName(substitutionGroup.class);
            for (let element of elements) {
                for (let substitution of substitutionGroup.substitutions) {
                    if (level > substitution.level) {
                        replaceInText(element, substitution.source, substitution.target);
                    }
                }
            }
        }
        if (substitutionGroup.id)
        {
            let element = document.getElementById(substitutionGroup.id);
            if (element)
            {
                for (let substitution of substitutionGroup.substitutions) {
                    if (level > substitution.level) {
                        replaceInText(element, substitution.source, substitution.target);
                    }
                }
            }
        }
    }
}