Junglish
One twenty-minute English mission a day, scheduled by FSRS.
- Role
- Design · Full-stack
- Team
- Solo
- Year
- 2026
- Stack
- SvelteKit · Svelte 5 · TypeScript · SQLite · ts-fsrs · PWA · Python · Docker
Junglish is a language app built for exactly one learner: a Korean engineer who needs English that survives a customer call and a Slack thread on the same day. One mission a day, twenty minutes on a phone, a year of material written ahead of time. It works on the subway and reminds me once, at a time I picked. No leagues, no gems, no notifications begging me to come back.
Problem
Most language apps optimise for the app, not the learner. The five-minute lesson that teaches three words exists because it makes a good retention metric, and the streak economy exists because it makes a good revenue metric. Neither survives contact with what I actually needed: vocabulary I don’t already know, phrasing a colleague would really use, and a schedule that doesn’t quietly re-teach me “important” for the fortieth time.
The other half of the problem was supply. Spaced repetition only works if there is something worth repeating, and a year of decent bilingual material — words, patterns, dialogues, all in Korean that a Korean actually speaks — is far more writing than one person does on the side.
Approach
One mission, always the same seven steps. Review → new words → new patterns → dialogue → shadowing → your own sentences → final review. The home screen draws them as a path with counts, so there is never a question of how much is left, and one button resumes exactly where I stopped.
Recall first, evidence second. A card shows the word alone; audio is one tap away but the answer never appears before an attempt. Flip it and the meaning arrives with a work sentence, a daily sentence and collocations. Three rating buttons only — 모름 / 애매 / 앎 — because a fourth choice on a phone is a coin flip, and each button renders the interval it buys from the same FSRS preview the scheduler will use. The cost of every answer is visible before you press it.
Nothing is learned in isolation. The day’s words and patterns come back inside a two-person scene at a robotics company, one blank at a time, with the line in question scrolled into view. Then the same scene is shadowed line by line through the Web Speech API. Once a card has been stable for two weeks it stops being multiple choice and asks you to type the word instead.
The server is the session. The client never decides what comes next: /api/step advances a server-side day_progress row and is idempotent, so a double tap or a flaky subway connection cannot skip a step or double-complete a day. Only one save is in flight at a time, so a double tap cannot rate one card twice and skip the next.
Installable, and fine offline. A service worker precaches the build and serves the last good copy of any screen already opened today; /api/* is never cached, because a cached rating would be a lie. A rating that cannot be sent goes into a localStorage outbox and the card moves on — a lost rating is worse than a lost second. The queue drains oldest-first when the network returns, and every rating carries the moment it happened, so a session finished at 23:50 and delivered at 00:10 still counts for the right day in Seoul. One web push a day, at a time I choose, opens the step I stopped at and stays quiet once the day is done.
Two databases, on purpose. content.db is opened read-only and is identical for everyone; progress.db holds users, card states, review logs and my own written sentences in WAL mode. A day of content can be regenerated and rebuilt in place without migrating a single row of learner data. Signup is invite-only: the code is compared in constant time, passwords are scrypt, and login and signup are rate limited in separate buckets so a mistyped invite cannot lock anyone out.
Every colour is a token. The two home screens above are the same markup under one swapped token block, so the app follows the system theme with nothing hard-coded. The layout is a fixed column: a scrolling pane over a bar pinned to the thumb zone, both padded with env(safe-area-inset-*), checked at 390×844 and 375×667. Buttons are solid with a 4px darker bottom edge that collapses on press, because a standalone PWA has no hover state and no browser back button.
Outcome
Junglish runs as a single Docker container on an Oracle Cloud ARM box behind nginx, with data/ bind-mounted so progress survives every rebuild and nightly snapshots kept for thirty days. Server logic and the offline outbox are covered by Vitest against in-memory SQLite using the production schema; the pipeline has its own pytest suite. Per-request work stays proportional to the screen — on a profile with 12,000 cards and 120,000 review logs, home builds in about a millisecond and stats in five.
- 10,950
- words
- 3,650
- patterns
- 365
- dialogues
- 155
- tests
The lesson I keep coming back to is that the hard part was never the scheduler — FSRS is a library. It was deciding what not to build. No social layer, no gamification, no fourth rating button, no skip on the writing step, one reminder a day and not two. Every one of those absences is a decision I can defend, and the app is small enough that I still read all of it.
One mission a day. Your only opponent is yesterday.