[Userscript] Lesson Furigana

Hi, this is my first user script. It’s probably badly implemented. I would like to share early with you all in case you find it useful. After some polish, I can publish it like other scripts.

If you know about implementing user scripts, I would appreciate your help in improving it.

Motivation: I would like to simplify the lessons view, I would rather collapse all tabs into a single one. As a first step I think the ‘reading’ section could be replaced with furigana and it might be worth to exist as its own user script.

This only applies to lessons, during learning, never when asked for input!

How it looks:

Note that the furigana for Vocabulary items is clickable and will play the audio!

image

image

Multiple readings:

image

Code:

I’m a software engineer but I don’t work with js or frontend and this is my first user script. You will laugh, but I wrote most of this with help from chatgpt!

The script is more complicated than it should because I use Omega Reorder and had to make sure the furigana gets applied after it, and only once. Without the observer hooks my changes would be lost after the reorder kicks in. If there is a better way please let me know.

"use strict";
// ==UserScript==
// @name         Wanikani: Furigana in Lesson
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  Adds Furigana to lessons
// @author       tomzalt
// @match        https://www.wanikani.com/subject-lessons*
// @match        https://preview.wanikani.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Inject custom CSS for furigana styling
    var style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = `
        .custom-furigana rt {
            font-size: 0.25em; /* Adjust the size as needed */
        }

        ruby {
          display: inline-flex;
          flex-direction: column-reverse;
          align-items: center;
        }
    `;
    document.head.appendChild(style);

    var observer; // Declare observer globally

    function applyFurigana() {
        var kanjiElements = document.querySelectorAll('.character-header__characters:not(.furigana-applied)');

        // For vocabulary
        var vocabularyHiraganaElements =
          Array
            .from(document.querySelectorAll('.reading-with-audio__reading'));

        // For kanji
        const kanjiHiraganaElements =
          Array
            .from(document.querySelectorAll('#reading .subject-slide__aside p'));

        var hiraganaElements = kanjiHiraganaElements.concat(vocabularyHiraganaElements);

        kanjiElements.forEach((kanjiElement, index) => {
            if (index < hiraganaElements.length) {
                kanjiElement.classList.add('furigana-applied'); // Mark as processed
                updateOrCreateFurigana(kanjiElement, hiraganaElements[index]);
            }
        });
    }

    function updateOrCreateFurigana(kanjiElement, hiraganaElement) {
        var existingRuby = kanjiElement.closest('ruby');
        if (existingRuby) {
            // Update the existing ruby element
            var rtElement = existingRuby.querySelector('rt');
            if (rtElement) {
                rtElement.innerText = hiraganaElement.innerText;
            } else {
                rtElement = document.createElement('rt');
                rtElement.innerText = hiraganaElement.innerText;
                existingRuby.appendChild(rtElement);
            }
        } else {
            // Create a new ruby element
            setFurigana(kanjiElement, hiraganaElement);
        }
    }

    var toggleVoiceActor = false; // To alternate between the two actors

    function makeFuriganaClickable(rtElement) {
        rtElement.style.cursor = 'pointer'; // Optional: change cursor on hover
        rtElement.addEventListener('click', function() {
            var buttons = document.querySelectorAll('.reading-with-audio__control');
            if (buttons.length >= 2) {
                // Assuming the buttons are always in the same order
                var buttonToClick = buttons[toggleVoiceActor ? 0 : 1];
                buttonToClick.click(); // Trigger the button's action
                toggleVoiceActor = !toggleVoiceActor; // Toggle the voice actor for next click
            }
        });
    }

    function setFurigana(kanjiElement, hiraganaElement) {
        // Disconnect the observer before making changes
        observer.disconnect();

        var hiraganaText = hiraganaElement.innerText;
        var rubyElement = document.createElement('ruby');
        rubyElement.innerText = kanjiElement.innerText;

        // Copy styles and classes from the original kanji element to the ruby element
        rubyElement.style.cssText = kanjiElement.style.cssText;
        rubyElement.className = kanjiElement.className;
        rubyElement.className += ' custom-furigana'; // Add custom class

        var rtElement = document.createElement('rt');
        rtElement.innerText = hiraganaText;
        makeFuriganaClickable(rtElement);

        rubyElement.appendChild(rtElement);
        kanjiElement.parentNode.replaceChild(rubyElement, kanjiElement);

        console.log("set furigana");

        // Reconnect the observer after changes
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Mutation observer to reapply changes if the DOM updates
    observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.type === 'childList') {
                applyFurigana();
            }
        });
    });

    // Start observing the target node for configured mutations
    observer.observe(document.body, { childList: true, subtree: true });

    // Initial application
    applyFurigana();
})();
3 Likes

Thanks for taking the time to create and post this. I’m sure this is something some folks will find useful!

For me personally, I wouldn’t want the first time a lesson is shown to me to reveal the reading before I’ve had a chance to hazard a guess. I say that because one of the things I’ve noticed that is quite nice is even though I’m only a couple of days out from being level 10, I’m already able to accurately guess the reading for more than half of the vocabulary the first time I’ve seen it. I’ve read on the forums here from multiple people who have made it to level 60 that ability only gets better as you keep leveling up.

Nevertheless, this type of user-contributed customization is one of the things I really like about WaniKani overall as it lets people selectively change things to suit their personal preferences better.

5 Likes

@tomzalt I think adding an options menu could be a good idea, where users can choose whether they want the furigana to display instantly/in the first lesson or not. :thinking: That would deal with the problem that @MikeyDC65 mentions (on which I agree, btw) :slight_smile:

2 Likes

Yep, I also find the guessing game really useful for learning because it helps me practice the kanji during the lessons. I even try not to look at the translation and to guess what the translation would be.
Sharpen + research + place, I wonder what that is. Or eye + medicine + someone.

But I’m sorry if it sounded like I’m putting your down, it’s great that everyone’s learning method is different, and it’s amazing that you’re investing your time in improving the learning experience!

3 Likes