TaleSelf
A story game for learning languages, where you are the main character.



Drills teach words. Stories make you stay.
Most language apps are good at flashcards and bad at the thing that actually makes you fluent: reading a lot of text you care about. Graded readers help, but a book does not react to you, and nobody finishes a book that does not hook them.
TaleSelf puts the learner inside the story. You pick a language and a level, choose a character that looks how you want, and read an adventure where people call you by name. At every step you decide what happens next, and the story bends to that choice. Every passage can be read aloud by a narrator, and any word can be tapped, translated and saved.
One shared story, written only once.
Generating a fresh chapter for every reader would be slow and expensive. Instead every passage is shared: the first reader to open a branch pays for it, and everyone after reads it instantly.
- 1
A reader picks a choice
Each step offers three choices, each tagged with its own CEFR level, so a choice can be easy or a stretch.
- 2
Explored before?
If someone already took this branch, the passage is read from Postgres. No model call, no wait, no cost.
- 3
If not, Gemini writes it once
The model writes the next passage in English as a template with slots, guided by recent passages for continuity.
- 4
Hydrated for every player
Slots like {name}, {they} and {their} are filled with each reader’s character, then translated to their language.
{name} stands near the tall doors. For an hour {they} have listened to the whispers of the court.Aryan se tient près des hautes portes. Depuis une heure, il écoute les murmures de la cour.Drawn in code, not in Photoshop.
A character is a small piece of data: hair style, mustache, glasses and six colors. The app paints it with a Flutter CustomPainter, and the website and admin draw the very same shapes as SVG, in the same 190 by 250 units. Because it is code, the mouth lip-syncs while the narrator talks, the eyes blink and look around, and a new character costs a few bytes instead of an illustrator’s week.
These are the real characters from the app, rendered by the same function. Click one to hear its recorded hello, each in a different language; the mouth follows the voice. Two of them wear the king and queen outfit a league winner gets.
- Mia
- Leo
- Zara
- Omar
- Lena
- Noah
- Aisha
- Ravi
Built like a game, so people come back.
Learning a language is a habit problem. Every reward flies to its counter, every unlock gets a celebration, and the numbers are tuned so nothing can be bought around the habit itself.
Streaks with gifts
A 5-day streak unlocks a blue character; a chest opens every 10 days. A streak freeze costs 150 coins and you can hold 2.
Weekly leagues
Groups of 30 readers across six tiers, Bronze to Diamond. Top 5 move up, bottom 5 move down, podium gets coins.

A word ladder
Save any word while reading. Spaced repetition brings it back after 1, 3, 7, 14 and 30 days until you know it.

A shop that respects you
Coins buy characters, refills and freezes. Rewarded ads are optional and keep the game free to play.

The stack, end to end.
App
- Flutter & Dart
- Provider
- flutter_animate
- CustomPainter avatars
- Local notifications
- AdMob rewarded ads
API
- Node.js & TypeScript
- Fastify + Zod
- Drizzle ORM
- PostgreSQL on Neon
- Vercel
AI
- Gemini on Vertex AI for stories
- Gemini translation
- Gemini text to speech narration
Web & tools
- Marketing website
- React admin
- Vitest
- Sentry
- Infisical for secrets
What I learned
- Cache the expensive thing at the right level. Sharing passages across players turned LLM cost from per reader into per branch.
- Templates beat regeneration. Writing once in English with slots, then hydrating and translating, keeps 22 languages consistent.
- Game feel is a feature, not polish. A number that silently changes teaches nothing; a coin that flies to its counter with a sound feels like progress.
Light and dark, nine app languages, right to left included.
The interface ships in English, German, French, Spanish, Italian, Dutch, Turkish, Persian and Arabic, with full RTL layouts.
