▶ Open the app · Spec · Curriculum · Changelog · Roadmap
Swedish is a free web app for learning Swedish from zero — and for building Stockholm while you do it. Every lesson takes about five minutes, every test is drawn from a large question pool, and every coin you earn goes into a city that grows across ten eras, from the first settlement on the island to a city among the stars.
No account, no server, no ads: progress lives in your browser, the app installs as a PWA and works offline. Sign in with Google only if you want sync across devices.
| 400 lessons |
8 levels, SFI A → SVA grund 4 |
≈5 400 words with audio |
31 everyday dialogues |
20 grammar summaries |
10 city eras, 38 buildings |
![]() |
![]() |
![]() |
| Topics and levels | A quiz in progress | Your Stockholm |
Each shot is an era with every one of its buildings finished — what the map looks like just before the next era opens. The first six are real history; the last four are informed guesses, and the app says so (SPEC.md §12.8).
![]() 1. The Settlement |
![]() 2. The Viking Age |
![]() 3. The Middle Ages |
![]() 4. The Age of Empire |
![]() 5. The Industrial Age |
![]() 6. Modern Stockholm |
![]() 7. The Green City |
![]() 8. The Connected City |
![]() 9. The Floating City |
![]() 10. The Star City |
- Pick a topic. Two tracks in order, SFI (kurs A–D) and then SVA grundläggande (delkurs 1–4). Opening a level takes you straight to the first topic you haven't passed.
- Read and listen. Short theory and a word list for each lesson, with every form of each
word, an example sentence, and
sv-SEspeech on anything Swedish — a tap is enough. - Take the test. Ten questions drawn from the lesson's pool, with feedback after each one. You get one free retry per run and no lives, and you can't lose progress.
- Build Stockholm. Spend your XP and coins on the city. Its buildings give perks (hint tokens, extra retries, XP and coin multipliers, daily income) that make the next lesson easier. Every new building and upgrade goes up with fireworks.
| 📚 Lessons by level | 400 five-minute lessons across SFI A–D and SVA grund 1–4; a longer topic is split into numbered parts |
| 🔊 Audio everywhere | Speech synthesis on every Swedish word, form, example and dialogue line, with a voice and speed picker; a Swedish word in a test is spoken as you pick it |
| 🧩 Quiz engine | Multiple choice, listening, typed answers, gap fills, word order, matching and true/false, weighted-sampled from each lesson's pool, with typo tolerance on typed answers |
| 🔁 Spaced repetition | One Leitner-box review deck across every lesson you have studied |
| 📖 Reference | Grammar summaries, a searchable dictionary of every word taught, and 31 everyday dialogues you can listen to or act out one role at a time |
| 🏙️ The city | Ten eras and 38 buildings drawn as an isometric scene with citizens, boats, weather and lights at night; each building gives a perk |
| 📜 History cards | Short, sourced notes (Birka, the rune stones, the first written mention in 1252, the Vasa, the metro) unlocked by the buildings they belong to |
| 🔥 Streaks and achievements | Daily streak with freezes, 36 achievements with up to six tiers each, and a stats page with medals and progress |
| 🌗 Comfortable to use | Light and dark themes, keyboard navigation, prefers-reduced-motion respected, sounds you can mute |
| 🇬🇧 🇷🇺 Two interface languages | One switch changes the interface and every translation shown |
| 💾 Local-first | Everything lives in localStorage; export and import a JSON save file |
| ☁️ Cloud sync (optional) | Sign in with Google to sync across devices, merged field by field so nothing is lost |
git clone https://github.com/gray0072/swedish.git
cd swedish
npm install
npm run dev # http://localhost:5173/swedish/| Command | What it does |
|---|---|
npm run dev |
Dev server with hot reload |
npm test |
Vitest unit tests — quiz engine, grading, selection weighting, SRS, city |
npm run validate |
Validates every content file against its Zod schema |
npm run build |
Type-check and production build into dist/ |
npm run preview |
Serve the production build locally |
npm run new:lesson |
Scaffold a new lesson folder |
npm run screenshots |
Regenerate the README screenshots from the running app (see below) |
- React 18 + TypeScript (strict), built with Vite 6; content is loaded with
import.meta.glob. - React Router with
HashRouter, so GitHub Pages needs no server configuration. - Zustand for app state, persisted to
localStorage. - Zod content schemas, checked at build time and in CI.
- Tailwind CSS with a small Swedish palette: falu red, birch, gold, pine and aurora. There is no component library.
- Web Speech API for pronunciation and Web Audio API for sound effects. Every sound is synthesized, so the app ships no audio files.
- vite-plugin-pwa for install and offline use.
- Supabase (Auth with Google PKCE, plus Postgres) is optional and used only for sync.
Every document is in English; the ones marked 🇷🇺 also have a Russian translation next to
them with the _ru suffix.
| Document | What's in it |
|---|---|
| SPEC.md 🇷🇺 | The product spec (an index of specs/), which has the final say on design decisions |
| CURRICULUM.md 🇷🇺 | The lesson-topic plan for every level; start here when picking what to write next |
| REFERENCE.md 🇷🇺 · DIALOGUES.md 🇷🇺 | Plans for the reference section and the dialogues |
| CITY_VISUALS.md | How the city scene is built: grid, buildings, motion, life |
| TODO.md 🇷🇺 | Outstanding work, content gaps and known discrepancies |
| CHANGELOG.md 🇷🇺 | Development log |
| AGENTS.md | Rules for contributors, human or agent |
Build & deploy
A push to main runs .github/workflows/deploy.yml, which
validates content, runs the tests, builds, and publishes dist/ through the official
actions/upload-pages-artifact + actions/deploy-pages flow (Settings → Pages → Source:
GitHub Actions). vite.config.ts sets base: '/swedish/' to match the repo name —
update it if the repo is ever renamed or moved to a custom domain.
npm run deploy (via the gh-pages package) is a manual alternative that publishes a local
build to a gh-pages branch instead of waiting on CI.
Cloud sync — setting it up for your own fork
Progress works fully offline out of the box (localStorage + export/import). Signing in with
Google in Settings additionally syncs the same save file through a Supabase project — merged
field by field (lessons, SRS items, buildings, wallet, streak, …) so progress made on two
devices between syncs is combined rather than one side overwriting the other. Full scheme:
SPEC.md §7.1.
- Create a free Supabase project.
- Run supabase/schema.sql once in its SQL editor — one
savestable with row-level security scoping every row to its own user. - Enable the Google auth provider (Authentication → Providers), which needs a Google Cloud OAuth client id/secret, and add your dev and prod URLs under Authentication → URL Configuration → Redirect URLs.
- Copy
.env.exampleto.env.localand fill inVITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEYfrom Project Settings → API. Neither value is secret — the anon key ships in the client bundle by design and RLS is what protects the data — but.env.localis gitignored anyway to keep per-fork keys out of the repo. - For the CI deploy, add the same two values as repository secrets (Settings → Secrets
and variables → Actions) named exactly
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEY. Skipping this is the usual reason a deployed build has no sign-in button — it isn't broken, it just built without them.
Without those variables the app builds and runs exactly the same, with cloud sync compiled out.
Google sign-in redirects to localhost on the deployed site? That is a Supabase setting,
not a code bug: Supabase falls back to its Site URL (default http://localhost:3000)
whenever the real redirect isn't in the Redirect URLs allow list. Set Site URL to your
deployed URL (e.g. https://<user>.github.io/swedish/) and add both that and
http://localhost:5173/swedish/ to Redirect URLs.
Screenshots — how they are made
Every shot except the quiz one is generated from the running app by npm run screenshots
(scripts/screenshots.ts). It boots the dev server, seeds a
throwaway finished save and drives your installed Chrome. Add -- pages or -- eras to redo
only that half. Re-run it whenever the scene or the chrome around it changes, and never edit
those PNGs by hand.
The banner at the top, docs/banner.png, which is also the repository's social preview, is rendered from docs/banner.html at 1280×640. The link preview of the deployed app, public/og-image.png, is rendered from docs/og-image.html at 1200×630.
Project structure
content/ all learning content — lessons, vocab, questions, city, history
lessons/<level>/<slug>/ lesson.json, theory.md, theory_ru.md, vocab.json, questions.json
curricula/ ordered lesson-id playlists per track
dialogues/ everyday dialogues for the reference section
reference/ grammar summaries (English + _ru)
city/ eras.json, buildings.json
history/ short sourced history cards
src/
content/ Zod schemas, the content loader/registry, vocab → quiz generators
quiz/ session creation, weighted selection, grading, hints, reward math
srs/ Leitner-box review scheduler
city/ economy, perks, era progress
store/ Zustand store (wallet, progress, city, settings), save/export/import,
optional Supabase cloud sync
components/, pages/ UI; the city scene lives in components/city/scene
lib/ speech, synthesized sounds, share card, helpers
scripts/ content validation, lesson scaffolder, screenshot generator
supabase/ schema.sql — the `saves` table + RLS policies
tests/ Vitest unit tests
MIT. The Swedish is Sweden's; the mistakes are ours — issues and pull requests are welcome.












