Junsang Yoo About ↗
← All Projects
Websites · Applications · 2026 · Solo

Junglish

One twenty-minute English mission a day, scheduled by FSRS.

Junglish
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.

Four Junglish screens: the home path in light and dark themes, a word card front and back
The day's path — in both themes — then a card before and after the flip. The three ratings carry the interval each one buys: 10분 / 15분 / 2일.

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.

Four Junglish screens: dialogue quiz, shadowing, typing recall, and the stats tab
Dialogue, shadowing, typing recall, and the stats tab — streak, cards in learning, a twelve-week grid, and the next seven days of review load.

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.

Four Junglish screens: the landing page, invite-only signup, reminder settings, and the day-complete screen
The landing page and invite-only signup at the same URL as home, the reminder settings, and the screen that ends the day.

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.