An audio-only language app. You learn to understand and speak by ear, the way you learned your first language. No reading, no writing, no script to memorize. Press play, listen, say it back. Marathi and Hindi ship today; the engine is built for more.
The method is grounded in second-language-acquisition research (see Learning method): comprehensible input + retrieval practice + spaced repetition + real speaking practice, interleaved and ordered easy to hard.
linguistic/ (local folder; GitHub repo is roshan-b1/bol)
├── web/ Vite + React web app (runs today, no setup)
├── mobile/ Expo + React Native app (builds for the App Store / Play Store)
├── scripts/ generate-audio.mjs (bake mp3s), render-icons.mjs (brand → PNGs), content QA tools
├── brand/ logo, app icon, wordmark, brand guide (SVG)
└── README.md
Web and mobile share the same content and core logic (SRS engine, answer matching, exercise builder, streak/level engine are byte-identical files). Only the UI layer differs. Note: the web app leads on features and the mobile app is a catch-up port behind it (see Mobile parity).
Every feature is identical for every language. Only the language changes. There is no Marathi-only or Hindi-only capability. The app is registry-driven: all screens, games, the guided path, spaced repetition, the AI tutor, and engagement mechanics run off a generic content model, so a learner picks Marathi or Hindi (or any language added later) and gets the exact same product with different words and audio. When adding a feature, it must work for all languages, not one.
- Onboarding: pick your heritage language first (this is your family's language, so it leads), then why you're learning and a daily goal. The language picker and the reasons are registry-driven and language-aware, so a Hindi learner is never shown "Marathi" copy. The choice persists, so the app opens in that language.
- Placement diagnostic: an optional "Already know some?" check for new learners, because a heritage learner walks in knowing scattered pieces (food and family words from childhood, but no grammar or idioms). It probes the whole course in order, 16 sections x 3 probes, and finds where your knowledge drops off. Probes are production, not multiple choice: you get the English and say it from memory. Multiple choice has a 25% guess rate, which would place people past material they can't actually produce. Where the browser has speech recognition, the mic grades you against the same pass bar as lessons, with no self-report buttons (a placement you can flatter corrupts the review schedule it seeds); a mic misfire is never scored, you just retry or skip. On browsers without speech recognition, an honest self-check is the fallback. Sections you pass are marked complete and their items are seeded into the SRS with a partial interval, so the guided path starts you at your real level instead of "hello".
- Today (guided daily session): the home screen leads with one Start today's session button. It runs your due reviews first, then the next lesson on the recommended path, blended into a single flow, so you never have to decide what to study. Reviews are capped per session so it stays short (research favors brief, daily practice).
- Lessons: learn new material in the Pimsleur arc: listen (intro) → pick the meaning (comprehension) → say it from memory (production). A correct answer auto-advances; a wrong one pauses so you study the fix. Missed items requeue to the back and come back until you get them right (no free pass while the answer is fresh), with an "I don't know" escape that shows the answer and re-queues it.
- Speak drill (Easy / Medium / Hard): English prompt in, you say it in the target language. Pure output practice, the thing most apps skip. Difficulty bands map to lesson levels.
- Review: a spaced-repetition queue brings each phrase back right before you'd forget it, with a visible schedule (ready now / next 24h / this week / further out / in long-term memory) and a "practice ahead" option when nothing is due.
- Talk: live conversation with an AI native-speaker tutor (Asha for Marathi, Meera for Hindi). She asks what you want to practice, chats by voice at your level using vocabulary you've already studied, gives gentle corrections of real mistakes, and offers "What could I say?" lifelines (2-3 natural options with pronunciation + meaning). Powered by the Claude API from the browser (see Accounts & keys).
- Dialogues roleplay: scripted real-life scenes (tea stall, market, doctor, auto...) where the app plays the other person and you speak your side. On-device and free; your lines feed the same SRS cards and earn XP.
- The path, shown as units: the course is chunked into units of 10 lessons following the recommended order, so 294 lessons never render as a wall. Your current unit is open and labelled "you're here"; finished ones collapse to a checkmark.
- Nothing is locked: you can open any unit at any time. A heritage learner poking ahead at "Idioms" is the whole motivation, so later units are only muted, never gated. The guided Today session still enforces the correct order for anyone who just wants to be told what to do.
- Topic chips + search: a row of quick jumps (Greetings, Family, Food, Numbers, Travel, Work, Body, Time, Verbs, Grammar, Shopping, Idioms) pulls matching lessons from across the whole course (Food → 19 lessons, Idioms → 4). Free search also matches lesson title, English meaning, romanized sound ("chaha"), or native script. The chips are English keywords on purpose: every deck's lesson titles are in English, so the same chips work for any language added later.
- Recommended learning path: the guided Today session follows a generic easy-to-hard order (level 1 to 4, high-utility sentence-building lessons first). Same rule for every language.
- Lightning: a 60-second timed comprehension blitz with combo scoring.
- Match: pair each audio clip to its meaning.
- Streaks + streak freeze: daily streak with loss-aversion pull; a banked freeze saves your streak through one missed day instead of resetting to zero, and refills weekly.
- XP + levels: every answer earns XP; XP maps to levels with titles (Newcomer → Conversational → Fluent-minded).
- Daily goal: a per-day XP target with a progress bar.
- Achievements: streak and volume milestones.
- Per-lesson mastery: each lesson shows how many of its words have reached durable long-term memory (a mastery bar).
- Celebrations: confetti and floating XP on correct answers.
- Share: share your streak/XP via the system share sheet.
- Baked audio, cross-browser: pre-rendered
.mp3clips (Microsoft neural voices via freeedge-tts) play identically in every browser and offline. The app readsaudio/manifest.json, prefers the baked clip, and falls back to live browser TTS if one is missing. - Romanization (Latin letters), on by default: on-screen text is the romanized sound (e.g. "namaste"), so nobody has to read a script to speak.
showRomandefaults on. Turning it off is pure-ear mode: no target-language text appears at all (not the roman, not the usage note, and never the native script as a fallback), just the English meaning and the audio. That rule lives in one shared helper,lib/reveal.ts, so web and mobile can't drift, and it's covered by unit tests plus a content-integrity test that fails CI if any lesson leaks Devanagari onto the screen. There is no reading or writing instruction anywhere in the app: no alphabet drills, no script-recognition rounds, no writing practice. The no-script rule holds on every surface, not just lessons: the AI tutor's replies, the dialogue roleplay, and even the echo of your own recognized speech all render romanized, never the native script. Titles, subtitles, and usage notes are all romanized too. (Duolingo ships this exact toggle for Chinese pinyin and Japanese furigana, but not for Hindi, which expects Devanagari from lesson one. That gap is the product's reason to exist.) - Speaking check: the mic uses the browser's free speech recognition to transcribe and score what you say. Defaults on on both platforms (on mobile it's currently a no-op, since mic grading is stubbed and the self-check flow runs instead). Browser speech recognition is Chrome/Edge only; Safari and Firefox get nothing, so every caller checks
sttSupported()before promising it and falls back to a self-check flow that works everywhere. - Playback speed: adjust how fast the target audio plays (0.6x to 1.1x, default 0.9x).
- Marathi (2,884 items / 294 lessons) and Hindi (3,106 items / 274 lessons): greetings, deep vocab domains, verb conjugation, grammar, full sentences, situational dialogues, idioms/proverbs. Every item has a baked audio clip (5,990 total, 100% coverage). Registry-driven; adding a language is one module file plus one line.
- Generative sentence frames: each deck opens with six swap-and-go patterns (I want ___ / Where is ___? / Is there ___? / I want to go to ___ / Please give ___ / How much is ___?). The frame never changes, so learning one pattern plus a noun list yields dozens of real sentences instead of dozens of memorized strings.
- Content QA: content is LLM-authored, then reviewed pass-by-pass for gender agreement, register, and romanization consistency (124 corrections applied across both decks). This is model review, not a native speaker authoring it. See Known gaps.
- Sign in: Google or email magic link, via Supabase (env-gated; the app runs fully on-device without keys).
- Cloud progress sync: when signed in, streak/XP/cards sync to the cloud and merge across devices.
- Settings: show romanization, mic speaking check, playback speed, reset progress, tutor key.
- Web: Vite + React + TypeScript. Runs with no backend; progress in
localStorage(plus cloud when signed in). - Mobile: Expo + React Native, device TTS + AsyncStorage. Builds for the App Store / Play Store via EAS.
Content and core logic are byte-identical. The UI is behind:
| Feature | Web | Mobile |
|---|---|---|
| Onboarding, Lesson, Review, Speak | yes | yes |
| Today (guided session) | yes | yes |
| Placement diagnostic | yes | yes |
| Talk (tutor) | yes | no |
| Dialogues roleplay | yes | no |
| Play games (Lightning, Match) | yes | no |
| Units + topic chips + search | yes | no (flat lesson list) |
Mobile UI has never been visually verified: there's no Expo simulator in the dev environment it was written in, so it's type-checked and code-reviewed but unrun.
- Comprehensible input: meaning is always given in a language you know, audio-first; the tutor stays at your level using words you've seen.
- Retrieval practice: you produce the answer before hearing it; recall beats re-reading. Missed items requeue until correct.
- Spaced repetition:
lib/srs.ts(SM-2 variant): missed items reset to short intervals, recalled items grow geometrically (1 day → 3 → a week → a month). - Output practice: a full ladder from controlled to free: Speak drill → Dialogues roleplay → open Talk conversation with correction.
- Interleaving + easy→hard ordering: the guided path and drills mix topics and ramp difficulty.
- Desirable difficulty: production is demanding on purpose; requeue prevents easy passes.
cd web
npm install
npm run dev # http://localhost:5173Build for production: npm run build (static output in web/dist; deploy to Vercel,
Netlify, GitHub Pages, anywhere).
cd web
npm test # vitest run (one shot)
npm run test:watch # watch modeThe suite is small and high-value. content.test.ts runs against the real language
registry, so it enforces the product's core rules on every shipped lesson: never display
native script in any on-screen field, always keep it in target (which drives TTS), unique
ids, required fields present, and no em dashes in copy. reveal.test.ts locks the
romanization-toggle behavior (off = pure ear, no script fallback), and translit.test.ts
covers the Devanagari-to-roman transliteration. Adding content that breaks any of these
fails the build rather than reaching a learner.
Off by default. The app answers only two questions: how many people start a lesson, and how many come back the next day. That's activation and retention, the only signals that say whether Bol works for real users.
To turn it on, create a free PostHog project and set two env vars in your deploy (e.g. Vercel):
VITE_POSTHOG_KEY=phc_... # your PostHog project API key
VITE_POSTHOG_HOST=https://us.i.posthog.com # or eu.i.posthog.com; optional, this is the default
With no key set, lib/analytics.ts is a complete no-op: PostHog is never loaded (Vite drops
it from the bundle entirely), no network, no cookies, nothing tracked. Privacy-first by
design: it respects Do Not Track, sends no PII (only event names + coarse props like
language code and lesson title, never a name or anything the user said or typed), disables
autocapture and session recording, and is cookieless.
Events, and the questions they answer:
| Event | Answers |
|---|---|
app_open |
Retention. PostHog's retention chart counts a return on any day a user fires an event. |
landing_view → get_started → onboarding_complete → practice_started → practice_completed |
The activation funnel: where people drop off between landing and their first finished lesson. |
practice_started / practice_completed |
How many actually start (and finish) a lesson, the core activation metric. |
placement_completed, landing_taste_played |
Secondary signals (alternate activation, hero engagement). |
In PostHog: build a funnel on the five funnel events for drop-off, and a retention
report on app_open for come-back rate. Note: analytics is web only for now; the mobile
app has no deploy target yet, so it isn't instrumented (add the PostHog React Native SDK when
mobile ships).
Talk uses the Claude API (Opus 4.8) straight from the browser. Set
VITE_ANTHROPIC_API_KEY in web/.env.local, or paste a key under Profile → Settings →
Tutor key (kept on the device, never synced). A key in a client-side build is
extractable: fine for personal use; add a small proxy before sharing the app publicly.
Sign-in and cloud sync are optional and env-gated. Set VITE_SUPABASE_URL and
VITE_SUPABASE_ANON_KEY in web/.env.local, create a progress table with row-level
security, and add your dev/prod URL to the Supabase redirect allowlist. Without keys the
app runs fully on-device.
cd web
TTS_PROVIDER=edge npm run audio:gen # FREE, no key (pip install edge-tts)
# or higher-end providers:
TTS_PROVIDER=google GOOGLE_API_KEY=... npm run audio:gen
TTS_PROVIDER=elevenlabs ELEVENLABS_API_KEY=... ELEVENLABS_VOICE_ID=... npm run audio:gen
TTS_PROVIDER=azure AZURE_SPEECH_KEY=... AZURE_SPEECH_REGION=... npm run audio:genedge uses Microsoft's neural voices (e.g. mr-IN-AarohiNeural, hi-IN-SwaraNeural)
through the open-source edge-tts package: free, no key, good quality. The bake is
resumable (skips existing clips). It writes web/public/audio/<lang>/<id>.mp3 and an
audio/manifest.json the app reads on startup.
One file plus one line. The app is data-driven, and every feature comes along automatically.
-
Create
web/src/content/<lang>.ts(copyhindi.ts). Each phrase:{ id: "hi-water", target: "पानी", roman: "paani", translation: "Water" }
Each lesson carries a
level: 1 | 2 | 3 | 4for the guided path order and the Speak drill. -
Register it in
web/src/content/languages.ts(available: true). -
Copy the content into
mobile/src/content/, add a tutor persona inlib/tutor.ts, then bake audio.
Real Expo project. Uses the device's built-in TTS (expo-speech) and AsyncStorage.
cd mobile
npm install
npx expo start # scan the QR with Expo Go (no Mac needed to try it)Ship via EAS (cloud build, no Mac required):
cd mobile
npm install -g eas-cli
eas login
eas build --platform ios --profile production
eas submit --platform iosIdentifiers (com.roshan.bol), icons, and splash are configured in mobile/app.json and
mobile/assets/. Mic grading on mobile is stubbed (self-check used); enable by adding
expo-speech-recognition and returning true from sttSupported().
Honest list. Last updated 2026-07-17.
Fixed 2026-07-17 (kept here briefly for the record): the romanization toggle used to be
inconsistent (one branch showed nothing, another fell back to Devanagari); usage notes
ignored the toggle and hundreds of them contained native script that rendered mid-drill
even with romanization on; and useSpeechCheck defaulted differently on web vs mobile. All three
are resolved, the display rule is now one shared, unit-tested helper, and a content-integrity
test guards against script or em dashes ever leaking back into a lesson.
Content is LLM-authored, not native-authored. It has been QA'd pass-by-pass by model review (124 corrections for gender agreement, register, romanization), which is not the same as a native speaker writing it. Highest-risk areas are idioms and register.
Audio is TTS, not human. Microsoft neural voices are good, but they are not a native speaker, and prosody on long sentences is the weak spot.
Mobile UI is unrun. Type-checked and reviewed, never rendered. See Mobile parity.
The tutor key is extractable. VITE_ANTHROPIC_API_KEY ships in a client-side bundle.
Fine for personal use; needs a proxy before the app is shared publicly.
No real users yet. Every claim about what works for learners is grounded in research and reasoning, not in observed retention.
brand/ holds the vector identity: a bold, centered B letterform in a blue gradient
on a white squircle (trust palette; blue #2563eb). scripts/render-icons.mjs rasterizes
it into the web favicons and all mobile icon/splash PNGs. See brand/brand.md.