Completed · Full-Stack Application · 2026-07-30
A twenty-four chapter book that teaches a complete beginner to read Japanese, one chapter per Japanese micro-season, with romaji that fades away as each character is learned.
Tech stack: Next.js, React, TypeScript, TailwindCSS, Web Audio API, Vercel
Source code (GitHub) · Live preview
二十四節気 is a book I wrote because my girlfriend asked me to teach her Japanese. That was the whole brief: she wanted to learn, and I wanted her to have something better than a streak counter to learn from. So I wrote her a book, and built the thing it needed to live in.
Japan divides the year into twenty-four sekki — micro-seasons of about fifteen days each: the start of autumn, white dew, frost descending, greater cold. The book has one chapter for each of them. It opens in August at 立秋 and closes the following July, and by the end of it a total beginner can read kana. A year is the honest timescale for the promise I was making to her — not thirty days, not a weekend.
Writing for one specific reader changed the engineering, not just the tone. The progress model is conservative because I didn't want the book flattering her. The pronunciation view has no score because I didn't want it grading her. Her name and her saved progress sit behind a password because the copy is hers even though the book isn't private.
Anyone can read it. Only one person has the copy that remembers her.
Beginner Japanese material tends to fail in one of two directions. Apps gamify the language into streaks and points, which measures showing up rather than reading. Textbooks hand over a kana chart on page one and expect it memorised by page two. Both leave romaji propped underneath the Japanese forever, so the training wheels never come off — you keep reading the letters you already know and never the characters you're supposedly learning.
I wanted something that read like a book rather than a course, and that could prove it was working: a reader should be able to see the romaji leaving.
Romaji sits beneath the Japanese and fades as each character is learned — per
mora, so あお can lose its romaji entirely while あき keeps the ki until the
か-row is earned. Nothing sets that opacity by hand; it's derived from progress,
so getting a character wrong later brings its romaji back. The book can't
flatter you.
Underneath that is a deliberately conservative model of "learned". A character's strength rises on a correct recall, but at most once per sitting — getting the same character right four times in one evening proves nothing about tomorrow. Full disappearance has to be earned across separate visits, so the book never claims to know a reader better than she knows herself.
For pronunciation there's a record-and-compare view instead of a score. She hears a native clip, records herself, and sees both waveforms on a shared time axis, where width is proportional to duration — because length is meaning in Japanese (おばさん is an aunt, おばあさん a grandmother). Silence is trimmed before measuring and amplitude is normalised, so tapping stop a beat late or holding the phone further away doesn't show up as a difference. There is no speech recognition and no grade: her ear does the judging.
si, tu, hu, sya) alongside HepburnBuilt solo. Next.js 16, React 19, TypeScript and Tailwind 4, deployed on Vercel at nihongo.jjayfabor.com.
Almost all of it is static. Chapters are prerendered and audio is synthesised at
build time through edge-tts, so the entire server-side surface is two routes:
one to claim a copy with a password, one to read and write a single private JSON
blob. Progress lands in local state first and syncs on a 1.5-second debounce, so
a tap never waits on the network, and an ETag conflict re-reads and merges
rather than overwriting — taking the higher strength and the more recent
timestamp per character, so a merge can never cost her ground.
The privacy model is enforced rather than promised. The password and the dedication name live only in environment variables; the name appears in no prerendered HTML and arrives only over the authenticated API after hydration. Leave the password unset and the deployment fails closed — everyone is a visitor, nothing is stored, and every chapter still reads.
All twenty-four chapters are written and live: 141 distinct characters, 192 words and phrases, 355 audio clips. Chapter 18 teaches ぜんぶよめます — I can read all of it — and chapter 24 closes with ぜんぶよめました in the past tense learned one chapter earlier, then links back to chapter 1: begin again. The book loops rather than dead-ends.
Two things stuck with me. The first was that derived state beats stored state almost every time — romaji opacity computed from progress meant there was no "mark as learned" path to keep in sync, and no way for the display to disagree with what the reader had actually earned.
The second was about being honest in the build. Every word and sentence in the
book carries a citation field pointing at a dictionary or corpus entry, and
npm run verify fails while any of them is still pending. They currently all
are — the vocabulary is common and I expect it to hold up, but the composed
example sentences genuinely want a native reader before I claim otherwise. It
was tempting to make the check pass. Leaving it red is the more useful choice:
the build tracks the debt instead of pretending it doesn't exist.