Originally, this script used the speech synthesis provided by the browser, but switched to using the Google translate hack in version 1.10. You could try using the old createAudioButton()
function from version 1.02. For convenience, here is the modified script (including the other fixes listed in this thread: my own fix, parts of @est_fills_cando’s modifications, and @rklie’s fix):
Script
"use strict";
// ==UserScript==
// @name Advanced Context Sentence
// @namespace https://openuserjs.org/users/abdullahalt
// @version 1.40
// @description Enhance the context sentence section, highlighting kanji and adding audio
// @author abdullahalt
// @match https://www.wanikani.com/lesson/session
// @match https://www.wanikani.com/review/session
// @match https://www.wanikani.com/vocabulary/*
// @require https://unpkg.com/@popperjs/core@2.5.4/dist/umd/popper.min.js
// @require https://unpkg.com/tippy.js@6.2.7/dist/tippy-bundle.umd.min.js
// @grant none
// @copyright 2019, abdullahalt (https://openuserjs.org//users/abdullahalt)
// @license MIT
// ==/UserScript==
// ==OpenUserJS==
// @author abdullahalt
// ==/OpenUserJS==
(() => {
//--------------------------------------------------------------------------------------------------------------//
//-----------------------------------------------INITIALIZATION-------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
const wkof = window.wkof;
const scriptId = "AdvancedContextSentence";
const scriptName = "Advanced Context Sentence";
const vocabularyPage = "/vocabulary";
const recognizedSelector = "a.recognized";
const unrecognizedSelector = "a.unrecognized";
const sessions = [
{
page: "/review/session",
mount: "#item-info-col2",
loading: "#loading",
getHeader: sentences => {
return sentences[0].previousElementSibling;
}
},
{
page: "/lesson/session",
mount: "#supplement-voc-context-sentence",
loading: "#loading-screen",
getHeader: sentences => {
return sentences[0].previousElementSibling;
}
}
];
let state = {
settings: {
recognizedKanjiColor: "#f100a1",
unrecognizedKanjiColor: "#888888",
recognitionLevel: "5",
tooltip: {
show: true,
delay: 0,
position: "top"
}
},
kanjis: [],
jiff: false // JLPT, Joyo and Frequency Filters
};
// Application start Point
main();
function main() {
// we don't need to observe any changes in the vocabulary page
if (isPage(vocabularyPage)) {
init(() =>
evolveContextSentence(sentences => {
return sentences[0].previousElementSibling;
})
);
return;
}
// Get the target for the session page to watch for changes
const session = getSessionDependingOnPage();
if (session) startObserving(session);
}
function startObserving({ mount, loading, getHeader }) {
const loadingObservationConfiguration = {
attributes: true,
childList: false,
subtree: false
};
const itemInfoObservationConfiguration = {
attributes: false,
childList: true,
subtree: false
};
const observeLoading = () => {
observeChanges({
element: loading,
config: loadingObservationConfiguration,
onChange: runInit
});
};
const runInit = () => {
init(() => {
observeSentenceChanges();
});
};
const observeSentenceChanges = () => {
observeChanges({
element: mount,
continuesObservation: true,
config: itemInfoObservationConfiguration,
onChange: () => evolve(),
onInitObserver: () => evolve()
});
};
const evolve = () => evolveContextSentence(getHeader);
/**
* Basically, this function will fire an observer that will
* watch when the loading screen on the session pages (lesson and review) stops,
* then it will fire another observer to watch for changing the sentences,
* whenever the sentence change it will fire the evolveContextSentence over it again
*
* why wait for the loading screen stops? because the script slows down the animation
* which makes a really bad user experience
*/
observeLoading();
}
function init(callback) {
createReferrer();
createStyle();
if (wkof) {
wkof.include("ItemData,Settings");
wkof
.ready("ItemData,Settings")
.then(loadSettings)
.then(proccessLoadedSettings)
.then(getKanji)
.then(extractKanjiFromResponse)
.then(callback);
} else {
console.warn(
`${scriptName}: You are not using Wanikani Open Framework which this script utlizes to see the kanji you learned and highlights it with a different color, it also provides the settings dailog for the scrip. You can still use Advanced Context Sentence normally though`
);
callback();
}
}
function evolveContextSentence(getHeader) {
const sentences = document.querySelectorAll(".context-sentence-group:not(.advanced)");
sentences.forEach(s => s.classList.add("advanced"));
if (sentences.length === 0) return;
if (wkof) evolveHeader(getHeader(sentences));
sentences.forEach(sentence => {
const japaneseSentence = sentence.querySelector('p[lang="ja"]');
let sentenceText = japaneseSentence.textContent;
const audioButton = createAudioButton(japaneseSentence.innerHTML);
//let advancedExampleSentence = "";
const chars = Array.from(sentenceText);
const newNodes = chars.map(char => tagAndLinkKanji(char));
japaneseSentence.replaceChildren(...newNodes);
japaneseSentence.append(audioButton);
});
highlightKanji();
}
function evolveHeader(header) {
const settings = document.createElement("i");
settings.setAttribute("class", "icon-gear");
settings.setAttribute(
"style",
"font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;"
);
settings.onclick = openSettings;
if (!header.querySelector("i.icon-gear")) header.append(settings);
}
function createAudioButton(sentence) {
if (!window.SpeechSynthesisUtterance) {
console.warn("Advanced Context Sentence: your browser does not support SpeechSynthesisUtterance which this script utilizes to implement the audio feature. update your browser or use another one if you want that feature");
return null;
}
const button = document.createElement("button");
button.setAttribute("class", "audio-btn audio-idle");
button.onclick = () => {
var msg = new SpeechSynthesisUtterance(sentence);
msg.lang = "ja-JP";
window.speechSynthesis.speak(msg);
msg.onstart = () => {
button.setAttribute("class", "audio-btn audio-play");
};
msg.onend = () => {
button.setAttribute("class", "audio-btn audio-idle");
};
};
return button;
}
function observeChanges(params) {
const {
element,
config,
onChange,
onInitObserver = () => {},
continuesObservation = false
} = params;
if (!window.MutationObserver) {
console.warn(
`${scriptName}: you're browser does not support MutationObserver which this script utilaizes to implement its features in /lesson/session and /review/sesson. update you're broswer or use another one if you want Advanced Context Sentence to work on them. This script is still useful on /vocabulary page though`
);
return;
}
onInitObserver();
const target = document.querySelector(element);
const observer = new MutationObserver(() => {
observer.disconnect();
onChange();
continuesObservation && observer.observe(target, config);
});
observer.observe(target, config);
}
//--------------------------------------------------------------------------------------------------------------//
//----------------------------------------------SETTINGS--------------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
function loadSettings() {
return wkof.Settings.load(scriptId, state.settings);
}
function proccessLoadedSettings() {
state.settings = wkof.settings[scriptId];
}
function openSettings() {
var config = {
script_id: scriptId,
title: scriptName,
on_save: updateSettings,
content: {
highlightColors: {
type: "section",
label: "Highlights"
},
recognizedKanjiColor: {
type: "color",
label: "Recognized Kanji",
hover_tip:
"Kanji you should be able to recognize will be highlighted using this color",
default: state.settings.recognizedKanjiColor
},
unrecognizedKanjiColor: {
type: "color",
label: "Unrecognized Kanji",
hover_tip:
"Kanji you shouldn't be able to recognize will be highlighted using this color",
default: state.settings.unrecognizedKanjiColor
},
recognitionLevel: {
type: "dropdown",
label: "Recognition Level",
hover_tip:
"Any kanji with this level or higher will be highlighted with the 'Recognized Kanji' color",
default: state.settings.recognitionLevel,
content: {
1: stringfySrs(1),
2: stringfySrs(2),
3: stringfySrs(3),
4: stringfySrs(4),
5: stringfySrs(5),
6: stringfySrs(6),
7: stringfySrs(7),
8: stringfySrs(8),
9: stringfySrs(9)
}
},
tooltip: {
type: "section",
label: "Tooltip"
},
show: {
type: "checkbox",
label: "Show Tooltip",
hover_tip:
"Display a tooltip when hovering on kanji that will display some of its properties",
default: state.settings.tooltip.show,
path: "@tooltip.show"
},
delay: {
type: "number",
label: "Delay",
hover_tip: "Delay in ms before the tooltip is shown",
default: state.settings.tooltip.delay,
path: "@tooltip.delay"
},
position: {
type: "dropdown",
label: "Position",
hover_tip: "The placement of the tooltip",
default: state.settings.tooltip.position,
path: "@tooltip.position",
content: {
top: "Top",
bottom: "Bottom",
right: "Right",
left: "Left"
}
}
}
};
var dialog = new wkof.Settings(config);
dialog.open();
}
// Called when the user clicks the Save button on the Settings dialog.
function updateSettings() {
state.settings = wkof.settings[scriptId];
highlightKanji();
}
//---------------------------------------------------------------------------------------------------------------//
//-------------------------------------------HELPER FUNCTIONS----------------------------------------------------//
//---------------------------------------------------------------------------------------------------------------//
function isPage(page) {
const path = window.location.pathname;
return path.includes(page);
}
function getSessionDependingOnPage() {
let result = null;
sessions.forEach(session => {
if (isPage(session.page)) result = session;
});
return result;
}
function tagAndLinkKanji(char) {
return isKanji(char) ? wrapInAnchor(char) : document.createTextNode(char);
}
/**
* Determine if the character is a Kanji, inspired by https://stackoverflow.com/a/15034560
*/
function isKanji(char) {
return isCommonOrUncommonKanji(char) || isRareKanji(char);
}
function isCommonOrUncommonKanji(char) {
return char >= "\u4e00" && char <= "\u9faf";
}
function isRareKanji(char) {
char >= "\u3400" && char <= "\u4dbf";
}
/**
* Renders the link for the kanji
* Knji pages always use https://www.wanikani.com/kanji/{kanji} where {kanji} is the kanji character
*/
function wrapInAnchor(char) {
const anchor = document.createElement("a");
anchor.setAttribute("target", "_blank");
anchor.setAttribute("class", "recognized");
if (!wkof) {
anchor.setAttribute("href", `https://www.wanikani.com/kanji/${char}`);
anchor.innerText = char;
return anchor;
}
const kanji = state.kanjis.find(item => item.char == char);
anchor.setAttribute("data-srs", kanji ? kanji.srs : -1);
anchor.setAttribute("data-kanji", char);
anchor.setAttribute(
"href",
kanji ? kanji.url : `https://jisho.org/search/${char}`
);
anchor.innerText = char;
return anchor;
}
function createTooltip(kanji) {
if (!wkof) {
const container = document.createElement("span");
return container;
}
const container = document.createElement("div");
container.setAttribute("class", "acs-tooltip");
if (!kanji) {
const span = document.createElement("span");
span.innerText = "Wanikani doesn't have this kanji! :(";
container.append(span);
return container;
}
const onyomis = kanji.readings.filter(
item => item.type.toLocaleLowerCase() === "onyomi"
);
const kunyomis = kanji.readings.filter(
item => item.type.toLocaleLowerCase() === "kunyomi"
);
const onyomi = stringfyArray(onyomis, item => item.reading);
const kunyomi = stringfyArray(kunyomis, item => item.reading);
const meaning = stringfyArray(kanji.meanings, item => item.meaning);
container.append(generateInfo("LV", kanji.level));
container.append(generateInfo("EN", meaning));
if (onyomi !== "None" && onyomi !== "")
container.append(generateInfo("ON", onyomi));
if (kunyomi !== "None" && kunyomi !== "")
container.append(generateInfo("KN", kunyomi));
container.append(generateInfo("SRS", stringfySrs(kanji.srs)));
if (state.jiff) {
container.append(generateInfo("JOYO", kanji.joyo));
container.append(generateInfo("JLPT", kanji.jlpt));
container.append(generateInfo("FREQ", kanji.frequency));
}
return container;
}
function stringfyArray(array, pathToString) {
let stringfied = "";
array.forEach(item => {
stringfied = stringfied.concat(pathToString(item) + ", ");
});
stringfied = stringfied.substring(0, stringfied.length - 2);
return stringfied;
}
function stringfySrs(srs) {
switch (srs) {
case -1:
return "Locked";
case 0:
return "Ready To Learn";
case 1:
return "Apprentice 1";
case 2:
return "Apprentice 2";
case 3:
return "Apprentice 3";
case 4:
return "Apprentice 4";
case 5:
return "Guru 1";
case 6:
return "Guru 2";
case 7:
return "Master";
case 8:
return "Enlightened";
case 9:
return "Burned";
default:
return "";
}
}
function generateInfo(title, info) {
const container = document.createElement("div");
const key = document.createElement("span");
key.setAttribute("class", "acs-tooltip-header");
const value = document.createElement("span");
key.innerText = title;
value.innerText = info;
container.append(key, " ", value);
return container;
}
function getKanji() {
const filters = {
item_type: ["kan"]
};
if (wkof.get_state("JJFFilters") === "ready") {
state.jiff = true;
filters.include_frequency_data = true;
filters.include_jlpt_data = true;
filters.include_joyo_data = true;
} else {
console.warn(
`${scriptName}: You don't have Open Framework JLPT Joyo and Frequency Filters by @Kumirei installed (version 0.1.4 or later). Install the script if you want to get more information while hovering on Kanji on Context Sentences. Script URL: https://community.wanikani.com/t/userscript-open-framework-jlpt-joyo-and-frequency-filters/35096`
);
}
return wkof.ItemData.get_items({
wk_items: {
options: {
assignments: true
},
filters
}
});
}
function extractKanjiFromResponse(items) {
const kanjis = [];
items.forEach(item => {
const kanji = {
char: item.data.characters,
readings: item.data.readings,
level: item.data.level,
meanings: item.data.meanings,
url: item.data.document_url,
srs: item.assignments ? item.assignments.srs_stage : -1,
jlpt: item.jlpt_level,
joyo: item.joyo_grade,
frequency: item.frequency
};
kanjis.push(enhanceWithAditionalFilters(kanji, item));
});
state.kanjis = kanjis;
}
function enhanceWithAditionalFilters(kanji, item) {
if (state.jiff) {
kanji.jlpt = item.jlpt_level;
kanji.joyo = item.joyo_grade;
kanji.frequency = item.frequency;
}
return kanji;
}
function createSource(type, sentence) {
const source = document.createElement("source");
source.setAttribute("type", type);
source.setAttribute(
"src",
`https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=ja&total=1&idx=0&q=${sentence}`
);
return source;
}
let tippys = new Set();
function highlightKanji() {
const rules = document.querySelector("#acs-style").sheet.cssRules;
rules[0].style.color = state.settings.recognizedKanjiColor;
rules[1].style.color = state.settings.unrecognizedKanjiColor;
if (!wkof) return;
tippys.forEach(t => t.destroy());
tippys = new Set();
const anchors = document.querySelectorAll(".context-sentence-group a");
anchors.forEach(anchor => {
const srs = anchor.getAttribute("data-srs");
const char = anchor.getAttribute("data-kanji");
if (srs >= state.settings.recognitionLevel)
anchor.setAttribute("class", "recognized");
else {
anchor.setAttribute("class", "unrecognized");
}
if (state.settings.tooltip.show) {
const kanji = state.kanjis.find(item => item.char == char);
const tooltip = createTooltip(kanji);
tippy(anchor, {
content: tooltip,
size: "small",
arrow: true,
placement: state.settings.tooltip.position,
delay: [state.settings.tooltip.delay, 20]
});
tippys.add(anchor._tippy);
}
});
}
// Neccessary in order for audio to work
function createReferrer() {
const remRef = document.createElement("meta");
remRef.name = "referrer";
remRef.content = "no-referrer";
document.querySelector("head").append(remRef);
}
// Styles
function createStyle() {
const style = document.createElement("style");
style.setAttribute("id", "acs-style");
style.innerHTML = `
/* Kanji */
/* It's important for this one to be the first rule*/
${recognizedSelector} {
}
/* It's important for this one to be the second rule*/
${unrecognizedSelector} {
}
.context-sentence-group p a {
text-decoration: none;
}
.context-sentence-group p a:hover {
text-decoration: none;
}
.acs-tooltip {
text-align: left
}
.acs-tooltip-header {
color: #929292
}
`;
document.querySelector("head").append(style);
}
})();
However, it might still not work: the available speech synthesis languages depend on the browser. You can go to this speech synthesis documentation page and check in the dropdown box which languages are supported by your browser. Firefox seems to only offer locally installed languages (at least for me).