Missing Vocabulary Images Resolved — Generated and converted flat vector permit card illustrations for arbetstillstand.webp and uppehallstillstand.webp using DALL-E and Pillow, and duplicated base terms for their definite variations (kassan.webp, beslutet.webp, arendenummer.webp).
Missing Lesson Illustrations Resolved — Mapped directions, map, and briefcase to customized icon/gradient fallbacks, and linked housing to apartment.webp and transport to bus.webp inside IntroStepView.tsx.
Theme-Aware Backgrounds — Swapped the hardcoded white login card background with the theme-aware var(--color-bg-card) variable, ensuring the login form correctly renders as dark grey in dark mode.
Login Form Scaling — Increased font sizes, input padding, button dimensions, card border radius, and overall component scaling for a highly accessible and spacious login interface.
Header Sign In Link — Integrated a dedicated 'Sign in' link directly in the sticky navigation header of the home page, making login visible without scrolling to the footer.
Parallax Grid Alignment — Fixed card column baseline misalignments on scroll by translating grid row containers as unified units instead of applying index-based offsets to individual cards.
Hardware Acceleration — Added will-change: transform css rules to scroll containers to optimize rendering and ensure smooth, jitter-free scrolling.
Every-Section Scroll Parallax — Infused dynamic scroll translations throughout all landing page sections (Module Grid, Three Principles, and Testimonials), adding organic drifting glow blobs and floating 3D spheres.
Staggered Card Offsets — Programmed page grids to slide vertically at staggered velocity rates depending on their card index (idx * 0.01 offsets). This produces a fluid waves-like layering drift as the user scrolls.
3D Hover Spotlight Grids — Scaled the 3D mouse hover perspective tilt and mouse-tracking color spotlights to the Three Principles cards and the Testimonial cards for unified tactile responses.
Styled Section Headers — Structured section headings with uppercase contextual tag overlays to match high-fidelity agency layout aesthetics.
Hiding Studied Cards — Modified the Study Mode list to completely hide studied questions by default, preventing scroll fatigue when multiple questions are learned.
Visa/Dölj Toggle — Added an interactive 'Visa/Dölj inlärda frågor' (Show/Hide learned questions) toggle button above the questions list for inline reviews and undos.
Learned Completion Empty State — Created a responsive completion empty state card that congratulates the user when all 20 sample questions are marked as studied and filters are active.
IconEyeOff Support — Added the IconEyeOff component mapping inside Icon.tsx to handle the toggled eye hide states.
Collapsible Studied Questions — Redesigned the question list inside the citizenship test study tab. When a question is marked as studied (Kan det ✓), the details collapse into a compact Knowledge Book entry that summarizes the core fact (correct answer and explanation) to save screen space.
Persistent Knowledge Book Sidebar — Embedded a reusable, collapsible Knowledge Book drawer panel inside the left sidebar layout. The panel is visible across all test preparation pages, providing instant, scrollable access to all studied facts.
Syntax Verification — Cleared syntax anomalies at the bottom of the main app landing page, ensuring type-safe and compilation-friendly production bundles.
Scroll Parallax Event Listeners — Implemented real-time scroll tracking using passive window event listeners to translate page depths dynamically on scroll.
Cinematic Text Fading — Integrated scroll-responsive scaling and opacity fades on the primary marketing headline banner. Scrolling down smoothly lifts the title and fades it away.
Multi-Velocity Vector Drift — Mapped background 3D glass spheres and metallic gold rings to travel at fast velocity rates relative to scroll depth, producing clear camera-lens depth planes.
Card & Dock Scroll Sliders — Added slower vertical scroll translations to the action cards and glassmorphic stats dock to create fluid multi-layer foreground offsets.
Photorealistic 3D Vector Spheres — Rendered custom 3D glass spheres and gold rings using pure SVG gradients. These assets float dynamically behind the cards at offset parallax depth layers.
3D Card Perspective Tilt — Programmed cards to rotate and skew in 3D perspective space (rotateX, rotateY, translateZ) when hovered, snapping back smoothly on mouse leave.
Modern Sans-Serif Typography — Upgraded card title displays from classic serif to an ultra-bold, high-contrast sans-serif font layout for a sleek, contemporary digital agency aesthetic.
Real-time Cursor Spotlights — Implemented hover tracking spotlight overlays. Cursor movement projects a high-contrast radial spotlight gradient inside each card reflecting cursor focus.
Cinematic Headline Gradients — Styled the hero header calling-to-action text with a vibrant moss-green to sky-blue linear text color gradient.
Double-Layer Parallax Backgrounds — Integrated responsive parallax listener layers controlling large ambient light glows and background wireframe shapes sliding in opposition to the mouse cursor.
Glassmorphic Stats Dock — Refactored the statistics shelf into a unified floating glassmorphism dock featuring borders, shadow filters, and backdrop blurring.
Laptop Screen Heights Fitting — Reduced padding, margin, title sizes, and text descriptions throughout the Hero section to ensure the main calls-to-action fit comfortably above the fold on compact laptop monitors (like Lenovo Thinkpads).
Visual Card Headers & Inline Chips — Relocated visual assets (Dala Horse and Swedish emblem) into the card header rows, and replaced vertical checkmark lists with space-saving inline feature pills, saving 150px+ vertical height.
Premium 3D Swedish Emblem — Generated a custom 3D glassmorphic Tre Kronor (Three Crowns) gold emblem WebP image, resolving the Windows system letters 'SE' fallback for the Swedish flag emoji.
Above-the-Fold Citizenship Prep — Redesigned the homepage hero as a dual-choice split grid, positioning SFI Everyday Swedish and Citizenship Prep side-by-side. Users can instantly choose their learning route without scrolling.
Interactive Cursor Parallax — Injected an immersive mouse-movement listener that dynamically moves background depth glows, Dala Horse, and official flag badges at varying velocities.
Trilingual Card Translation — Added localized headings, descriptions, and feature lists for all cards in English, Arabic, and Farsi.
Level 0 & 1 Core Vocabulary Illustrations — Generated 14 missing core vocabulary card illustrations in our target impressionistic oil-painting style (with thick brushstrokes, centered layouts, and warm Swedish contexts).
Format Conversion & Integration — Programmatically optimized all PNG image generations to WebP format, stashed them in public assets, and configured the TS unit files to display them directly on study and review cards.
Interactive Choice-Picking in Study Mode — Turned static 'Reveal Answer' study cards into interactive choice selectors. Tapping any option immediately highlights correct (green) and incorrect (red) choices, exposes explanation guides, and triggers the standard study feedback footer.
Bilingual Swedish/L1 Translation Toggles — Integrated globe icons beside the questions across Study Mode, Free Sample tests (both Exam & Study), and premium Exam Mode. Instantly switches questions, options, and explanations between Swedish and the chosen translation (English, Arabic, or Farsi) to verify vocabulary.
Category Study Drilldowns — Paid users can now drill down directly into any of the 12 categories (e.g. Historia, Demokrati, Arbetsmarknad) from the landing page, launching a focused study session containing only questions matching that topic.
Question Bookmarking Loop — Added a golden star icon in Study, Quiz, and Sample views. Bookmarked questions are stashed in local storage and accessible directly from a new 'Sparade frågor' (Saved Questions) quick-launch card on the landing page.
Quick-Launch Weak Areas — Added a 'Svaga områden' card directly on the landing page if the user has wrong answers from their last test, making active recall and drilling targeted questions frictionless.
SFI Learning Path Integration — Added an interactive 'Swedish Citizenship Challenge' milestone callout card at the start of Level 3 (Formal & Civic CEFR A2 section) on the main learning path page, guiding language learners directly to the civic test preparation.
Low-Pressure Study Default — Swapped the default tab in the sample citizenship test view from 'Exam Mode' to 'Study Mode'. Now, when a new user first clicks the sample test, they land directly on the self-paced, list-based study view showing all questions, answers, and explanations, reducing onboarding friction and exam anxiety.
Admin Premium Gate Bypass — Resolved an issue where administrators were still prompted with the 'Unlock Premium' paywall modal on Level 2+ unit pages. Updated the subscription verification utilities and status endpoints to automatically grant full premium access to administrators, aligning backend gates with frontend layout indicators.
Weekly Global Leaderboard API & UI — Established a database-driven global weekly leaderboard page at `/leaderboard` backed by a new `/api/leaderboard` MongoDB projection. Shows the top 50 users ranked by XP, displaying user initials, L1 language flags (🇸🇦, 🇮🇷, 🇬🇧), streaks, and highlighted current user rows.
Unified Grammar Reference Hub — Added a dedicated `/grammar` reference page showing core grammar hints (word order, modal verbs, noun genders) with trilingual translations. Accessible via a new card in the Practice hub.
Referral Tracking Loop — Injected an inline script check into the HTML head in `app/layout.tsx` to automatically parse and stash referral codes (`ref=USER_ID`) in localStorage for viral re-engagement loops.
Progressive Paywall Previews — Replaced hard paywall locks on the main course path with interactive blurred previews. Tapping locked premium units now opens a beautiful preview modal showing a syllabus outline, durations, and key vocabulary to build interest before the paywall.
Citizenship Test Study Mode — Introduced a toggle on the free sample citizenship quiz allowing users to switch between Exam Mode and a relaxed, self-paced Study Mode. This displays the 20 sample questions with answers and explanations visible immediately, helping users learn key concepts without testing pressure.
Profile layout fix — Restructured the profile page columns to keep the learning menu, settings menu, and admin actions inside the left column. This prevents a large vertical empty space on desktop/PC web viewports when the sidebar is tall.
Streak modal fix — Added max-height (90vh) and vertical scrolling (overflow-y-auto) to the streak badge details modal. This prevents the close (X) button from being pushed off-screen and freezing the UI on mobile web viewports.
Guest mode indicator — Added a subtle, persistent banner at the top of the course path page for unauthenticated users, letting them know progress is local-only and providing a direct 'Sign In' link.
Citizenship test quick check — Split the citizenship test start options to offer a 'Quick Test (10 questions)' mode alongside the 'Full Test (all questions)' and 'Challenge Mode'. Slices and shuffles the questions dynamically to prevent user overwhelm.
Practice page fix — Fixed a trailing closing brace syntax error at the end of the practice page code.
Offline background sync — progress no longer lost when the network drops mid-session. Failed POSTs are queued in IndexedDB; an `online` event listener flushes immediately on reconnect. On Chrome/Edge, the Background Sync API lets the service worker retry even if the tab is closed.
BACKLOG cleanup — stale audio note for numbers 11–100 corrected (15 MP3s were already generated in commit 2997679, not TTS fallback).
Numbers 11–100 sub-lesson (L0-1-4) — Interactive practice for elva through hundra: 14 listen_look steps, 3 pick_sound drills, 2 speak steps, and a pattern tip (tjugoett = tjugo + ett). Fulfils the highest-priority SFI content gap.
RoleplayStepView i18n — 'Roleplay' label was hardcoded English for all users. Now reads strings.roleplay (ar: تمثيل الأدوار · fa: نقشآفرینی).
RoleplayStepView design — scene background replaced hardcoded hex gradient (#DCE7D9→#B5CDB0) with bg-[var(--color-moss-soft)], consistent with design system.
PWA manifest — removed invalid maskable SVG icon entry (spec requires raster for maskable); SVG remains as purpose: any.
L2-9 Migrationsverket — New premium unit (A2): checking application status, giving case number, asking about the decision, renewing a residence permit. 5 sub-lessons: context, phrases, dialogue, roleplay, cultural tips. Critical gap for the target audience.
SF2 — Teacher dashboard now shows per-level progress: click 'Show levels' on any student card to see L0–L6 chips (green = complete, blue = in progress).
Audio — 8 MP3s generated/regenerated via ElevenLabs (Sanna Hartfield): tip-j-as-y, var-forsiktig-bluffannonser, forberedelseklass-nyanlanda-barn (new); skolmaten, patientavgift, grundkonto, sfi-kombineras, ja-akuta-jouren (corrected text from v3.88 audit).
Audio pre-caching (P3) — LessonPlayer now prefetches all vocab + dialogue MP3s for a lesson on mount using fetch(force-cache). Eliminates first-play delay on spotty connections.
SF6 — Join link persistence: stashes join code in localStorage on arrival; PendingJoinRedirect global component redirects authenticated users back to /join/[code] if onboarding lost the callbackUrl.
generate-audio.mjs — script updated: 3 new entries, 5 corrected entries aligned with v3.88 content fixes.
L0-1 greetings — Added 'j = y' pronunciation tip (info_card, sky) before the first hej step. Biggest stumbling block for Arabic/Farsi learners.
SF3 — L2-8 (SFI registration) now links to the 'school' phrasebook category from the unit page.
SF5 — Shadowing difficulty filter (Easy / Medium / Hard tabs) confirmed already live — marked done.
Admin leads view — New /admin/leads page with table, l1 breakdown stats, and email search. API at /api/admin/leads.
SF1 — TeacherSection now trilingual (ar/fa/en): Classes, Teaching, Enrolled, Join, Create, Leave, empty state.
L2-5 dialogue — Fixed typo: 'halsinfekion' → 'halsinfektion' in doctor dialogue vocab.
L2-4 tip-skolmat — Corrected false claim: free school meals cover lunch only (not breakfast or afternoon snack) in grundskolan.
L2-5 tip-patientavgift — Updated fee range to 200–400 kr (was 200–350); replaced outdated 'landstinget' with 'regionen'; added 'Barn under 18 år betalar ingenting.'
L2-1 cultural — Added rental scam warning (6th card, berry): fake listings on Blocket/Facebook, foreign account payment requests.
L2-4 cultural — Added förberedelseklass card (new 6th card, sky): preparatory Swedish class for newly arrived children.
L2-7 dialogue — Removed placeholder phone number '020-123 456'; replaced with instruction to check landlord's website.
L2-3 phrases — Added note for Persian speakers to substitute 'persiska' for 'arabiska' in the language phrase.
L2-8 tip-kombination — Fixed incorrect program name 'SFI med lärlingsspår' → 'SFI-kombination'.
L2-2 tip-gratis-konto — Hedged 'vanligtvis gratis' to 'ofta gratis eller billiga' with advice to compare fees.
globals.css — 2 new keyframes (slideUp, flipReveal) + 3 new utility classes (animate-slide-up, animate-flip-in, animate-fade-in-fill with animation-fill-mode: both for delay-safe staggered entrances).
AudioButton.tsx — Added duration-150 and active:scale-95 to base class so the scale press feels responsive rather than snapping.
app/shadowing/page.tsx — Blur reveal now fades opacity 0→1 alongside blur removal (transition-all duration-500 ease-out) for a smooth reveal even in browsers where filter doesn't transition.
BottomNav.tsx — Active indicator pill always rendered; uses opacity/scale-x transition (duration-200) so it fades in/out rather than appearing instantly.
app/review/page.tsx — Translation reveal uses animate-flip-in (translateY + scale entrance) instead of animate-fade-in for a card-reveal feel.
PaywallBanner.tsx — Sheet entrance now slides up from bottom (animate-slide-up) instead of appearing instantly.
LessonPlayer.tsx — Right-panel step container gains animate-fade-in on key={stepIdx} remount, so each step transition cross-fades.
app/citizenship-test/page.tsx — Explanation feedback box gets animate-scale-in so the correct-answer reveal feels earned.
UnitCertificateModal.tsx — Stats row items stagger-fade in (150ms, 230ms, 310ms delay) and vocab chips stagger in from 380ms in 30ms increments using animate-fade-in-fill.
app/path/page.tsx — All 5 contextual nudge cards (l1complete, milestone, streak, review, continue, signin) get animate-fade-in so they pop in when conditions become true.
PickSoundStepView.tsx — Correct answer button gets animate-pop (scale bounce) matching the shake already on wrong answers.
PronunciationResult.tsx — Result banner uses animate-scale-in; word diff badges stagger in 60ms per word using animate-fade-in-fill.
app/page.tsx — Fatima's testimonial ('I booked my dentist appointment without a translator — for the first time ever.') added above the principles section. First-time visitors now see social proof before they read the product description.
app/api/checkout/route.ts — Monthly plan gets trial_period_days: 7. Yearly plan unchanged. isPremiumStatus already handles 'trialing' — no subscription.ts change needed.
components/PaywallBanner.tsx — Monthly CTA changes to 'Start 7-day free trial'; trial note 'X days free · then 29 SEK/month · cancel any time' shown below the button. S.ctaTrial and S.trialNote added (en/ar/fa).
app/api/cron/lapse-email/route.ts — New daily cron (18:00 UTC). Two behaviors in one run: (1) Day-2 bridge — users with last_active_date === yesterday and streak≥1 get a 'come back today' email (skipped Mondays to avoid double-emailing with the weekly cron); (2) 7-day lapse — users with last_active_date 7–9 days ago get a re-engagement email. Both are trilingual (en/ar/fa), RTL-aware, with GDPR unsubscribe link. Tracks cron_result event for admin CronHealthCard.
vercel.json — /api/cron/lapse-email added at schedule '0 18 * * *' (18:00 UTC daily).
app/path/page.tsx — L1 upsell card now triggers at 5+ completed L1 units (was: all 9). Title is adaptive: 'Ready for Level 2?' for mid-journey, 'Level 1 complete!' when all 9 are done. Users who reach the halfway point and are clearly committed now see the upgrade nudge instead of having to exhaust all free content first.
app/path/page.tsx — L1 now unlocks after the first L0 sub-lesson (greetings vocab) instead of requiring all 3. Users who stopped after numbers were seeing a fully locked path page with no explanation — a silent exit point.
app/onboarding/page.tsx — Commitment screen hints rewritten from time-to-completion ('Level 1 in ~5 weeks') to daily-benefit framing ('One new phrase every day', 'Real conversations in weeks', 'Fastest path to fluency'). The old framing filtered out motivated users who needed Swedish urgently.
app/onboarding/page.tsx — Email screen heading changed from 'Keep your streak alive' (incoherent — no streak exists yet) to 'Get weekly Swedish challenges'. Sub copy and CTA rewritten to match the actual moment: pre-streak, post-commitment.
components/PaywallBanner.tsx — S.sub and S.features[0] corrected: '25 scenarios across Levels 2–5' → '31 scenarios across Levels 2–6' (en/ar/fa). The /upgrade page was already correct; the banner — the highest-intent conversion surface — was the only surface still showing stale numbers.
lib/analytics.ts — onboardingStarted(l1) event added (Plausible + MongoDB). Fires on language selection (first deliberate user action) in app/onboarding/page.tsx.
app/api/analytics/route.ts — onboarding_started count added to funnel; D2 multi-day return aggregation added (session_ids with lesson_started on 2+ distinct dates in last 30d). Both included in funnel response.
components/AnalyticsDashboard.tsx — FunnelCard rebuilt as 6-step activation + conversion funnel (started onboarding → completed onboarding → started lesson → completed lesson → hit paywall → converted). Three rate KPIs added: onboarding drop %, paywall→paid %, and D2 return % with absolute counts.
lib/db-init.ts — A5: compound index {event:1, ts:-1} on events collection; A6: index on progress.data.last_active_date. Eliminates full-collection scans on every /admin load and every weekly-email cron run.
app/api/cron/daily-push + weekly-email — A7: each cron writes a cron_result event doc on completion ({cron, sent, gone/errors, ts}). Admin dashboard CronHealthCard reads most-recent run per cron and shows last-fired time, sent count, and errors — red dot + 'Never run — check CRON_SECRET' when absent.
app/api/webhooks/stripe/route.ts — A8: invoice.payment_failed now falls back to a transactional Resend email when the user has no push subscription or it is gone (expired). Payment failure is unambiguously transactional — no opt-out required.
lib/stripe-helpers.ts — A9: calculatePeriodEnd() extracted from the webhook handler as a pure, testable function. __tests__/stripe-webhook.test.ts adds 13 tests covering cancel_at, monthly/yearly advance, missing fields, future anchor, and the 36-iteration safety cap. Total test count: 53 → 66.
components/AnalyticsDashboard.tsx — KPI card label changed from 'Sessions' to 'Browser sessions'; sub text updated to '30d · not unique users'. The sessions30 value counts distinct session_id values (per localStorage per browser), not unique users — one user on two devices or after clearing storage inflates the count.
app/api/analytics/route.ts — expanded inline comment to document the browser-session limitation.
app/api/cron/daily-push/route.ts — getPushMessages(l1, streak, due) helper added. Returns localised title/body/url for all 3 push variants (streak-at-risk + due, streak-at-risk only, review-only) in English, Arabic, and Farsi. l1 read from progress.l1_choice ?? 'en'. Arabic/Farsi users now receive streak warnings and review reminders in their own language.
CLAUDE.md — new 'Stripe webhook events' subsection added to Development workflow. Lists all 6 events the handler processes, their confirmed status, and flags invoice.upcoming as never confirmed in the Stripe Dashboard (required since v3.1, but the v3.15 confirmation only covered invoice.paid and invoice.payment_failed). No code change — handler is correct. Action required: verify invoice.upcoming is subscribed in Stripe Dashboard → Developers → Webhooks → [endpoint].
types/lesson.ts — email_unsub?: boolean added to UserProgress.
lib/sanitize-progress.ts — email_unsub sanitized (only true passes; anything else becomes undefined).
app/api/unsub/route.ts — new GET endpoint. Verifies HMAC-SHA256 token (base64url(email).hex(32)), sets data.email_unsub = true in MongoDB, returns a branded HTML confirmation page.
app/api/cron/weekly-email/route.ts — makeUnsubToken() helper added; MongoDB query now filters data.email_unsub: { $ne: true }; email footer includes signed Unsubscribe link alongside Settings link.
app/profile/page.tsx — Notifications settings row no longer says 'Soon'. useEffect reads Notification.permission synchronously on mount and sets notifStatus: 'enabled' (green), 'blocked' (berry), 'off' (faint), or 'unsupported' (faint). notificationsSoon string replaced with notificationsEnabled / notificationsOff / notificationsBlocked / notificationsNA, all trilingual.
app/path/page.tsx — LEVEL_MILESTONE constant added with title/sub strings for L2–L5 completions in all 3 languages. levelMilestone useMemo finds the highest complete premium level whose next level hasn't been started. Milestone card (moss, trophy icon, dismiss ×, Link to first next-level unit) inserts into the 2-card contextual nudge system between l1complete and streak. Dismissed state stored in ss_levelcomplete_dismissed localStorage key (comma-separated level numbers). L2 → 'Formal & civic Swedish is next' → /unit/L3-1; L3 → 'Everyday life…'; L4 → 'Culture & society…'; L5 → 'Fluency & independence…'.
components/UnitCertificateModal.tsx — listening nudge condition widened from relatedEpisode != null to relatedEpisode != null || unit.level >= 2. Units mapped in UNIT_EPISODE_MAP still show the specific episode title; all other L2–L6 units (27 previously showing nothing) now show a generic 'Browse all listening episodes' subtitle pointing to /listening. listenBrowse string added to S in all 3 languages.
app/progress/page.tsx — subtitle and progress bar label now branch on isPremium. Premium users see 'Your Swedish journey (A1–B2).' and 'Course progress'; free users see the original 'Your A1 Swedish journey.' and 'A1 course complete'. Trilingual across en/ar/fa.
app/profile/page.tsx — streak motivation card (shown when streak >= 2) now shows a shield count line for premium subscribers: IconShieldCheck icon + 'N shields remaining' in amber (or 'No shields left' in faint when 0). Previously, streak shields were promised on the upgrade success page but had no visible representation anywhere in the app.
app/upgrade/success/page.tsx — S.sub updated to 'Levels 2 through 6 are now unlocked.' S.confirms[0] updated to '31 scenarios across Levels 2–6 unlocked' (was stale at L2–5 / 25 scenarios since L6 was added in v3.40).
app/profile/page.tsx — scenariosChip updated to '31 scenarios' (was '25 scenarios'). Shown as the right-label on the Upgrade to Premium row for free authenticated users.
G3 — app/page.tsx: 'Browse course' CTA now routes to /onboarding for first-time visitors (same condition as primary CTA), so Arabic/Farsi speakers aren't dropped into an English /path.
G4 — app/onboarding/page.tsx: PreviewScreen now shows L0 'Survival Kit' as a pinned 'Start here' card above a 'Then — Level 1' divider and the 9 L1 units. Heading updated to 'What's coming — after your first 7 words'. Removes the bait-and-switch of showing L1 scenarios when the first lesson is actually L0.
G5 — app/path/page.tsx: l1CompleteSub updated from 'Unlock 8 Level 2 scenarios' to 'Unlock 31 premium scenarios across Levels 2–6'. Consistent with PaywallBanner and upgrade page.
G6 — app/page.tsx: principles section now appears before citizenship test section. Trust-building (how the app works) precedes feature promotion.
G7 — app/onboarding/page.tsx: XP badge removed from CommitmentScreen options. XP is unexplained at this point; the completion-time hint ('Level 1 in ~2 weeks') carries the weight. xpLabel field and sub copy cleaned up.
G8 — app/path/page.tsx: saveSub rewritten from 'Closing your browser means losing your progress' (threat) to 'Your progress is saved in this browser — sign in free to sync across devices' (reassurance).
components/PaywallBanner.tsx — unauthenticated CTA promoted from a plain text link to a full-width moss button (matches the authenticated upgrade button in size and weight). guestNote rewritten from "Your subscription will be securely linked to the email you use at checkout" (implied guest checkout is possible) to "You'll need a free account before upgrading — no credit card needed until then" (accurate).
app/onboarding/page.tsx — dir={isRtl ? "rtl" : "ltr"} added to PreviewScreen wrapper. CommitmentScreen and EmailScreen already had this; PreviewScreen was the only onboarding screen missing it. Arabic/Farsi users on screen 2 now see unit titles and descriptions with correct RTL text alignment.
content/units/L0-1-survival-kit.ts — fa fields for all 10 number entries updated from bare word ("یک") to word · Eastern numeral ("یک · ۱"), mirroring the en pattern ("one · 1"). Farsi-script numerals are more native for Iranian users and the · ۹ on nio also visually disambiguates it from nej's «نه».
components/lesson-steps/SeeWordStepView.tsx — dir={isRtl(l1) ? "rtl" : "ltr"} added to the outer container div. The Swedish letter grid already had dir="ltr"; without this fix, RTL instruction text and the optional tip box could inherit conflicting direction from the page root. Letter grid is unaffected (its own dir="ltr" takes precedence).
app/citizenship-test/page.tsx — masteryMsg rewritten as l1==="en" ? English : sv(key, l1) across all three branches. Eliminates two-style maintenance: Arabic and Farsi now both read from UI_I18N. Also fixes a latent Arabic bug where the count branch used sv("masteryLabel") (the zero-state key) instead of sv("masteryProgress") (the per-fact label).
L0-1-survival-kit.ts — fa: "بله / بلی" changed to fa: "بله". The slash notation rendered as a typographic artifact on the vocab card. Afghan Dari variant «بلی» now surfaced via a note field (same amber info box pattern used for nio/nej disambiguation in v3.55).
lib/i18n.ts — Farsi label_staff changed from «کارمند» (office worker) to «طرف مقابل» (the other party). «کارمند» sounds wrong above a pharmacist or nurse bubble; «طرف مقابل» is register-neutral and fits shop workers, doctors, drivers, and officials equally.
L0-1-survival-kit.ts — dialogue line fa: "ممنون!" → "ممنونم!" (adds first-person suffix). Vocab card was corrected to «ممنونم» in v3.19; dialogue was missed. Learners no longer see two different Farsi translations for «Tack!» within the same unit.
L0-1-survival-kit.ts conversation sub-lesson intro description — added parenthetical in all 4 languages (sv/en/ar/fa): 'Ahmed is an example name — say your own name.' Afghan/Dari speakers no longer see an identity marker that signals the app was built only for Arabic speakers.
41 unit files (L0–L6) — 772 fa_phonetic lines stripped of Arabic diacritics (fatha/damma/kasra/shadda/sukun/tanwin). e.g. «فُرلوت» → «فورلوت», «اورسِکتا» → «اورسکتا».
ar_phonetic fields untouched — diacritics are standard pedagogical practice in Arabic but alien to everyday Farsi/Dari readers.
lib/i18n.ts — Farsi label_you changed from «شما» (formal/plural) to «تو» (informal/direct). Matches SESSION_S.speakerYou which already used «تو». Learners are now taught the correct everyday register for spoken Swedish contexts.
content/units/L0-1-survival-kit.ts — new vocab entry 'tip-forlat-ursakta' (index 7) added to greetings vocab array. Trilingual (en/ar/fa).
content/units/L0-1-survival-kit.ts — info_card step (vocab_id: 7, accent: amber) inserted between the förlåt and ursäkta listen_look steps. Farsi users see: «در فارسی هر دو ببخشید میشوند — اما سوئدیها فرق قائلاند». Arabic and English versions also provided.
lib/i18n.ts — 3 new UiStrings keys added (all 3 languages): read_word_label, letters_count (n: number) => string, play_letter_by_letter.
components/lesson-steps/SeeWordStepView.tsx — 'Read the word' chip label, '{n} letters ·' count, and 'Play letter by letter' button now use strings.* — Farsi and Arabic users no longer see English in this step.
types/lesson.ts — VocabWord gains optional note?: Partial<Record<L1Lang, string>> field for language-specific clarifications on ambiguous translations.
content/units/L0-1-survival-kit.ts — nio (9) entry gets a Farsi note explaining that «نه» is spelled identically to nej (no) but pronounced differently — prevents learners from confusing them in pick_sound exercises.
components/lesson-steps/ListenLookStepView.tsx — renders word.note[l1] as an amber info box below the vocab card when present; zero impact on words without a note.
Farsi persona review conducted: identified 2 critical content bugs, 5 high-impact UX gaps, 4 medium quality issues across L0-1 survival kit, SeeWordStepView, lib/i18n.ts, and onboarding.
Critical: nio (9) and nej (no) both map to «نه» in Farsi — pick_sound exercises actively mislead learners. Needs a note field on the nio vocab entry.
Critical: SeeWordStepView has 3 hardcoded English strings ('Read the word' label, 'N letters', 'Play letter by letter') visible to all Farsi users in every lesson.
High: label_you is «شما» (formal/plural) in lib/i18n.ts but «تو» (informal) in SESSION_S — wrong register mismatch; lesson player teaches wrong formality level.
High: fa_phonetic fields use Arabic vowel diacritics (damma ُ, kasra ِ) that Persian readers don't use or recognize in everyday writing — should be stripped.
components/Icon.tsx — 3 missing icons registered (were silently rendering as IconCircleDot): IconStar (DoneScreen stars), IconRepeat (Review card), IconUser (shadowing/roleplay speaker avatar).
app/practice/page.tsx — All 4 session modes (Drills, Shadowing, Review, Roleplay) now fully trilingual. Added SESSION_S strings object with 30+ keys in en/ar/fa. DoneScreen titles, stat labels, and all button/hint text translated.
app/practice/page.tsx — SessionShell root div gets dir=rtl for Arabic/Farsi — entire session UI flips correctly. Arrow icons get arrow-flip class for RTL navigation.
app/practice/page.tsx — Empty-state heading on Practice hub changed from 'Coming soon' to 'Start a lesson to unlock' (trilingual) — previous text read as a broken/incomplete page.
app/path/page.tsx — 'X of Y scenarios complete' completion counter now translates into Arabic and Farsi (was hardcoded English shown to all users).
app/profile/page.tsx — Removed status === 'loading' early return. Page now renders immediately with unauthenticated state while NextAuth resolves, instead of blocking indefinitely. Fixes blank spinner in local dev when NEXTAUTH_SECRET / NEXTAUTH_URL / MONGODB_URI are absent from .env.local.
app/onboarding/page.tsx — Back button added on steps 2–5 (RTL-aware: IconArrowRight for Arabic/Farsi). Screen-to-screen fade transition via key={screen} + animate-fade-in. Progress bar h-1 → h-1.5.
app/onboarding/page.tsx — cursor-pointer added to all 6 button elements (was missing everywhere). text-left → text-start on ReadyScreen unit card for RTL correctness.
app/path/page.tsx — 'Complete' badge now trilingual (was hardcoded English). cursor-pointer on locked-unit button. Nudge card spacing mb-1 → mb-2. Sign-in dismiss button p-1 → p-2 (44px touch target). End-of-list IconFlame → IconMap.
app/profile/page.tsx — StatCards get shadow-sm for elevation against page background. MenuRow onClick buttons get cursor-pointer. 🇸🇪 emoji replaced with IconId (no-emoji-icons rule). Citizenship card moved before InviteCard. Streak motivation hidden until streak ≥ 2.
components/Icon.tsx — 5 missing icons registered (were silently rendering as IconCircleDot): IconCoffee, IconUserPlus, IconCreditCard, IconShieldCheck, IconClockHour4.
Installed UI UX Pro Max Claude Code skill (uipro-cli v2.2.3). Adds 67 UI styles, 96 color palettes, 57 font pairings, and 99 UX guidelines to Claude's design reasoning.
Skill auto-activates on UI/UX requests (design, review, improve). Searchable via python3 .claude/skills/ui-ux-pro-max/scripts/search.py.
app/onboarding/page.tsx — Language picker footer note now trilingual (en/ar/fa); was English-only even after user selected Arabic.
app/onboarding/page.tsx — Arabic button labels ('العربية', 'الشرح بالعربية') now have explicit var(--font-arabic); previously could render in Latin fallback font.
app/onboarding/page.tsx — Language selector buttons: text-left → text-start (Tailwind logical property) so Arabic/Farsi text aligns to the correct edge in RTL.
app/onboarding/page.tsx — Email submission now catches network errors and shows a trilingual berry-red message; previously silent failure always advanced to 'done' state.
components/BottomNav.tsx — Nav labels bumped from text-[10px] to text-[11px]; Arabic x-height is shorter than Latin, making 10px labels nearly unreadable on mid-range Android screens.
public/images/vocab/ — deklaration.webp (L3-3 tax return), skatteaterb.webp (L3-3 skatteåterbäring), receptfornyelse.webp (L3-4 prescription renewal), 1177.webp (L3-4 healthcare helpline) added and wired via image field in their respective vocab entries.
.github/workflows/ci.yml — removed the npm run lint step; next lint no longer exists as a CLI subcommand in Next.js 16 (was treating 'lint' as a directory argument and failing).
package.json — lint script replaced with an informational echo until ESLint is set up separately; typecheck + build steps in CI are unaffected.
app/phrasebook/page.tsx — PlayButton now uses /audio/sv/phrasebook/<id>.mp3 (ElevenLabs) instead of browser TTS fallback. TTS remains as safety net if file fails to load.
public/audio/sv/phrasebook/ — 148 MP3s generated via ElevenLabs (Sanna Hartfield Swedish Conversational voice). Covers all 12 categories: pharmacy, doctor, housing, school, supermarket, bank, transport, job interview, numbers, days & months, family, colors.
scripts/generate-phrasebook-audio.mjs — new script for future phrasebook audio regeneration.
public/images/vocab/ — bifoga.webp (L3-1 formal email), myndighet.webp (L3-1 formal email), beslut.webp (L3-2 official letters), sista-datum.webp (L3-2 official letters) added and wired via image field in their respective vocab entries.
20 components and pages migrated from inline ternary strings (l1 === 'ar' ? ... : l1 === 'fa' ? ... : ...) to getL1Text() object pattern — groundwork for adding Somali and future languages without touching every file twice.
public/images/vocab/ — studievagledare.webp (L2-8 SFI), fungerar-inte.webp (L2-7 felanmälan), arende.webp (L3-1 formal email) added and wired via image field in their respective vocab entries.
content/citizenship-questions.ts — 22 new questions added across all 9 categories: Vikingatiden, Swedish emigration to America, Miljonprogrammet (historia); the 4 fundamental laws, NATO membership (2024), KU (demokrati); personnummer, folkbokföring, gymnasieskolan (samhälle); Astrid Lindgren, kräftskivan, IKEA (kultur); midnattssolen, Skandinaviska halvön (geografi); presstödet, källkritik (medier); asylrätt, demonstrationsrätt (rättigheter); den svenska modellen, semesterlagen (arbetsmarknad); religious demographics, SST (religion).
content/citizenship-questions.ts — CHEAT_SHEET expanded with 2–3 new facts per category (historia +3, demokrati +3, samhälle +3, kultur +3, geografi +2, medier +2, rättigheter +2, arbetsmarknad +2, religion +2). Total cheat sheet facts grew from 48 to 70.
content/citizenship-i18n.ts — Full Arabic and Farsi translations for all 22 new questions (QUESTION_I18N) and all new cheat sheet facts (CHEAT_I18N).
Total question count: 90 → 112. Each category now has 12–13 questions for more varied daily challenge draws.
scripts/generate-audio.mjs — 137 new phrase entries added for all 6 new units: L5-3 (arbetskultur), L5-4 (bostadsköen), L5-5 (medborgarskap), L6-1 (jobbintervjun), L6-2 (lokala val), L6-3 (utvecklingssamtal).
public/audio/sv/ — 133 new MP3s generated (ElevenLabs, Sanna Hartfield - Swedish Conversational voice). 4 files skipped as already present (betyg.mp3 etc.).
Coverage: all vocab words, phrase sentences, dialogue lines, and cultural tip narrations for each unit. Enables audio playback and TTS fallback across all B1–B2 content.
components/FeedbackWidget.tsx: usePathname() added — widget now returns null on /lesson/*, /onboarding/*, /admin*, /auth/*, /join/*, /teacher/*, /login. Previously the floating button appeared over every page including lesson cards and the onboarding flow.
app/api/feedback/route.ts: insertOne wrapped in try-catch. A MongoDB write failure now returns JSON 503 instead of crashing the handler → HTML 500 → parse error in the widget → 'Something went wrong' shown incorrectly.
content/units/L5-3-arbetskultur.ts — Swedish Work Culture (B1): fika, lagom, flat hierarchy, flex hours. 5 sub-lessons, full roleplay.
content/units/L5-4-bostadskon.ts — The Housing Queue (B1): bostadskö, hyresrätt vs bostadsrätt, accepting an apartment offer, deposit rules, notice period.
content/units/L5-5-medborgarskap.ts — Citizenship (B1): requirements, dual citizenship, no language test, Migrationsverket process, ceremony.
content/units/L6-1-jobbintervjun.ts — Job Interview (B2): CV, personligt brev, interview questions, salary negotiation, thank-you email.
content/units/L6-2-lokala-val.ts — Local Elections (B2): who can vote (EU citizens + PUT), kommunfullmäktige vs riksdag, ballot procedure, Valkompassen.
content/units/L6-3-utvecklingssamtal.ts — Parent-Teacher Meeting (B2): school system, frånvaroanmälan, betyg from year 6, school breaks, kurator.
types/lesson.ts — cefr union extended to include 'B2'. app/path/page.tsx — L6 level divider 'Fluency & independence' added between L5 and L6 units.
app/upgrade/page.tsx — premium feature count updated from 25 to 31 scenarios, Levels 2–6, A2–B2.
lib/db.ts — getDb() now returns null on connection errors (wraps connect() in try-catch, resets client). Previously an M0 connection failure caused an unhandled throw → HTML 500 → FeedbackWidget showed 'Something went wrong'.
components/AnalyticsDashboard.tsx + app/api/analytics/route.ts: SubscriberCard now shows estimated MRR (monthly × 29 SEK + yearly × 199/12) and churned-this-month count alongside active subscriber count.
app/profile/page.tsx: free authenticated users now see an 'Upgrade to Premium' row in Settings (was no upgrade CTA on the profile page). Monthly subscribers see a 'Switch to yearly — Save 43%' row below their subscription row.
app/profile/page.tsx: SupportCard (Ko-fi / tip jar) moved from free users to premium-only. Was competing with subscription conversion for the most cost-sensitive segment.
components/PaywallBanner.tsx: testimonial block added (moss-tinted, above plan toggle) — realistic quote from 'Fatima, Gothenburg', trilingual en/ar/fa.
app/upgrade/page.tsx: headline changed from 'SuperSvenska Premium' to 'From struggling to speaking' — outcome-oriented. 'No credit card needed' note added under the Free column header.
app/onboarding/page.tsx: preview screen now shows unit descriptions instead of unit codes (L1-1 etc.) — communicates value, not developer labels. Email screen heading changed to 'Keep your streak alive' with benefit-framing sub ('weekly Swedish challenge + magic link, no password').
content/sponsor.ts: added optional startDate/endDate fields (YYYY-MM-DD). Banner renders only within the campaign window — enables time-limited sponsorships without a deploy.
components/SponsorBanner.tsx: date range gate applied before render. lib/analytics.ts: sponsorImpression and sponsorClicked now also write to MongoDB (were Plausible-only).
app/practice/page.tsx: second SponsorBanner placement added between Drills & Speaking cards and the Currently drilling info block.
components/AnalyticsDashboard.tsx + app/api/analytics/route.ts: SponsorStatsCard added — shows 30-day impressions, clicks, and CTR. When no sponsor is active, shows an inline prompt to activate.
app/api/admin/facebook-post/route.ts: new admin-gated POST endpoint. Text-only posts go to /{page}/feed; photo posts (when imageUrl provided) go to /{page}/photos with caption — Facebook fetches the image from the URL.
components/FacebookPostWidget.tsx: admin panel widget. Auto-selects today's post type from the day-of-week rotation (Phrase of the Day Mon/Wed/Fri, Did you know Tue, Feature highlight Thu, Testimonial Sat/Sun). Pre-fills trilingual EN/AR/FA template. Optional image URL field with live preview and validity check. Publish button posts directly to the SuperSvenska Facebook page via Graph API.
app/admin/page.tsx: FacebookPostWidget rendered between Quick links and Analytics sections.
profile/page.tsx: past_due subscribers can now reach the billing portal. Previously isPremium gated the portal row — payment failures hid the portal from users who needed it most.
profile/page.tsx: subscription fetch now runs for all authenticated users, not just premium. past_due row uses berry icon + 'Update payment' label.
upgrade/page.tsx: free feature count corrected to 10 scenarios (L0 + Level 1). Premium column updated to '25 scenarios across Levels 2–5 (A2–B1)' with streak shields added.
manifest.ts: start_url changed from '/' to '/path' — the old value triggered a redirect that fails offline, showing a browser error on first offline open.
public/offline.html: branded trilingual (en/ar/fa) offline page shown instead of Chrome/Safari's generic error when navigation fails without network.
sw.js: precaches offline.html on install so it's available from the very first offline visit; navigation fetch handler now serves it as fallback.
InstallPrompt: moved from app/path to app/layout — now appears on any hub page after first lesson, not just the Learn tab. Fixed-positioned above the bottom nav.
InstallPrompt: iOS share instructions now include the ⬆ icon so users can identify the Share button without guessing.
class/join: inline XFF extraction replaced with clientIp() — rate limit was bypassable via spoofed X-Forwarded-For header.
onboarding-email: 24h per-email cooldown added — prevents IP-rotating attackers from spamming a victim address.
admin/feedback: type filter now whitelisted against valid types (bug/feature/content/other) — any string was previously accepted.
admin/feedback + admin/subscribers: parseInt() NaN guard added — invalid query params no longer produce undefined skip/limit.
push/subscribe: rate limiting added (10/hr per email) — endpoint was auth-gated but otherwise unbounded.
daily-push cron: progress docs now batch-loaded in one query instead of N+1 findOne calls — O(2) regardless of subscriber count.
maxDuration = 60 added to both cron routes — prevents silent timeout on Vercel Hobby (10s default).
Sentry beforeSend filter: drops AbortError and network noise so the 5k/month free-tier quota isn't exhausted by benign browser events.
CI: npm run lint step added — catches ESLint issues not caught by TypeScript.
Backup: SHA-256 checksum verification added for mongodump binary download.
Env var table: CRON_SECRET, VAPID_SUBJECT, SENTRY_AUTH_TOKEN documented — all were silently required but missing from the table.
clientIp() now reads x-real-ip first (Vercel edge-set, unspoofable) instead of X-Forwarded-For[0] — prevents IP rate-limit bypass on waitlist, class-join, and onboarding-email routes.
subscriptions.email unique index added to db-init.ts — isPremiumUser() was doing a full collection scan on every L2+ lesson access.
used_magic_tokens TTL index moved from magic-verify POST handler to server-startup ensureIndexes() — removes unnecessary createIndex round-trip on every login.
centrum.webp → L1-3 (åka buss) vocab card.
ansokan.webp → L1-5 (Skatteverket) and L2-3 (Arbetsförmedlingen) ansökan vocab cards.
identifikation.webp → L2-2 (öppna bankkonto) vocab card.
Stripe webhook: deduplication entry now removed on processing error so Stripe can retry — previously a failed subscriptions.retrieve() call would silently drop the subscription update.
GET /api/class/my: added .limit(100) to class memberships query — was unbounded.
DELETE /api/class/my: class code now validated against ^[A-Z0-9]{6}$ before hitting MongoDB.
POST /api/onboarding-email: email regex tightened to match the stricter pattern used by all other routes.
L1-1 and other previously completed units were showing as locked (lock icon, no navigation) for users who finished them before the Survival Kit (L0) was added.
Root cause: isComplete was computed after isUnlocked in unitStates, so it couldn't be used as an unlock condition.
Fix: compute isComplete first, then use it as the first unlock condition — a completed unit is always accessible for replay.
Fixed --color-moss-dark circular CSS var (was self-referencing → hover states on all primary buttons were broken).
Added --color-amber-dark and --color-amber-text CSS vars; replaced 14 hardcoded hex values across 9 files.
Citizenship test page: RTL dir attribute added to all 3 return paths — layout now flips for Arabic/Farsi users.
Citizenship test wrong-answer feedback now uses design system berry tokens instead of hardcoded pink hex.
BottomNav bg-white → bg-[var(--color-bg)] for theme consistency.
Changelog back arrow now has arrow-flip for RTL navigation.
All Level 1 units now unlock together once the Survival Kit (L0) is complete — pick the scenario you need most urgently.
New email capture screen in onboarding: optional, sends a 'continue learning' link via email, trilingual.
Day-1 streak card on path: 'Come back tomorrow to keep it going' after completing the first lesson.
Commitment screen now shows completion estimates: '10 min/day → Level 1 in ~2 weeks'.
Paywall headline updated: 'Unlock Level 2 & 3' → 'Unlock Premium' with accurate sub-text covering all 25 scenarios.
Guests who complete ≥1 lesson now see a one-time sign-in nudge on the path page: 'Save your progress — sign in free'.
Feedback widget now open to unauthenticated users — guests see the full form with an 'anonymous' note.
Admin feedback inbox defaults to showing only unresolved items; toggle to see resolved ones.
Resolve checkmark on each feedback item — optimistic update, persisted to DB.
Anonymous submissions rate-limited by IP (3/hr) to prevent spam; authenticated still 10/hr.
Analytics dashboard now shows a conversion funnel card: Onboarded → Started lesson → Hit paywall → Converted.
Paywall → paid conversion rate shown inline (green ≥5%, amber >0%, grey if no data).
Language distribution bar chart shows Arabic / Farsi / English split at onboarding — key for content prioritisation.
Invite-shared count surfaced in the funnel card (was tracked but never displayed).
Phrasebook: 'Numbers' category — 11 through 100 (elva, tolv…hundra), all irregular teens, round tens.
Phrasebook: 'Days & Months' category — all 7 weekdays, 12 months, 4 seasons, idag/imorgon/igår.
Phrasebook: 'Family' category — mamma/pappa through all grandparent and aunt/uncle terms, with en/ett notes.
Phrasebook: 'Colors' category — 11 color adjectives (en/ett forms shown), Swedish flag phrase included.
Classes section: 'Join a class' is now the primary button — correct priority for SFI students receiving a teacher code.
Upgrade success page sub-text updated: 'Level 2 & 3 are now unlocked' → 'Levels 2 through 5 are now unlocked'.
Upgrade success confirms[0] updated: '25 scenarios across Levels 2–5' instead of 'Level 2 & 3 scenarios'.
'Start Level 2' CTA now links directly to /unit/L2-1 instead of /path — subscribers land in their first premium unit, not at the top of the path page.
L2 level divider now shows a small hint ('Complete all Level 1 units to unlock this level') when a premium subscriber hasn't finished L1 yet — prevents confusion about why L2 units are still locked after upgrading.
Onboarding now routes new users to L0-1 (Survival Kit) instead of L1-1 — fixes post-v3.17 regression where the ready screen previewed L0 but the CTA launched L1.
Onboarding ready screen sub-text updated to describe the Survival Kit (7 essential words) rather than the old 'picking up a package at the post office' copy.
Preview screen count corrected: 'first 8 real situations' → 'first 9 real situations' (L1 has 9 units).
PaywallBanner features list updated: was '8 L2 + 4 L3 scenarios' (12 implied), now correctly surfaces all 25 premium scenarios across Levels 2–5.
PaywallBanner sign-in link now uses current pathname as callbackUrl instead of hardcoded '/path' — users land back at the unit/page where they hit the paywall after logging in.
L0-1: 'tack' Farsi translation ممنون → ممنونم (first-person suffix; more natural standalone).
L0-1: 'förlåt' Farsi translation متأسفم → ببخشید (heavy formal 'I am sorry' → natural everyday apology, matching how Persian speakers use the word).
L0-1: 'ja' Farsi translation بله → بله / بلی to cover both Iranian Farsi (بله) and Afghan Dari (بلی).
Onboarding screen 1: Arabic and Farsi subtitle lines lightened from ink-faint to ink-soft — more legible in bright outdoor light.
kvitto.webp wired to receipt vocab in L1-2 (Handla ICA).
påse.webp wired to bag vocab in L1-2 (Handla ICA).
linje.webp wired to bus line vocab in L1-3 (Åka buss).
L0-1 Survival Kit — a zero-to-one unit before Lesson 1. Three sub-lessons: 7 essential words (hej, tack, förlåt, ursäkta, ja, nej), numbers 1–10, and the first name-and-country conversation.
23 new MP3s generated for all L0 phrases and dialogue lines.
Path page: L0 now gates L1-1. A 'Level 1 · A1 · Free' divider appears between L0 and L1. Header subtitle updates dynamically for each level.
Lesson routes: generateStaticParams now pre-renders L0 + L1 routes (level ≤ 1).
L4-5 Reklamation — returning defective goods, consumer rights (konsumentköplagen 3-year rule, Hallå Konsument, ångerrätt).
L4-6 BankID & personnummer — applying at Skatteverket, folkbokföring documents, activating BankID.
L4-7 Bilprovning — booking mandatory car inspection, reading the plate-digit period system, pass/fail rules.
L5-1 Försäkringskassan — reporting VAB, karensdagen explained, SGI registration, same-day reporting.
L5-2 Midsommar — first midsommar celebration, ring dances, traditional food, cultural tips for newcomers.
106 new MP3s generated (ElevenLabs, Sanna Hartfield voice). cefr type extended to B1 for L5 tier.
Stripe webhooks are now deduplicated — retries from Stripe no longer send duplicate push notifications.
Subscription renewal dates now stay accurate after every billing cycle via a new invoice.paid handler.
Users receive a push notification when a payment fails, prompting them to update their card.
12 new questions added — medier, rättigheter, arbetsmarknad and religion now have 10 questions each (up from 7).
New topics: ansvarig utgivare, MPRT, public service mission, oskuldspresumtionen, förvaltningslagen, DO, provanställning, A-kassan, 40h arbetstid, nationella minoriteter, religiös klädsel, sekulär stat.
Cheat sheet expanded with 12 new facts across the same four categories — full Arabic and Farsi translations included.
Added /api/health endpoint — enables uptime monitoring tools to check both server and database health.
Weekly automated database backup now runs every Sunday via GitHub Actions (90-day artifact retention).
Service worker now caches JavaScript bundles and audio files on first load — repeat visits and lesson audio load faster.
Sentry error stack traces now show readable source code instead of minified output (requires SENTRY_AUTH_TOKEN in Vercel).
Lesson player now uses the full screen width on mobile — the conversation sidebar is hidden on small screens so the active step card is no longer squished.
Three new lesson intro illustrations added: doctor appointment (L1-4, L2-5), Skatteverket (L1-5), and fika/café (L1-6).
SuperSvenska logo now appears correctly in Google search results.
Citizenship test paywall now shows the live question count dynamically — always accurate as more questions are added.
Added rate limiting to waitlist signup, class join, and teacher class creation endpoints.
Events API now validates session_id format before storing — prevents analytics poisoning.
Removed magic link URL from development console log — tokens no longer appear in any log output.
Push subscribe endpoint now validates keys object structure before storing to DB.
Package pickup, supermarket, and bus illustrations now show on lesson intro screens.
Added RTL direction to lesson player, unit detail, flashcard review, shadowing, progress, and listening pages — Arabic/Farsi users now see correct layout on every screen.
Added --color-moss-dark CSS variable and replaced 49 hardcoded #1F3D33 hex values across the app and all components.
Citizenship test 'wrong answer' backgrounds now use the design system berry-soft tint instead of Tailwind red-50.
L1Lang type imported from the shared type definition in citizenship test — removes a local duplicate.
Added aria-label to the icon-only play button on the shadowing page.
AudioButton error state now uses design system berry colors instead of raw Tailwind red classes.
L1-1: Staff dialogue now reads as natural spoken Swedish — 'Här är ditt paket!' replaces the awkward combined phrase.
L1-2: Self-checkout help phrase updated to 'Kan någon hjälpa mig? Jag förstår inte självkassan.' ICA hours and Systembolaget Sunday info now include accurate caveats.
L1-3: Fixed phonetic transcription for 'vid' — correctly represents the short /ɪ/ vowel for Arabic and Farsi speakers.
L1-5: folkbokföring tip now explains the word means 'registrera din adress hos Skatteverket'.
L1-7: 'Trevligt att träffas!' no longer repeated between phrases and dialogue sections.
L1-8: Standardised on 'tvättpass' throughout — 'tvättid' removed to avoid confusion.
When you return after missing a day, a new interstitial appears before your streak resets — showing your streak count and a clear path to protect it with Premium.
Streak shields are now front-and-centre at the moment they matter most, not just a bullet point in the paywall.
Expanded from 43 to 90 questions across all 9 categories — deeper coverage of history, law, society, culture, and geography.
New Daily Challenge mode: 10 randomised questions every day with a streak counter. Come back daily to build your streak 🔥
All 47 new questions fully translated to Arabic and Farsi.
181 new MP3 files generated via ElevenLabs for L3-5 through L4-4 — every vocab word, phrase, dialogue line, and cultural tip now has native Swedish audio.
Voice: Sanna Hartfield (Swedish Narration). Audio generation script updated to include all new units.
Level 3 grows from 4 to 8 units: parental leave (Försäkringskassan), unemployment benefits (A-kassa), rental contract & tenant rights, and getting a Swedish driver's license.
New Level 4 — Everyday Life: dental appointment, filing a police report, library card & services, and the first maternal health centre (MVC) visit.
All 8 new units are fully trilingual (English, Arabic, Farsi) with complete dialogues, roleplay, and cultural tips.
When you finish all 9 Level 1 scenarios and haven't upgraded yet, a green card now appears at the top of your path — the highest-intent moment in the funnel.
Tapping it opens the Premium paywall directly. You can dismiss it permanently with the × button.
Three new analytics events — Onboarding Complete, Paywall Hit, and Paywall Converted — are now tracked in both Plausible and MongoDB, making the full free-to-premium funnel visible for the first time.
Paywall hits are tagged by source (path page vs. unit page) so you can see which context drives more upgrades.
The admin analytics dashboard now shows a subscriber card at the top — active count with monthly/yearly split, past-due alert, and canceled count, all without leaving the app.
New feedback inbox at /admin/feedback: filter by type (bug, feature, content, other), see sender, page context, and relative timestamps.
New subscriber list at /admin/subscribers: color-coded status badges, plan, renewal/cancellation dates, and a clickable mailto link for quick support replies.
Premium subscribers who have push notifications enabled will now receive a reminder 3 days before their subscription renews — giving time to review or manage the subscription before being charged.
The upgrade success page now lists what you get — including streak shields — so subscribers know from day one that a single missed day won't reset their streak.
Streak shields also appear in the paywall feature list before checkout.
Premium subscribers now see a small amber crown 'Premium' chip next to their name on the profile page — a clear visual confirmation of their subscription status.
Premium subscribers no longer see an amber crown on the Level 2 divider — it now shows a green ✓ Unlocked chip in their language. Free users still see the amber crown and upgrade button as before.
The Review card in Practice now shows how many words in your deck come from premium (L2/L3) lessons — e.g. 'incl. 12 from premium lessons'. Reinforces that your subscription is actively helping you.
All lesson player labels are now trilingual — 'Staff' / 'You' speaker labels, 'Good to know' info cards, intro step feature list ('X phrases to listen to', 'Say the phrases aloud', 'Roleplay the real conversation', 'Cultural tips'), and the 'min · new words' header
Arabic and Farsi speakers no longer see English strings inside every lesson card
Certificate and lesson completion share messages no longer say 'Free' — just the URL. Premium subscribers won't accidentally signal the product is free when sharing their achievements.
Premium subscribers can now see their plan type (Monthly / Yearly) and next renewal date directly on the profile page
Tapping 'My Subscription' opens the Stripe Customer Portal — cancel, update payment method, or download invoices without contacting support
If your subscription is set to cancel at period end, the profile shows 'Cancels [date]' in red so there are no surprise endings
Premium subscribers no longer see the 'This app is free — support it' Ko-fi card on their profile — it only shows for free users now
Lesson intro banners now support 16:9 illustration images — upload to /images/illustrations/ and they appear automatically with gradient fallback
Onboarding language selector now shows the welcome title and subtitle in English, Arabic, and Farsi simultaneously — before you've chosen your language
Paywall now tells unauthenticated users that their subscription will be securely linked to the email they use at checkout, reducing drop-off for guests who haven't signed in yet
Vocabulary image added for hämta (to pick up) in Unit L1-1
Farsi phonetics added to every lesson across all 21 units — Arabic and Farsi speakers now both get a pronunciation guide written in their own script
Arabic and Farsi use different letters for the same Swedish sounds (پ vs ب for /p/, گ vs غ for /g/, و vs ف for /v/) — each language now gets the correct version
Profile page is now fully trilingual: stat labels (Lessons, Streak, Words, XP), section headers (Learning, Settings), and all menu items (My Progress, Achievements, My Vocabulary, Interface language, Notifications, What's new, Sign out) display in Arabic and Farsi
Learn page and Practice hub now flip to right-to-left layout for Arabic and Farsi — text alignment, card order, and icons all correct
Conversation bubbles inside lessons now align correctly in RTL: your speech on the left, the instructor on the right
Arabic unit descriptions no longer cut off mid-word — they now wrap to a second line instead of being clipped
Sync conflict sheet (shown when progress is found on another device) is now fully translated into Arabic and Farsi
Profile page streak card and sign-in prompt now display in Arabic and Farsi
Sign-in page is now fully trilingual — Arabic and Farsi speakers can sign in, send magic links, and read error messages in their own language
All 10 achievements now display in Arabic and Farsi — names, descriptions, and the achievements page header
Achievement toast notification also uses the correct language when it pops up after completing a lesson
Sentry error monitoring is now active — production crashes, failed API calls, and server errors are captured automatically so we can fix them faster
Events database auto-purges after 90 days — prevents unbounded growth over time
Rate-limit counters cleaned up automatically — no stale data accumulation
Server startup optimised — database indexes created once at cold start, not on every request
L2 and L3 lesson content is now verified on the server on every request — premium content requires a valid subscription at the routing level, not just in the UI
Non-premium users are redirected to the unit page before any lesson content is loaded or rendered
L2-1 free preview (first context sub-lesson) remains accessible to all users
Each unit now shows a "Quick reference" card at the bottom of the sub-lesson list, linking directly to the relevant phrasebook section
Tap it during or after a lesson to see all phrases for that situation in one place
9 units linked: shopping, transport, doctor, housing, bank, employment, school, pharmacy, and more
New unit: L1-9 Nödläge (Emergency) — how to call 112, what to say to police, fire, and ambulance; permanently free for everyone
7 new real-conversation listening episodes added — total is now 13
New episodes cover: parcel pickup, tax office, café ordering, introducing yourself, laundry room, employment office, and emergency calls
All 9 Level 1 units now have a matching listening episode to reinforce what you learned
After your first lesson, a prompt appears on the learning path to install SuperSvenska on your home screen
Android / Chrome: one tap triggers the native install dialog — no app store needed
iOS Safari: instructions guide you through Share → Add to Home Screen
Hidden if already installed; dismissible and never shown again once you've decided
Full L1 curriculum free forever — 8 units covering the first 30 days in Sweden
Premium subscription (29 SEK/month or 199 SEK/year) unlocks L2 & L3
Citizenship test — 69 SEK one-time unlock, 42 official questions across 9 categories
Arabic & Farsi UI throughout — built for the immigrant community
Spaced repetition, XP, streaks, achievements, and daily goals
Sponsor slot and Ko-fi/card support for the community to keep it running
Single sponsor banner injected between L1 and L2 on the learning path — seen by every user who progresses
Content/sponsor.ts config file: flip active: true and fill in the fields to go live, no env vars needed
SPONSORED disclosure label, rel="sponsored" on link — legally compliant with Swedish marknadsföringslagen
Translated tagline support (EN/AR/FA) and RTL layout
Click tracked in Plausible (Sponsor Clicked event)
Renders nothing when active: false — safe to deploy now, enable when deal is signed
Added a 'Buy me a coffee' card on the profile page linking to Ko-fi
Translated to Arabic and Farsi — matches the audience
Click tracked in Plausible (Kofi Clicked event)
Citizenship test is now a separate 69 SEK one-time purchase (forever access)
Compelling paywall landing page with urgency copy, feature list, and price
Premium subscribers (199 SEK/year) get citizenship test included automatically
Webhook-confirmed unlock: page polls up to 16s after Stripe redirect, shows 'Activating…' state
Premium upsell line on paywall — 'get this + Level 2 & 3 for 199 SEK/year'
New unlocks collection in MongoDB for durable one-time purchase records
L1 (8 units) remains free forever; L2–L3 (12 units) require a Premium subscription
Stripe Checkout integration — 29 SEK/month or 199 SEK/year (early access)
PaywallBanner bottom sheet: plan toggle, upgrade CTA, fully translated to Arabic and Farsi
Upgrade page (/upgrade) with free vs premium feature comparison
L2 level divider shows crown + 'Upgrade to unlock' button for free users
Premium users get immediate L2 access without completing all of L1
Webhook handler for subscription lifecycle events (active, past_due, canceled)
Floating 'Feedback' button appears on every page above the bottom nav
Four categories: Bug report, Feature idea, Content issue, Other
Fully translated to Arabic and Farsi
Submissions saved to database and emailed to the owner via Resend
19 missing audio files generated with ElevenLabs (Elin Nilsson — Clear and Calm Swedish Narration) — app now has 100% audio coverage, no TTS fallbacks
Leaderboard links removed from Profile and Progress pages until real ranking data is wired up
Student cards now show words mastered (book icon · count) alongside lessons and streak
New 'Words' sort option on the student roster
Search input on the vocabulary page filters by Swedish word or L1 translation in real time
Works in combination with existing filter chips (mastery, due, unit)
Clear button resets search; fully RTL-aware for Arabic and Farsi
Returning to a mid-lesson now shows a 'Continue / Start over' prompt instead of silently restarting
Prompt shows how far through the lesson you got (% complete)
Translated into Arabic and Farsi
Citizenship POST now sanitises data server-side — knownIds filtered against real question IDs, test scores clamped, invalid categories dropped
Magic-link email validation upgraded from includes('@') to strict EMAIL_RE regex
Class name capped at 200 chars to prevent DB bloat
Progress and citizenship writes rate-limited at 30/min per user
Replaced .find({}).toArray() with a filtered MongoDB cursor — only active users with completed lessons are fetched, one doc at a time
Token moved from query string (?token=…) to URL fragment (#token) — fragments are never sent to the server and never appear in access logs
Magic-verify endpoint changed from GET to POST; /auth/verify client page reads the hash and POSTs the token
Replaced Math.random() with crypto.randomInt() in generateCode() — codes are now drawn from a CSPRNG, making them unpredictable
RTL-detection script moved to /public/rtl-init.js and loaded as a plain render-blocking <script src> — no inline script, no dangerouslySetInnerHTML
Replaced === with crypto.timingSafeEqual in both cron routes — prevents timing-based secret enumeration
GET /api/class/my no longer includes teacher_email in the response — students receive only class code, name, and their own join date
Removed teacher_email from the StudentClass type and the optimistic join insert in TeacherSection
completed_lessons filtered against the real sublesson ID set on every POST — forged lesson counts are stripped before storage
vocab_scores values clamped 0–100 and total entries capped at 2,000 — bounds vocab_count inflation visible to teachers
streak_current, streak_longest, xp_total, xp_today clamped to non-negative reasonable maxima
l1_choice validated against known L1Lang values
Replaced loose includes('@') check with a strict EMAIL_RE regex that rejects addresses containing HTML special characters
Added escapeHtml() — email is escaped before interpolation into the HTML body, blocking tag/link injection
POST /api/progress and POST /api/citizenship now reject bodies over 64 KB with HTTP 413
Body is read as raw text before JSON parsing so the limit is enforced before any allocation
Added Content-Security-Policy — restricts scripts to self + Plausible, fonts to Google Fonts, frames blocked entirely
Added X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, HSTS (2-year, preload), Permissions-Policy
Removed X-Powered-By: Next.js header (poweredByHeader: false)
Renamed NEXT_PUBLIC_ADMIN_EMAILS → ADMIN_EMAILS — the list is now server-only and never inlined into the JS bundle
isAdmin flag baked into the JWT at sign-in (both Google OAuth and magic link paths) so client components read a boolean from their session, not the email list
Added NextAuth type augmentation so session.user.isAdmin is properly typed
NEXTAUTH_SECRET no longer has a fallback — missing it now fails hard (500 / redirect to /login?error=ServerError) instead of silently using a public default
createToken and verifyToken now take the secret as an explicit parameter with no module-level constant
props payload is now capped at 1 KB — oversized payloads are silently dropped (204)
IP rate limit: 60 events per minute per IP — well above any real user, stops flood writes
Extracted shared rate-limit helpers to lib/rate-limit.ts (used by magic-link and events)
Magic link requests are now rate-limited: 3 per email per 10 minutes, 10 per IP per 10 minutes
Blocks email-bombing a victim's inbox and cost-exhaustion attacks cycling through many addresses
Implemented via MongoDB atomic counters with a TTL index — no new infrastructure required
Returns 429 with Retry-After: 600 when limits are exceeded
Push subscription endpoint is now validated before being stored or used
Rejects any endpoint that is not HTTPS, points to a private/internal IP range, or is not a valid URL
Prevents an attacker from registering a crafted subscription to make the cron job send HTTP requests to internal infrastructure
Magic link URL is now always built from NEXTAUTH_URL — never the request Host header
If NEXTAUTH_URL is unset the endpoint returns 500 instead of silently using an attacker-controlled host
Prevents an attack where a spoofed Host header caused the sign-in link to point to an attacker's server
Magic link tokens can no longer be replayed within their 15-minute window
Each token is fingerprinted (SHA-256) and recorded in MongoDB on first use — a second click redirects to /login?error=TokenAlreadyUsed
The used_tokens collection self-purges via a TTL index so no manual cleanup is needed
Context intro now warns that 1177 starts with an automated menu, not a human
Added robot phrases to phrases sublesson: "Välkommen till 1177" and "Tryck 1 för att boka tid" — heard before the receptionist
New cultural tip (shown first): full explanation of the robot menu, what to press, and the tip to use 1177.se instead
Added "Har du ICA-kort?" as a "they say" phrase in the phrases sublesson
Appears right after the bag question in the step sequence, matching the real checkout order
Two new pick_sound quizzes drill both cashier opening questions against each other
Added "Kan du hjälpa mig med självkassan?" as a practised phrase (listen + speak) in the phrases sublesson
Updated self-checkout cultural tip to include the help phrase and reassure that staff are always nearby
Fixed vocab index bug: ange-din-kod (5) and tack-hdb-ica (6) steps were swapped
Added "Ange din kod" as a recognisable terminal phrase in the phrases sublesson (with listen + quiz)
Added cultural tip: 'Ange din kod' = enter your PIN, then press the green button
Added cultural tip: sometimes insert, sometimes tap — always look at the screen
Prevents the freeze moment when the terminal behaviour doesn't match what the roleplay taught
Added cultural tip: "You don't pay anything — staff scan the barcode from your SMS, check your ID, hand it over. Done."
Removes anxiety about an expected PIN or payment step that doesn't exist
Added "Har du en avisering?" (Do you have a notification?) as a clerk phrase in phrases and dialogue sublessons
Dialogue now flows: greeting → Har du en avisering? → show SMS → Kan jag se din legitimation? → collect package
New pick_sound quiz tests recognition of the phrase alongside similar-sounding clerk lines
Added 6th cultural tip to L1-1 (Hämta ett paket): staff at the ombud counter often say nothing — just show your phone (the SMS) and your ID
Intro description updated from "Five things" to "Six things"
Added 15 missing illustration types to the intro banner map (grocery, bus, doctor, skatteverket, fika, laundry, apartment, bank, office, school, pharmacy, maintenance, email, letter, tax)
All unit intro screens now show a matching icon and colour gradient instead of the default stethoscope fallback
Wrong answer on pick-picture and pick-sound now shows a persistent explanation strip
Strip reveals the correct Swedish word and its translation in your language
"Try again" resets the question so you can practise once more
"Continue" lets you move on if you prefer
Fully localised for Arabic and Farsi (RTL strip layout)
"Share certificate" button on the unit completion modal generates a 1080×540 PNG
On mobile: opens the native share sheet (WhatsApp, iMessage, etc.) with the image attached
On desktop: downloads the PNG directly
Image shows unit color banner, trophy, heading, unit title, stats, and vocab chips
Fully supports Arabic and Farsi (RTL canvas layout for ar/fa)
"Copy link" secondary action replaces the old share button
Replaced the unit grid + basic word list with a focused vocabulary manager
Filter chips: All · Due · Untested · Needs work · Practicing · Mastered · per-unit
"Due" chip shows a live count; "Review now" CTA appears and links straight to the review queue
Each row shows a mastery dot, translation, unit badge, and "Due" badge or next-due countdown
Mastery legend items are tappable shortcuts to the matching filter
Fully translated into Arabic and Farsi; RTL layout applied
New screen between course preview and ready: "How long can you study per day?"
Three options — 5 min (10 XP), 10 min (20 XP), 20 min (40 XP) — tap to select and advance
Sets daily_xp_goal in progress immediately; the path page XP ring reflects the choice from day one
Fully translated into Arabic and Farsi; RTL layout applied for ar/fa
Removed all // REVIEW: fa developer annotations from 32 source files — translations are live, not placeholders
Farsi text is present and correct across all units, UI components, and onboarding screens
Prompt no longer appears when NEXT_PUBLIC_VAPID_PUBLIC_KEY is unset — subscribing would silently fail
subscribe() now resets the loading state in all exit paths (moved vapidKey check inside try/finally)
Cheat sheet container now sets dir="rtl" when the interface language is Arabic or Farsi
Text, bullet dots, and category headers all align correctly for right-to-left reading
Back-arrow icons flip via the existing arrow-flip utility; removed the hardcoded ← character
"Resend link" button on the confirmation screen — re-fires the same email without navigating away
"Wrong email? Try a different address" resets the form so you can correct a typo
Resend button shows a brief "Sent!" confirmation after the second email goes out
Again / Hard / Good sublabels now show when the card will resurface ("tomorrow", "in 5d", …)
Computed live from SM-2 so the preview reflects your actual card's interval, not a fixed hint
Again stays red, Hard stays amber, Good stays green — colors unchanged
Added a stopped-ref guard so the timer tick chain halts the moment time expires
Prevents onDone from being called more than once if the parent re-renders before QuizView unmounts
Answers are saved to localStorage after each question — navigating away no longer loses your progress
Returning to the quiz resumes from where you left off, with a banner confirming it
"Start over" button lets you discard the draft and restart from Q1
Timed (45-min) quizzes are excluded from autosave — challenge mode stays strict
Each scenario card now shows ~X min next to the lesson count
Time reflects only the remaining sub-lessons — shrinks as you progress
Disappears once the unit is complete
Filter chips: All · Weak · per-unit (L1-1, L1-2 …) — horizontally scrollable
Weak filter shows words where your pronunciation score is below 60
Mastery dot on every word: grey = untested, red = needs work, amber = practicing, green = mastered
Translations now shown in your interface language (Arabic / Farsi)
Removed the mock 'Rank #X of 21' from the profile page — it was based on hardcoded scores, not real users
Daily push now fires when your streak is in danger (2+ days, not yet practiced today)
Message shows your streak count so you know exactly what's on the line
Combined message when both a streak and review words are due
Tap ‹ in the lesson header to go back one step
Revisit any audio, vocabulary card, or question you missed
Step state resets cleanly on back (answer selections, recording)
Full Arabic and Farsi translations for all 43 questions, answers, and explanations
Timed mock test mode — 45-minute countdown with auto-submit (real exam pressure)
Citizenship test card on the learning path for easier discovery
Cloud sync: test results and known-card state saved to your account
Daily push reminder when SR words are due — tap to open the review queue
Opt-in banner on the path page after your first lesson (one tap to enable)
Weekly email every Monday: streak, XP, and how many words need review
Cron jobs run at 07:00 UTC daily and 08:00 UTC every Monday
Roleplay steps now listen to your full sentence, not just the first word
Word-by-word colour diff shows which words landed (green / amber / red)
Tap the mic again to retry as many times as you like
Remaining dialogue lines play automatically after a successful take
SM-2 flashcard review — words resurface at growing intervals (1→3→7→20 days)
Review queue on the path page: tap to drill any words due today
Again / Hard / Good ratings tune each word's schedule individually
Vocab from every completed lesson enters the queue automatically
+2 XP per correct card to keep review streaks rewarding
Sign in with Google or a magic link — no password needed
Progress syncs automatically across all your devices
Smart merge resolves conflicts when two devices diverge
Progress stays safe on-device even without an account
Teacher classes and student roster now tied to your account
4 new L3 units: formal email, official letters, tax return, prescription renewal
Voice recording with real-time pronunciation feedback in every speak step
Full Swedish audio for all 20 units — L1, L2, and L3
Pronunciation result shows word-by-word colour diff (correct / close / wrong)
Practice page: Drills and Roleplay sessions now use real voice checking
Daily XP goal ring in the learning path header
Streak shields — auto-protect your streak when you miss a day
10 achievements to unlock as you progress
Streak rescue nudge on the path page when you haven't practised today
XP stat on your profile page
Create a class and share a 6-character join code with students
Student roster with progress bars, streak indicators, and last-active dates
Students can join classes via a share link (/join/CODE)
Teacher and student sections on the profile page
8 real-life scenarios for the first 30 days in Sweden
Listen, look, speak, and roleplay in each unit
Arabic and Farsi interface support
Streak tracking and word collector
Offline-capable with local progress sync
More scenarios and features arriving regularly.