Inspiration

About 10 million people in South Africa speak isiXhosa at home, 16.3% of the population (Stats SA, Census 2022). The language has 15 click sounds built on three basic clicks, c, x and q. It is also tonal, and tone is not written, so a learner who only reads words learns them wrong.

Duolingo announced an isiXhosa course in 2021 and never shipped it. The dedicated apps we found are thin, and none of them drills the clicks as a core mechanic. The founder is Norwegian and enrolled at the University of Cape Town. Molo is the course he wanted, for English and Norwegian speakers.

What it does

Molo looks like Duolingo: a path of units and short lessons, hearts, streaks, XP, weekly leagues and spaced-repetition review. Inside it runs on one rule, and the rule is enforced in code:

No learner ever sees content that a human isiXhosa editor has not approved.

Unit 1, "Say hello and say who you are", is live on hellomolo.com and in the app. It has five lessons in three skills: greeting someone and saying goodbye, hearing the difference between c, x and q, and saying yes, no and thank you. Its 14 exercises are of four kinds:

  • Click identification: hear a bare click and pick its letter. First the three basic clicks, then plain against aspirated (c and ch, x and xh, q and qh).
  • Listen and select: hear a word, pick what it means.
  • Match pairs: match words to meanings. Tapping an isiXhosa tile plays the word.
  • Culture cards: short notes an editor approved, for example that molo greets one person and molweni more than one.

Every word in the unit, and each of the six clicks it drills, is a studio recording by our tutor, Thandeka. Each click letter has its own colour wherever isiXhosa appears, so a learner sees the click before hearing it. The interface is in English and Norwegian. Glosses are stored per source language, because a Norwegian learner needs different notes than an English one.

New on 28 September:

  • New words come before practice (web and app). A card shows the word, plays its recording once and gives its meaning, then the exercise starts.
  • Hearts in the lesson header (web and app). A lost heart pops, shakes and drops away while the count ticks down. With reduced motion only the number changes. Screen readers hear how many hearts are left.
  • Out of hearts is a card in the lesson (app), not a sheet on top of it: practise to earn a heart, get Plus, or continue later.
  • A trail joins the path (web and app). It is solid where you have walked and dashed ahead, and the newest stretch draws itself once after a lesson.
  • Screens do not wait (web and app). They draw from what the device already has, fetch the next lesson before the tap, warm its first recordings, and show a skeleton shaped like the real screen in the one cold case left.
  • Offline units are a Molo Plus perk (app), behind one pill in the unit header.
  • Match pairs never gives an answer away (web and app). The old shuffle left at least one meaning in its own word's row in about 3 of 5 exercises; the new order is a derangement, the same for everyone.
  • Reminders at the learner's own time (app, with email as a fallback). After the first finished lesson the mascot asks whether it may send a nudge, at a time the learner picks. A yes brings a daily reminder at that local time, an evening streak saver, and win-back messages on days 1, 3 and 7 that then stop. At most two a day, and nothing between 21:30 and 08:00.

How we built it

  • A TypeScript monorepo on Bun. A Hono API on Cloudflare Workers; a TanStack Start web app that is both the learner app and the editor dashboard; an Expo app for iOS and Android with an encrypted offline cache (op-sqlite with SQLCipher). PlanetScale Postgres through Hyperdrive, R2 for audio, Queues for audio processing, Better Auth with email, Apple and Google sign-in, Sentry in the EU, infrastructure as code with Alchemy.
  • Three Rust crates where correctness earns it. xh-morph generates noun-class forms from rules and is tested against golden forms a native speaker signed off; a rule without a golden case is a rejected change. xh-audio trims each recording, normalises it to −16 LUFS, encodes it and writes a manifest. xh-fsrs wraps fsrs-rs, the scheduler Anki ships, compiled to WebAssembly and run inside the Worker that schedules reviews.
  • A publish gate in the code, not in a document. Every content row carries a status, and learner endpoints read published only. Anything a language model drafts enters as ai_draft and cannot reach published without a human editor in the dashboard. An integrity suite of 38 test files runs against a real Postgres and asserts this, among other things; weakening it fails CI. 1,121 unit tests, Playwright end-to-end tests, mobile Jest tests and cargo test run on every pull request.
  • Tools built around the tutor's hour. Thandeka records in a browser studio; her takes go through xh-audio and into review. She answers golden cards for the rule engine, writes the sentences a unit asks for, and can leave a note on any item in the studio.
  • Licensed sources, not scraped ones. The lexicon is adapted from IsiXhosa.click (CC BY-SA 4.0, a student-led dictionary supported by UCT and SADiLaR). The Gothenburg Corpus of Spoken isiXhosa (CC BY 4.0) and the NCHLT text corpora (CC BY 2.5 ZA) rank the vocabulary. Our adapted lexicon goes back out under CC BY-SA at hellomolo.com/lexicon. An Oxford school dictionary is looked up word by word, and its corrections change only drafts no human has touched.
  • An ops CLI for a person and an agent alike (molo). Every command that writes or spends is a dry run unless --live, and every command has --json. molo content promote can send content to review but never publish it; publishing happens only in the dashboard.
  • One person and AI coding agents. Claude Code and Codex wrote much of the TypeScript and Rust under written project rules, which the test suite enforces. None of them writes isiXhosa.

How we use RevenueCat

  • One entitlement, plus, two products (molo_plus_monthly, molo_plus_yearly) in one offering, configured in App Store Connect and Google Play, with list prices of NOK 79 / 599 and USD 6.99 / 49.99.
  • The server is the source of truth. RevenueCat's webhook posts to POST /webhooks/revenuecat with a shared secret. One pure module maps the events: purchase, renewal, uncancellation, product change and transfer grant; a cancellation keeps access until expiry; expiration and refund revoke; a billing issue changes nothing until the store expires the subscription. It is idempotent on the event id.
  • The app trusts the SDK for a moment. An active plus entitlement from the SDK means unlimited hearts at once, while the server catches up through the webhook. The paywall feels instant and the ledger stays on the server. Every price on the paywall comes from the store.
  • Web Billing is wired on hellomolo.com/plus through @revenuecat/purchases-js, with the 14-day withdrawal right of Norwegian and EU consumer law and an express-start consent built into the checkout. It is limited to accounts registered in Norway until VAT registration elsewhere is done, and it is not yet switched on in production.
  • What Plus buys is honest: unlimited hearts, a streak freeze every week, offline units, and it funds the recordings and the editors who check every word. Hearts cannot be bought one by one.

Challenges we ran into

  • Tone is not written. Text corpora silently lose a feature of the language, so audio is the only carrier. A word without a native recording cannot be published, and synthetic speech never satisfies the gate.
  • Trusting a model exactly as far as it deserves. Language models write fluent, plausible, wrong isiXhosa. So a model drafts English and Norwegian glosses for words the lexicon already has, never the isiXhosa itself, and forms like plurals come from the rule engine or not at all.
  • The tutor's time is the scarcest resource. Every editor tool asks one clear question per card and saves as she goes. Locatives are asked on golden cards before any locative rule exists, and agreement will be asked as whole sentences to say rather than as grammar terms.
  • Selling a subscription lawfully in Norway, the EU and South Africa. That took a legal review and rewrites of the terms and privacy policy: an age gate (13+, and 18+ in South Africa), withdrawal rights, POPIA, and data processing agreements on file.
  • Shipping daily while the app is in store review. Over-the-air updates are gated on a fingerprint of the native build. A copy change once looked native to that fingerprint and held updates back until we found why.

Accomplishments that we're proud of

  • Unit 1 is published, and every word and click in it is a native speaker's recording.
  • Thandeka has signed off the first 11 plural forms the rule engine is tested against.
  • The editor gate is a test, not a promise, and it has never been weakened to ship a feature.
  • A production stack on Cloudflare deployed from CI, both stores configured (privacy questionnaires, age ratings, subscriptions), and the app submitted to both 24 days after the first commit on 3 September.
  • The lexicon goes back to the community under the licence it came with.

What we learned

Content, not code, is the product. The realistic way a project like this dies is a beautiful app with forty features and two hundred unvalidated words. So the plan puts the content risk first: a native speaker's hour, recordings for each unit, and nothing published until a person says it is right.

What's next

isiXhosa is where we started, not the only language the system is built for. Courses, words and sentences each record the language they belong to, so a second course is new content in the same tables, not a new app. The editor gate, the recording studio, the audio pipeline, the review queue and the scheduler carry over as they are. isiZulu would be the natural next course: it is mutually intelligible with isiXhosa, and our rule engine follows ZulMorph, a finite-state approach first built for isiZulu. Sesotho, Setswana and other languages that few apps teach well would each need their own sources, rule engine and native editors, the way isiXhosa did.

  • Units 2 to 10 exist as drafts in the curriculum. Each opens when it is recorded and an editor approves it.
  • 136 golden cases in the rule engine's test file still wait for Thandeka's answer, and locative cards are on her page before any locative rule exists.
  • Sentences for Unit 1, which Thandeka writes from English and Norwegian prompts, and the grammar notes, once a speaker has checked the rule behind each one.
  • The code in the open under an open-source licence.

Built With

Share this project:

Updates

Submission history