Skip to content

Instantly share code, notes, and snippets.

@lewdev
Last active June 9, 2024 02:39
Show Gist options
  • Select an option

  • Save lewdev/43232426f63ab014c628a94e7c512ae4 to your computer and use it in GitHub Desktop.

Select an option

Save lewdev/43232426f63ab014c628a94e7c512ae4 to your computer and use it in GitHub Desktop.

๐ŸŽŽ Typing Japanese Practice

This app allows you to practice typing the romanized Japanese in 3 different sets: A-NA, HA-WA, and Digraphs.

Next features

  • Katakana sets
  • Stores times and "completion" ratings (bronze, silver, gold)
  • Writing Homework Random Generation
  • Vocabulary writing
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<div class="container text-center mt-3">
<div id=sets></div>
<div id=contents></div>
<p id=o style="font-size: 50px;"></p>
<div class="w-50 mx-auto">
<input type=text id=input class="form-control text-center" onkeyup=enterVal(event) style=display:none>
</div>
<input id=enableSpeak type="checkbox" checked="checked" onchange=setEnableSpeak(this.checked) /> Enable Speak
<div id=msg></div>
<div id=log></div>
</div>
<script>
const shuffle = arr => arr.map(val => ({ val, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({val}) => val)
;
const message = (str, color) => msg.innerHTML = `<span class="text-${color}">${str}</span>`;
let enableSpeak = true;
let currVal = null;
let currSet = null;
const setEnableSpeak = isEnabled => enableSpeak =isEnabled;
const hiragana = "ใ‚,a,ใ„,i,ใ†,u,ใˆ,e,ใŠ,o,ใ‹,ka,ใ,ki,ใ,ku,ใ‘,ke,ใ“,ko,ใ•,sa,ใ—,shi,ใ™,su,ใ›,se,ใ,so,ใŸ,ta,ใก,chi,ใค,tsu|tu,ใฆ,te,ใจ,to,ใช,na,ใซ,ni,ใฌ,nu,ใญ,ne,ใฎ,no,ใฏ,ha,ใฒ,hi,ใต,hu|fu,ใธ,he,ใป,ho,ใพ,ma,ใฟ,mi,ใ‚€,mu,ใ‚,me,ใ‚‚,mo,ใ‚„,ya,ใ‚†,yu,ใ‚ˆ,yo,ใ‚‰,ra,ใ‚Š,ri,ใ‚‹,ru,ใ‚Œ,re,ใ‚,ro,ใ‚,wa,ใ‚’,wo,ใ‚“,n,ใŒ,ga,ใŽ,gi,ใ,gu,ใ’,ge,ใ”,go,ใ–,za,ใ˜,ji,ใš,zu,ใœ,ze,ใž,zo,ใ ,da,ใข,ji,ใฅ,zu|du,ใง,de,ใฉ,do,ใฐ,ba,ใณ,bi,ใถ,bu,ใน,be,ใผ,bo,ใฑ,pa,ใด,pi,ใท,pu,ใบ,pe,ใฝ,po,ใใ‚ƒ,kya,ใใ‚…,kyu,ใใ‚‡,kyo,ใ—ใ‚ƒ,sha,ใ—ใ‚…,shu,ใ—ใ‚‡,sho,ใกใ‚ƒ,cha,ใกใ‚…,chu,ใกใ‚‡,cho,ใซใ‚ƒ,nya,ใซใ‚…,nyu,ใซใ‚‡,nyo,ใฒใ‚ƒ,hya,ใฒใ‚…,hyu,ใฒใ‚‡,hyo,ใฟใ‚ƒ,mya,ใฟใ‚…,myu,ใฟใ‚‡,myo,ใ‚Šใ‚ƒ,rya,ใ‚Šใ‚…,ryu,ใ‚Šใ‚‡,ryo,ใŽใ‚ƒ,gya,ใŽใ‚…,gyu,ใŽใ‚‡,gyo,ใ˜ใ‚ƒ,ja,ใ˜ใ‚…,ju,ใ˜ใ‚‡,jo,ใณใ‚ƒ,bya,ใณใ‚…,byu,ใณใ‚‡,byo,ใดใ‚ƒ,pya,ใดใ‚…,pyu,ใดใ‚‡,pyo".split`,`;
const hiraganaList = hiragana.reduce((prev, curr, i) => {
if (i % 2) prev.push({ja: hiragana[i-1], ro: curr});
return prev;
}, []);
const hiraganaSets = {
"A-NA": hiraganaList.filter(({ro}) => "aiueokstn".includes(ro[0]) && ro[1] !== "y"
&& !["sha", "shu", "sho"].includes(ro)),
"HA-WA": hiraganaList.filter(({ro}) => "hmyrw".includes(ro[0]) && ro[1] !== "y" || ro === "n"),
//"Non-Digraphs": hiraganaList.filter(({ro}) => ro[1] !== "y"),
"Digraphs": hiraganaList.filter(({ro}) => ro[1] === "y"),
};
const startSet = setName => {
input.value = log.innerHTML = "";
message("");
contents.innerHTML = hiraganaSets[setName].map(({ro}) => ro).join(", ");
currSet = shuffle(hiraganaSets[setName]);
input.style.display = "";
currVal = currSet.pop();
showProblem();
};
//------------------
/*
Sources:
* https://manu.ninja/using-the-speech-synthesis-interface-of-the-web-speech-api/
* https://css-tricks.com/using-the-web-speech-api-for-multilingual-translations/
* https://www.zeolearn.com/magazine/the-new-speech-synthesis-web-api
*/
let syn = speechSynthesis, voice, attempts = 0;
const initJpVoice = () => {
attempts++;
voice = syn.getVoices().find(_voice => /ja[-_]JP/.test(_voice.lang));
if (!voice) {
if (attempts < 10) setTimeout(() => initJpVoice(), 250);
else alert('`ja-JP` voice not found.');
}
};
initJpVoice();
const speak = s => {
if (!voice || !syn || syn.speaking) return;
const utter = new SpeechSynthesisUtterance(s);
utter.voice = voice;
syn.speak(utter);
};
//------------------
const render = () => sets.innerHTML = (Object.keys(hiraganaSets).map(s => `<button class="btn btn-info mx-1" onclick="startSet('${s}'); return false;">${s}</button>`).join``);
const enterVal = e => {
const { value } = e.target;
if (currVal.ro.split`|`.includes(value)) {
if (currSet.length === 0) {
message("Done!", "success");
input.style.display = "none";
o.innerHTML = "";
return;
}
currVal = currSet.pop();
message("Correct!", "success");
input.disabled = true;
showProblem();
setTimeout(() => {
input.disabled = false;
input.value = "";
input.focus();
log.innerHTML += `${log.innerHTML ? ", " : ""}<span>${value}</span>`;
}, 1000)
}
};
const showProblem = () => {
render();
if (!currSet) return;
if (!currVal) currVal = currSet.pop();
if (enableSpeak) speak(currVal.ja);
o.innerHTML = currVal.ja;
};
showProblem();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment