This app allows you to practice typing the romanized Japanese in 3 different sets: A-NA, HA-WA, and Digraphs.
- 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> |