An Android app that takes an English speaker from their very first Polish word to confident intermediate. Thirty-two units across four CEFR levels, pronunciation for every word, lessons that work with no connection and sync when they get one, and a second interface language built over the whole thing. Designed, built, localised and released single-handed.
I moved to Poland speaking no Polish. The apps I tried all made the same assumption: that I could already read the alphabet, that I knew what a case was, that ł and ż and ę were sounds I had heard before. They start at lesson one of a course that quietly began somewhere else.
LinguiSpark starts at zero. The first thing it teaches is how the letters sound, and every single word on every card can be tapped to hear it spoken. From there it runs a structured A1 → A2 → B1 → B2 path, thirty-two units, each one a short lesson followed immediately by practice on what was just taught.
It is a real product with real users on the Play Store, not a portfolio exercise. That constraint is what makes it interesting to build: it has to install, launch, work offline, survive a phone being reset, and keep someone coming back on day thirty.







All thirty-two units are TypeScript data files conforming to a shared Unit type, one file per unit. There is no backend serving content and no admin panel.
That means the entire course is type-checked at build time, reviewable in a diff, and testable. A malformed exercise is a compile error rather than something a learner discovers in Warsaw on a train.
Progress is written locally and reconciled into Firestore field by field, rather than the last device to sync overwriting the record wholesale.
Lessons are bundled into the app, so the whole course works on a plane or an underground platform. Sign-in is optional and adds cross-device continuity on top, instead of being the price of entry.
Every Polish word is spoken by the device's own text-to-speech engine, slowed slightly for learners. There is not a single recorded audio asset in the bundle.
The trade is a small loss of control over voice quality. What it buys is that every word ever added to the course arrives with audio automatically, at zero download size. That is the difference between a handful of spoken words and all of them.
The app shipped English-only. Adding Ukrainian without duplicating thirty-two units meant building a gloss layer: translations keyed to each item's own stable id, resolved at render time, sitting over untouched Polish content.
The interface followed via a dependency-free translation catalogue, now several hundred keys held at parity between the two languages by an automated test. The Polish under examination is never substituted, because in a grammar exercise the Polish options are the question.
Getting an app finished and getting an app published turned out to be two different projects. Google requires a new personal developer account to run a closed test with a minimum number of testers for a continuous period before production access is even unlocked, which puts a hard floor under the timeline no amount of engineering removes.
Expo Application Services builds the production Android bundle, so releases do not depend on the state of one local machine.
Every build numbered and tracked, from the first closed test through to the current production version.
Type-checking and a full Jest suite gate every change, including tests that guard the translation catalogues against drift.
Some faults only exist in a release build on real hardware. Those get diagnosed over adb on a physical phone, not in a simulator.
The most instructive bug of the project was invisible to every local check. Type-checking passed, the tests passed, the bundle exported cleanly, and the installed app died at the splash screen every time. A transitive dependency had quietly resolved to a version built against a newer SDK, which JavaScript tolerated and the native linker did not. It was only findable by reading the crash log off a real device.
Green tests are not the same thing as a working app. That lesson cost a release cycle and was worth it.
The architecture separates a course from the app that renders it, so a new language is content plus a gloss layer rather than a fork. Polish for English speakers is the version on the store today; Polish for Ukrainian speakers is built on that same seam and in testing. More language pairs, and an iOS release, follow the same path.