Children's multilingual English learning app — 7 worlds, 6 languages, curated translations
A complete educational platform built solo from scratch — teaching English to children aged 6–15 through interactive story-driven quests across 7 themed worlds. Each world supports two age modes (Kids and Tweens) with entirely separate content. The app features a 3-tier curated translation system supporting Czech, German, Spanish, French, Italian, and Portuguese, a custom admin workspace for managing translations at scale, a profileless identity system with memorable adventure codes, daily streaks, vocabulary tracking, and a gamification layer with scoring and celebrations. No WordPress, no templates. Everything designed, architected, and built from zero.
Status: In private beta with passcode-gated access. Core platform is fully functional — 7 worlds, quest engine, translation system, admin CMS, profiles, streaks, and vocabulary tracking are all operational. Content curation and testing ongoing.
Imaginaria Worlds — landing page with emerald gradient, Leo the astronaut, and "Let's Go!" CTA
Not just a learning app
Children see a colourful adventure. Behind it: a 3-tier translation cascade with curated word maps in JSONB, a structured content architecture spanning 7 worlds with dual age modes, a custom admin workspace for managing hundreds of translations, and a database design that tracks vocabulary growth per user without any authentication system.
All educational content lives in 7 structured JSON files at src/content/worlds/. Each world — Space Mission, Deep Ocean, Dinosaur Discovery, Dragon Tales, Enchanted Castles, Jungle Expedition, Mystic Mountains — defines a complete data tree: slug, title, order, art paths per mode, intro/outro narrations, media references (video, audio, art), and a nested array of topics containing quests. Each quest carries a question with kids/tweens variants, multiple-choice options with per-option audio, correct/incorrect feedback, media for each flow step, and sound effects. Every world supports two age modes — Kids ("Little Explorers") and Tweens ("Epic Adventurers") — with completely separate narration text, artwork, and videos per mode. A listWorlds() server function reads these files at runtime using Node.js fs, sorts by order, and supports slug aliases for backwards-compatible URL routing.
Key Details
7 themed worlds with full data trees in structured JSON
Dual age modes — Kids and Tweens with separate content per mode
Nested architecture: World → Topics → Quests → Options
Per-quest media: videos, audio narration, artwork, sound effects
listWorlds() server function with fs-based runtime loading
Slug aliases for backwards-compatible URL routing
World selection — 7 worlds with Kids/Tweens tab filters and mode-specific cover art
The app guides children through a carefully designed progression. It starts at the landing page with an emerald gradient and Leo the astronaut character, then flows through a language picker (6 languages with flag icons), world selection (filterable by age mode), world landing with intro video and narration, quest selection with progress badges, the core quest flow (Watch → Read → Answer → Celebrate), and finally a world completion page with score summary and rating. Each transition is intentional — checking localStorage for language preference before routing, hiding the navigation header entirely during quests for immersive mode, and firing confetti on correct answers. The quest flow is the heart of the experience: a looping video builds context, tappable narration text teaches vocabulary, multiple-choice tests comprehension, and streak celebrations reward consistency.
Key Details
Landing → Language → Choose → World → Quests → Quest Flow → Complete
Language check on entry — routes to /language if not set, /choose if set
World landing with intro video from Supabase Storage and narration text
Quest cards with progress badges (completed, first-try, score)
Immersive quest mode — header hides entirely on /quest/* routes
WATCH → READ → ANSWER → CELEBRATE four-step quest flow
Language picker — 6 languages (Czech, German, Spanish, French, Italian, Portuguese) with flag icons
World landing — intro video, narration text, and "Begin Adventure" button
Quest flow — WATCH step with looping video and "I'm Ready → Read the Story" button
Quest flow — READ step with tappable narration text and floating translation tooltip
This is the technical heart of the app. The /api/translate route implements a priority waterfall for every word tap. Tier 1 checks the quest_narrations word map — a curated, context-aware, per-quest dictionary stored as JSONB in Supabase. This is the highest-quality translation because it knows that "trunk" means "tree trunk" in a jungle quest, not "car trunk." Tier 2 falls back to the word_translations table — cross-world vocabulary overrides for consistent translations of recurring words. Tier 3 falls back to Google Translate's free endpoint (no API key needed) for any word not yet curated. The same cascade applies to full narration translation via the POST variant — checking quest_narrations.text_[lang] before falling back to Google Translate sentence by sentence. This means the app works immediately for any word in any language, but the quality improves over time as translations are curated through the admin workspace.
Architecture Decision
The cascade design means the app is immediately functional in all 6 languages from day one — Google Translate handles everything not yet curated. But as I work through the admin workspace, Tier 1 curated translations progressively replace the automatic ones, improving quality without any code changes or redeployment.
Key Details
Tier 1: quest_narrations.map_[lang] — curated per-quest word maps (JSONB)
Tier 2: word_translations table — cross-world vocabulary overrides
Tier 3: Google Translate free endpoint — automatic fallback
Context-aware: "trunk" → "tree trunk" in jungle, not "car trunk"
POST variant for full narration translation with same cascade
Quality improves over time as translations are curated in admin
Word tap — floating tooltip showing curated translation from the 3-tier cascade
A full-screen dark CMS at /admin/translations for curating all translations that power the Tier 1 and Tier 2 systems. The left sidebar lists all quests grouped by world with a live search filter. Selecting a quest loads its data into the right panel, which has four controls: a Kids/Tweens mode toggle (switching which narration variant is being translated), a language selector with six flag icons, a translation text area with a "Google Translate ✨" button for auto-populating then manually correcting translations, and a word map editor showing every unique English word from the narration as an editable row with its current translation. Common grammatical words (a, the, is, are) are pre-marked as null (skipped). A "Generate from text" button auto-populates the word map via positional alignment between English and translated word lists. On save, everything upserts to quest_narrations using the composite key (world_slug, quest_id, mode).
Key Details
Full-screen dark CMS at /admin/translations
Left sidebar: quest list grouped by world with live search
Kids/Tweens mode toggle for separate narration variants
6-language selector with flag icons
Translation text area with Google Translate auto-populate + manual edit
Word map editor — per-word editable grid with null-skip for grammar words
"Generate from text" for positional word-map alignment
Upserts to quest_narrations with composite key (world_slug, quest_id, mode)
Translation workspace — quest sidebar, mode toggle, language selector, and translation editor
Word map editor — per-word translation grid with skip markers for grammar words
Language selector with Google Translate auto-populate button
No login, no email, no password. Users are identified by a memorable 3-part adventure code (WORD-WORD-0000) generated at profile creation and stored in localStorage. The /welcome page lets users pick a name (max 20 chars) and choose from 16 avatars (8 kid-style, 8 tween-style characters). On submit, createProfile() inserts a row into Supabase profiles and returns the adventure code. The /profile page shows avatar, name, score, daily streak badge (active fire or dormant), a 3-stat grid (words learned, worlds explored, quests done — all derived from user_vocabulary), and the adventure code as a copyable button. Below the stats sits the VocabularyBook component — a scrollable list of all tapped words grouped by world with a language-switcher to view translations in any of the 6 supported languages.
Key Details
No authentication — adventure codes (WORD-WORD-0000) as identity
16 avatars: 8 kid-style + 8 tween-style characters
Profile stored in Supabase, code in localStorage
Stats derived from user_vocabulary: words, worlds, quests
VocabularyBook component with per-world grouping and language switcher
Copyable adventure code for cross-device access
Profile page — avatar, name, score, streak badge, 3-stat grid, and adventure code
Vocabulary Book — all tapped words grouped by world with language switcher
The gamification system runs on two tracks — localStorage for quest-level progress and Supabase for persistent streaks. Correct answers award 10 points, a 3+ answer streak adds 5 bonus points, world completion gives 25 points, and a perfect world (all first-try) gives 50 points. Quest progress (completed, first-try success, score 1–3, attempts) is stored in localStorage under keys like worldProgress:kids:space-mission. The daily streak is stored in Supabase profiles (streak_count and last_quest_at) so it persists across devices. The StreakCelebration component fires on quest completion — showing a flame banner if the streak is 2+ days, with canvas-confetti for visual reward. Every correct answer triggers an SFX, incorrect answers allow retry, and the celebrate step uses Framer Motion for smooth transitions.
Key Details
+10 per correct answer, +5 streak bonus, +25 world complete, +50 perfect world
Quest progress in localStorage with per-world keys
Daily streak in Supabase profiles — persists across devices
StreakCelebration component with flame banner and confetti
canvas-confetti for visual celebration on correct answers
Framer Motion transitions between quest flow steps
ANSWER step — multiple choice with SFX feedback on selection
CELEBRATE step — confetti, streak fire banner, and continue button
Streak celebration — flame banner on 2+ day consecutive streak
Four PostgreSQL tables in Supabase power the entire backend. profiles stores user identity (adventure code, name, avatar, score, streak_count, last_quest_at, native_lang). quest_narrations is the curated translation table with a composite key (world_slug + quest_id + mode) — it stores the original English text, full translated narrations per language (text_cs, text_de, text_es, text_fr, text_it, text_pt), and JSONB word maps per language (map_cs through map_pt) mapping each English word to its context-aware translation. word_translations is a cross-world vocabulary override table (word_en, world_slug, lang, translation) used as Tier 2 in the cascade. user_vocabulary records every word a user has tapped — with upsert logic incrementing times_seen on repeat taps of the same word.
Key Details
profiles: adventure code identity, score, streak, native language
quest_narrations: composite key, 6-language text + JSONB word maps
word_translations: cross-world vocabulary overrides (Tier 2)
user_vocabulary: per-tap records with upsert times_seen increment
No authentication tables — identity via adventure codes
JSONB columns for flexible per-language word map storage
Videos are hosted in a Supabase Storage bucket (imaginaria-videos) organised by world with per-mode variants (kids-intro.mp4, tweens-intro.mp4, per-quest videos). These are referenced via direct Supabase CDN URLs embedded in the world JSON files. Audio files (MP3 narration) and images (JPG/PNG artwork) are served from Vercel's static file system under /public. Four Python upload utility scripts in the repo root handle batch media operations: uploading from the local filesystem to the Supabase bucket, batch compression, and direct upload. The split approach keeps the most bandwidth-heavy assets (video) on Supabase CDN while keeping smaller assets (audio, images) fast and simple on Vercel's edge network.
Key Details
Supabase Storage bucket for video files with per-world directories
Per-mode video variants: kids-intro.mp4, tweens-intro.mp4, etc.
Vercel static serving for audio (MP3) and images (JPG/PNG)
4 Python batch-upload scripts for media management
Supabase CDN URLs embedded directly in world JSON files
Split delivery: heavy video on Supabase CDN, light assets on Vercel edge
Seven API routes power the app: GET/POST /api/translate for the 3-tier translation cascade (GET for single words, POST for full narrations), GET /api/worlds and /api/worlds/[slug] for world data, GET /api/quests/[id] for quest data, GET /api/health as an unprotected health check, and dev-only debug endpoints. The entire app runs behind a beta gate — middleware.ts checks a BETA_PASSCODE environment variable and protects all routes except health checks and static assets. Access is unlocked via a ?beta=CODE query parameter (sets a 30-day httpOnly cookie) or an existing cookie. Unauthenticated requests see an inline HTML gate page with a passcode form — no external assets needed. PWA support is configured via a Web App Manifest with apple-mobile-web-app-capable metadata and a service worker registration component.
Key Details
GET/POST /api/translate — 3-tier cascade for words and narrations
World and quest data APIs with server-side JSON loading
Beta gate middleware with BETA_PASSCODE environment variable
30-day httpOnly cookie for persistent beta access
Inline HTML gate page — zero external dependencies
PWA manifest with apple-mobile-web-app-capable and service worker
Beta gate — inline passcode entry form protecting all routes
The visual identity is built around emerald green (#10b981) — evoking nature, adventure, and calm learning. The landing page uses an emerald gradient with a glow-pulsing "Let's Go!" button and Leo the astronaut as a friendly guide character. World cards display mode-specific cover art in 3:4 aspect-ratio tiles with inner glow borders. The AppHeader renders a three-item bar (LanguageSelector, Logo, HamburgerMenu) and reads usePathname() to return null entirely on /quest/* routes for full immersive mode. The HamburgerMenu provides navigation to Choose, Profile, About, Feedback, and conditionally Admin. All quest flow transitions use Framer Motion for smooth step changes. The app is fully responsive with Tailwind CSS v4 utility classes throughout.
Key Details
Emerald green (#10b981) identity — nature, adventure, calm
Leo the astronaut as friendly guide character
AppHeader hides entirely during quests for immersive mode
HamburgerMenu with conditional admin link
Framer Motion transitions in quest flow
3:4 world cards with inner glow borders and mode-specific art
Engineering Challenges
Context-aware translation for children's vocabulary
A single-word lookup via Google Translate has no context — "trunk" could be a tree trunk, car trunk, or elephant trunk. Designed a 3-tier cascade where Tier 1 (curated per-quest word maps) provides context-aware translations set manually in the admin workspace. This means every word in every quest can have a different translation depending on the story context, while Tiers 2 and 3 provide automatic fallbacks so the app works immediately for uncurated content.
Dual age modes with separate content across 7 worlds
Kids and Tweens need different narration text, artwork, and videos for the same quest structure. Designed the world JSON schema with mode-aware fields at every level — narration.question.kids / .tweens, art.kids / .tweens, media variants per mode. The admin workspace has a Kids/Tweens toggle so translations are curated separately per mode. A single quest component handles both modes by resolving the active mode from the URL path.
User identity without authentication
Children can't manage passwords, parents won't create accounts for a learning app they're trying out. Designed an adventure code system (WORD-WORD-0000) — memorable enough to write down, unique enough to identify a user. Profile data lives in Supabase, the code lives in localStorage. Cross-device access works by entering the adventure code on a new device. No email, no password, no OAuth flow.
Building a translation admin that scales to hundreds of quests
With 7 worlds × multiple quests × 2 modes × 6 languages, the translation matrix is enormous. Built the admin workspace with a searchable quest sidebar, mode toggle, and language selector so any specific combination is one click away. The "Google Translate ✨" button auto-populates a starting translation that can then be manually refined. The "Generate from text" feature auto-creates the word map from positional alignment — cutting per-quest curation time from minutes to seconds.
Scoring split between localStorage and Supabase
Quest-level progress needs to be instant (no network latency during gameplay) but streaks need to persist across devices. Split the storage: quest completion, scores, and attempt counts go to localStorage for zero-latency writes during gameplay. Daily streak goes to Supabase profiles so it persists. On profile creation, the two systems link via the adventure code.
Reflection
Imaginaria Worlds is the project that turned me from someone who builds websites into someone who builds products. The difference is enormous. A website has pages. A product has systems — and those systems need to work together, evolve independently, and serve users who will never read a single line of your code.
The 3-tier translation cascade was the biggest architecture lesson. Designing a system where quality improves progressively — from automatic Google Translate on day one to fully curated context-aware translations over time — without any code changes or redeployment, taught me to think in layers. Every hard problem in this project was solved by adding a layer, not by replacing what was already working.
Building the admin workspace taught me that the tool you build for yourself is just as important as the tool you build for users. A bad admin interface means you stop curating content. A good one means you open it every day and the product gets better without writing code.
And designing for children — the most honest users in the world — taught me that if something is confusing, they simply leave. No polite feedback, no workarounds. That discipline shaped every UX decision: progressive disclosure, minimal text, visual rewards, and a flow that never asks a child to make a decision they don't understand.
Skills Demonstrated
Product architecture: content engine, translation system, identity layer, gamification
Structured content design with nested JSON world schemas
3-tier translation cascade with JSONB word maps and progressive curation
Custom admin CMS for managing translations at scale
Supabase: PostgreSQL tables, Storage CDN, real-time upserts
Profileless identity system with adventure codes
Gamification: scoring, streaks, celebrations, vocabulary tracking
Next.js 15 App Router with React Server Components
Framer Motion and canvas-confetti for delightful UX
Beta gate middleware with httpOnly cookie authentication
PWA manifest and service worker for app-like experience
Python utility scripts for batch media management
Outcome
7 themed worlds with dual age modes. A 3-tier translation cascade supporting 6 languages with progressive curation. A custom admin workspace for managing hundreds of translations. An adventure-code identity system with no authentication. Daily streaks, vocabulary tracking, and gamified celebrations. Supabase for database and media storage. Next.js 15 with TypeScript end to end. Deployed on Vercel with beta gate middleware. Every line written by one person.