New "Books" row in the Practice tab opens a library of bundled bilingual
books. Each chapter renders Spanish paragraph-by-paragraph; tap any
word for a definition sheet (DictionaryService with on-device AI
fallback), or toggle the toolbar button to swap to the pre-computed
English translation inline.
Local-only Book + BookChapter SwiftData models added to the local
container schema (reset version bumped to 5). DataLoader.seedBooks
walks the bundle for `book_*.json` resources, so future books drop in
without touching app code — just bundle a new JSON and bump
bookDataVersion.
First book: Olly Richards' "Spanish Short Stories For Beginners
Vol 2" — 13 chapters, 2,646 paragraphs, bilingual.
Scripts/books/ is the repeatable pipeline for future EPUBs:
extract_epub.py → translate_chapters.py (per-chapter resumable jobs) →
bundle_book.py. Translation is done by parallel Claude Code subagents
reading per-job input files and writing output files — no API key
required, matching the pattern used for the textbook vocab vision
pass. See Scripts/books/README.md for the full how-to.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-week "Extra Study (N)" row appears at the end of each LanGo week
section when at least one card is marked. Cards are marked from inside
VocabFlashcardView via a star next to the speaker on reveal. Marks are
keyed by the same SHA256 hash CourseReviewCard uses, so a mark and its
SRS state describe the same logical card.
ExtraStudyMark is CloudKit-synced (private DB), with uniqueness enforced
by fetch-or-create on id since CloudKit forbids @Attribute(.unique).
Skipped for textbook courses: DeckStudyView nils out the mark context
when the deck's courseName matches a TextbookChapter, and CourseView
hides the row when the active course is a textbook — so there are no
orphan marks the user can't reach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small fixes after the LLM-vision pass:
1. merge_pdf_into_book.py — when the LLM classifies an image as 'hybrid'
but extracts zero pairs (e.g., a conjugation table whose only English
text is on the section header that was excluded by the prompt rules),
respect that decision instead of falling through to the bbox/heuristic
pipeline. Previously: 1 chapter-2 estar conjugation table generated
4 bad pairs from the heuristic fallback.
2. fix_vocab.py language_score — recognize Spanish present-perfect
('he tenido', 'He andado por este pueblo') as Spanish. The classifier
was treating the auxiliary 'he'/'has'/'ha' as English subject pronouns,
producing false-positive mis-orientation flags on 4 chapter-15/20/23
present-perfect example tables.
Result: mis-oriented vocab pairs across the book go from 5 → 0.
textbookDataVersion bumped to 14.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bbox-OCR pipeline mis-paired ~114 vocab tables across the book — the
chapter 7 "Other Idioms" image (issue #32) being the most visible.
Three failure modes were collapsing the data:
1) classifier blind to subject pronouns ("yo", "I", etc.)
2) right-then-left OCR reads on 2-col tables
3) Y-cluster drift on multi-line cells in 4-col layouts
Replaced the entire vocab-extraction tier with a Claude vision pass over
all 931 vocab images. Output is keyed by image with three classifications:
- pair_table (extract all Spanish↔English pairs)
- reference_only (Spanish-only conjugation tables — no pairs, UI shows
the flat OCR lines as a reference list instead)
- hybrid (some header pairs + reference content beneath; only
the genuine pairs become cards)
merge_pdf_into_book.py now picks pair source by priority:
llm-vision → bounding-box OCR → block-alternation heuristic.
Numbers (across the whole book):
- mis-oriented tables: 114 → 5
- quarantined cards: 250 → 2
- extracted pairs: 2832 → 4569
textbookDataVersion bumped to 13. Per-batch agent outputs gitignored
under Conjuga/Scripts/textbook/paired_vocab_llm/ — only the merged
paired_vocab_llm.json (also gitignored) is needed to rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The eligibility filter required every form's regularity tag to equal
"regular", but the data uses four labels:
- regular (179 forms — curated paradigm verbs)
- ordinary (50,992 forms — pattern-following verbs like hablar, comer)
- irregular (8,653)
- orto (176 — orthographic spelling changes like busqué)
Result was a 27-combo eligible pool, ~26 of which were -ir verbs in
present tense — every Full Table prompt landed on the same handful of
verbs.
Pulled the rule into a SharedModels function (FullTableEligibility) so
it's testable in isolation. Accepts "regular" + "ordinary" (both mean
"follows the pattern"); rejects "irregular" and "orto". 9 unit tests
cover the matrix including edge cases (incomplete forms, mixed labels,
unknown values).
PracticeSessionService.makePromptIfFullyRegular now delegates to
FullTableEligibility, sorting forms by personIndex so the regularity
array lines up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full Table (issue from chat): drop the level filter — Full Table tests
regular conjugation patterns, not vocabulary recognition, so restricting
to Basic-level verbs collapsed the eligible pool to two combos
(vivir present, ir future). Pool now draws from all 1,750 verbs. Random
sampling first; if 40 attempts fail we fall through to a deterministic
shuffled scan that guarantees finding any eligible (verb, tense) combo
when one exists. Returning nil now happens only when the user's filters
genuinely produce zero eligible prompts. The view replaces its silent
blank screen with a ContentUnavailableView pointing at the settings
that need adjusting. FeatureReferenceView documents the level exception.
Streak (issue #31 follow-up): activity recording was scoped to flashcard
and Full Table reviews only, so spending an hour on textbook work,
guides, videos, or AI chat could break a "streak" that the dashboard
kept displaying as if it were intact. Three fixes:
1. Extract ReviewStore.recordActivity(context:) — a streak-only entry
point that any user-initiated learning action can call.
2. Add UserProgress.validateStreakIfStale(today:context:) — resets a
broken currentStreak to 0 immediately, called from app launch and
dashboard appear so the displayed number is never a lie.
3. DailyLog formatter pins POSIX locale + current timezone so the
yyyy-MM-dd strings can't drift across locales.
Wired recordActivity into every previously-silent learning action: chat
send, story-quiz completion, textbook exercise submit, grammar exercise
completion, course-deck study finish, week test / checkpoint save,
listening + pronunciation check, cloze quiz completion, lyrics word
lookup, video stream / play / download success, sentence-builder check,
and course-vocab SRS rate (which was bypassing ReviewStore entirely).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- VideoActionsView: stack icon over title via a custom label style so
the Stream/Download/Play row fits iPhone width without wrapping
mid-word ("Stre am", "Down load").
- project.yml: add textbook_data.json and textbook_vocab.json to the
Conjuga target's Copy Bundle Resources. They were missing, so
DataLoader.seedTextbookData early-returned every launch and the
textbook never appeared in the Book tab. Regenerated Conjuga.xcodeproj.
Add YouTube video integration, reflexive-verb list + practice-pool
filters, verb-detail example sentences, lyrics word lookup. Bump iOS /
Xcode requirements to 26. Correct grammar-note count and document the
YouTubeKit + AVFoundation muxing architecture plus the video-curation
script.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap 24 tense-guide / grammar-note videos to The Language Tutor's
numbered lesson series where a matching lesson exists, filling the two
remaining gaps (ind_preterito_anterior → Lesson 65, estar-gerund-
progressive → Lesson 113). All 32 TLT picks preserved on this pass.
For the non-TLT slots, prefer BaseLang's beginner lesson series where a
topic-specific video exists: ser-vs-estar, preterite-vs-imperfect,
subjunctive-triggers, object-pronouns, conditional-if-clauses,
tener-expressions, future-vs-ir-a, possessive-adjectives,
irregular-yo-verbs, and stem-changing-verbs.
Retire both Tell Me In Spanish videos (personal-a → castellano4U,
types-of-irregular-verbs → Master IRREGULAR VERBS Complete Lesson).
Generator header note clarifies that "not available on this app" rows
are a transient yt-dlp extraction limit — videos still play when tapped
in the app via the Stream button, which opens youtube.com externally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
youtube_videos.md lists every entry in youtube_videos.json with its
tense-guide / grammar-note id, title, channel, upload date, duration,
views, and likes (where public). Also flags the two topics with no
curated video so the gap is auditable in one place.
generate_videos_markdown.py queries yt-dlp in parallel for each unique
videoId and writes the markdown. Rerun when curation changes. One
current entry (saber-vs-conocer → j87i7MVCvIE) is now marked Private
Video — needs re-curation as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YouTubeKit returns stream URLs only; its own README punts on the
progressive-vs-adaptive problem. Most modern YouTube videos above 360p
serve video and audio as separate DASH tracks, so the previous "any MP4"
fallback silently grabbed a video-only track and the downloaded file had
no audio.
VideoDownloadService now resolves a DownloadPlan up front. Progressive
MP4 (when one exists) still streams straight to the final destination.
Otherwise the service downloads the best MP4 video track + best M4A
audio track to temp files, then combines them into a single MP4 via
AVMutableComposition + AVAssetExportSession. Passthrough preset first
(lossless container rewrite) with a fallback to highestQuality if the
codec combination requires re-encoding. Temp files are cleaned up on
both success and failure.
DownloadStatus replaces the bare Double progress so the UI can show
per-phase labels (Video %, Audio %, Finalizing…). Muxing progress is
rendered as an indeterminate spinner since AVAssetExportSession's
progress property doesn't cross actor boundaries cleanly.
Also retires the deprecated Stream.subtype comparison in favor of
Stream.fileExtension, matching YouTubeKit's current API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The middle button in the Stream/Download/Play row now cycles through the
full download lifecycle instead of ending at a disabled "Downloaded"
checkmark. Once a video is on disk the button becomes a red, destructive
"Delete" with a trash icon; tapping presents a confirmation dialog, and
confirming removes the file + SwiftData row, flipping the button back to
"Download" and disabling Play.
Settings → Downloaded Videos retains the swipe-delete and "Delete all"
affordances for bulk management.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each of the 56 tense guides and grammar notes gets a curated YouTube video
attached (54 with picks, 2 silent nulls on rare / hard-to-find topics).
Users can stream in YouTube/Safari, download via YouTubeKit for offline
viewing, or play the local MP4 full-screen via AVPlayer.
YouTubeVideoStore loads the bundled youtube_videos.json at launch and serves
lookups by tense id or grammar note id. VideoDownloadService resolves the
best progressive MP4 stream off the main actor (YouTubeKit isn't Sendable),
writes to documents/videos/<videoId>.mp4, and records a DownloadedVideo row
in the local SwiftData container so the app knows what's on disk across
launches.
VideoActionsButtonRow is the unified UI for both detail views: three large
buttons — Stream (red, always enabled), Download (blue, disabled while in
flight and after completion, shows progress), Play (green, enabled only
when downloaded). Full-screen cover on tap. Settings gains a Downloaded
Videos list with swipe-delete, total-size summary, and a 500 MB warning.
Local store reset version bumped to 4 for the new DownloadedVideo schema.
Known fragility: YouTubeKit scrapes YouTube's private stream API and will
break when YouTube changes their internal format. Streaming keeps working
regardless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the 100 most common reflexive verbs from spanishwithdaniel.com as a
canonical list and wires it through the UI. Compound list entries (recibirse
/ graduarse, equivocarse / confundirse) are split. Trailing prepositions and
set-phrase completions are captured as usageHint (e.g. acordarse "de",
ponerse "de acuerdo").
ReflexiveVerbStore loads the JSON at launch and exposes lookups by base
infinitive, both via @Environment for SwiftUI and a static shared instance
for services. Verbs whose bare infinitive isn't in the list skip the UI
treatment silently.
VerbDetailView shows a new Reflexive section with the reflexive infinitive,
usage hint, and English meaning when there is a match. VerbListView gains a
"Reflexive verbs only" filter alongside the existing Level and Irregularity
filters. Settings adds the same flag so it also constrains the practice
pool; PracticeSessionService applies the reflexive filter in all six pick
paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tapping a verb now shows six example sentences beneath the conjugation table,
one per core tense (Present, Preterite, Imperfect, Future, Present Subjunctive,
Imperative). Each example renders the tense label, Spanish sentence, and
English translation in the DeckStudyView style.
Generation uses Foundation Models with a @Generable schema that pins each
response to the requested tenseId and forces tú/nosotros subjects for
imperatives. Results are cached as JSON in the Caches directory keyed by
verb id (DictionaryService pattern); cache misses regenerate on demand.
Devices without Apple Intelligence see an inline notice instead of the
loading state. No network dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UserProgress gains selectedLevelsBlob and enabledIrregularCategoriesBlob
(mirrors the existing tense-blob pattern). The multi-level setter keeps the
legacy selectedLevel String in sync with the highest-ranked selection, so
widget sync, AI scenarios, and achievement checks keep working unchanged.
Legacy single-level users are migrated on first read.
Settings replaces the level Picker with per-level toggles and adds an
Irregular Types section with three toggles. Practice pool is the literal
intersection: empty levels means zero results, empty irregular categories
means no irregularity constraint.
Pure filter logic lives in SharedModels (PracticeFilter, VerbLevel.highest)
and is covered by 20 Swift Testing cases. ReferenceStore delegates so the
intersection behavior is unit-tested without a ModelContainer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tokenize Spanish lyric lines into a flow layout of underlined, long-pressable
words. Long-press (0.35s) opens a sheet with base form, English, part of
speech, and a Tense · person row for verbs. Unknown words silently no-op.
English gloss lines remain untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README: expanded from the original terse list into categorized sections
covering the six conjugation practice modes, textbook reader, AI chat
and stories, listening/pronunciation, cloze, lyrics, vocab SRS, offline
dictionary, grammar notes/exercises, and CloudKit sync. Architecture
section now documents the dual local/cloud SwiftData stores with the
App Group ID, the widget-schema-must-match requirement, and the
Scripts/textbook extraction pipeline.
app_features.md: added a full Conjuga section (practice modes, verb
reference, grammar, dictionary, sync, widgets, data counts) alongside
the existing ConjuGato and Conjuu ES analyses; added Conjuga as a
first column in the comparison table with rows for the new capability
axes (AI, textbook, speech, offline dictionary, lyrics, CloudKit,
widgets); added a "Conjuga excels at" strengths section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codifies the rule that Claude must not run git commit or git push
without an explicit request from the user in the current turn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the repeatedly-disappearing textbook: both widget timeline
providers were opening the shared local SwiftData store with a schema
that omitted TextbookChapter. On each widget refresh SwiftData
destructively migrated the store to match the widget's narrower schema,
dropping the ZTEXTBOOKCHAPTER rows (and sometimes the table itself).
The app then re-created an empty table on next open, but
refreshTextbookDataIfNeeded skipped re-seeding because the UserDefaults
version flag was already current — leaving the store empty indefinitely.
Three changes:
1. Widgets (CombinedWidget, WordOfDayWidget): added TextbookChapter to
both schema arrays so they match the main app. Widget refreshes will
no longer drop the entity.
2. DataLoader.refreshTextbookDataIfNeeded: trigger now considers BOTH
the version flag and the actual on-disk row count. If rows are
missing for any reason (past wipes, future subset-schema openers,
corruption), the next launch re-seeds. Eliminates the class of bug
where a version flag lies about what's really in the store.
3. StoreInspector: reports ZTEXTBOOKCHAPTER row count alongside the
other entities so we can confirm state from logs.
Bumped textbookDataVersion to 12 so devices that were stuck in the
silent-failure state re-seed on next launch regardless of prior flag
value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toolbar Filter menu now has two sections:
- Level (existing)
- Irregularity: Any Irregular / Spelling Change / Stem Change /
Unique Irregular
Filters combine, so "Basic" + "Unique Irregular" narrows to the
foundational ser/ir/haber-class verbs. Categories are derived at load
time from existing IrregularSpan rows using the same spanType ranges
already used by PracticeSessionService (1xx spelling, 2xx stem, 3xx
unique), so no schema or data changes are required.
UI additions:
- Per-row icons (star / arrows / I-beam) show each verb's
irregularity categories at a glance, tinted by type.
- When any filter is active, a chip bar appears under the search
field showing the active filters (tap to clear) and the resulting
verb count.
- Filter toolbar icon fills when any filter is applied.
Data coverage: 614 / 1750 verbs are flagged irregular — 411 spelling,
275 stem-change, 67 unique — consistent with canonical lists from
SpanishDict, Lawless Spanish, and Wikipedia.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
randomFullTablePrompt now rejects any (verb, tense) combo where any
form has regularity != "regular" and keeps retrying (40 attempts) until
a fully-regular combo is picked. Full Table practice is meant for
drilling regular conjugation patterns — irregular tables belong in the
Irregularity Drills mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs were causing chapters to disappear on every relaunch:
1. seedTextbookData used `try? context.save()` (swallowing errors) and
returned `inserted > 0`, so a failed save still reported success.
Callers then bumped UserDefaults textbookDataVersion and subsequent
launches skipped the re-seed entirely — with no rows on disk.
2. refreshTextbookDataIfNeeded wiped chapters via the batch-delete API
`context.delete(model: TextbookChapter.self)`, which hits the store
directly without clearing the context's .unique-id index. Re-inserting
chapters with the same ids could then throw a unique-constraint error
on save — also silently eaten by `try?`.
Fixes:
- seedTextbookData now uses do/catch around save(), returns false on
error, and verifies persistence via fetchCount before returning true.
- refreshTextbookDataIfNeeded fetches and deletes chapters individually
so the context tracks the deletion cleanly; wipe save is also now
checked and bails early on failure.
- Bumped textbookDataVersion to 11 so devices poisoned by the previous
silent-failure path retry on next launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier launches (before cd491bd bundled textbook_data.json) ran
seedTextbookData, got "not bundled — skipping", and still bumped
UserDefaults textbookDataVersion to 9. Subsequent launches then
short-circuited in refreshTextbookDataIfNeeded and never re-seeded,
so ZTEXTBOOKCHAPTER stayed empty and the Course tab hid the section.
- seedTextbookData now returns Bool (true only when chapters inserted).
- Both call sites only write the version key on success, so a missing
or unparseable bundle no longer locks us out of future retries.
- Bumped textbookDataVersion to 10 to force existing installs to
re-seed on next launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the chapter reader showed vocab tables as a flat list of OCR
lines — because Vision reads columns top-to-bottom, the Spanish column
appeared as one block followed by the English column, making pairings
illegible.
Now every vocab table renders as a 2-column grid with Spanish on the
left and English on the right. Supporting changes:
- New ocr_all_vocab.swift: bounding-box OCR over all 931 vocab images,
cluster lines into rows by Y-coordinate, split rows by largest X-gap,
detect 2- / 3- / 4-column layouts automatically. ~2800 pairs extracted
this pass vs ~1100 from the old block-alternation heuristic.
- merge_pdf_into_book.py now prefers bounding-box pairs when present,
falls back to the heuristic, embeds the resulting pairs as
vocab_table.cards in book.json.
- DataLoader passes cards through to TextbookBlock on seed.
- TextbookChapterView renders cards via SwiftUI Grid (2 cols).
- fix_vocab.py quarantine rule relaxed — only mis-pairs where both
sides are clearly the same language are removed. "unknown" sides
stay (bbox pipeline already oriented them correctly).
Textbook card count jumps from 1044 → 3118 active pairs.
textbookDataVersion bumped to 9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pbxproj references textbook_data.json and textbook_vocab.json as Copy
Bundle Resources, so xcodebuild fails if they're missing. Committing the
generated output keeps the repo self-sufficient — regenerate via
Conjuga/Scripts/textbook/run_pipeline.sh when content changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Assistant messages now render each word as a button. Tap shows a sheet
with base form, English translation, and part of speech. Dictionary
lookup first; falls back to Foundation Models (@Generable ChatWordInfo)
for words not in the local dictionary. Results cached per-session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch audio session to .record-only, use nil tap format so the system
picks a compatible format, and route through AVAudioEngine with a 4096
buffer. Avoids the mDataByteSize(0) assertion seen on some devices.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cards now show "tengo — I have" instead of just "tengo", so learners
see the English meaning alongside the Spanish yo form. Bumps course
data version to 6 to trigger re-seed on next launch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Guard against empty audio buffers before appending to speech
recognition request — AVAudioBuffer asserts non-zero data size.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ser vs Estar: 100 sentences
- Por vs Para: 100 sentences
- Preterite vs Imperfect: 100 sentences
- Subjunctive Triggers: 100 sentences
- Personal A: 100 sentences
Each session randomly selects 10 from the pool for variety.
Closes#15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes stuck state after completing grammar exercises — adds a
dismiss button on the results screen.
Closes#14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap startRecording in do/catch so audio setup failures don't crash.
Validate recording format has channels before installTap. Use
DispatchQueue.main.async instead of Task{@MainActor} in recognition
callback to avoid dispatch queue assertions.
Closes#13
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Side by side on iPad, stacked vertically on iPhone. Fixes
calendar grid overflowing on narrow screens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Request authorization off main queue and marshal callback result back
via DispatchQueue.main.async. Check current status first to avoid
unnecessary system prompt if already authorized or denied.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New features:
- Offline Dictionary: reverse index of 175K verb forms + 200 common
words, cached to disk, powers instant word lookups in Stories
- Vocab SRS Review: spaced repetition for course vocabulary cards
with due count badge and Again/Hard/Good/Easy rating
- Cloze Practice: fill-in-the-blank using SentenceQuizEngine with
distractor generation from vocabulary pool
- Grammar Exercises: interactive quizzes for 5 grammar topics
(ser/estar, por/para, preterite/imperfect, subjunctive, personal a)
with "Practice This" button on grammar note detail
- Listening Practice: listen-and-type + pronunciation check modes
using Speech framework with word-by-word match scoring
- Conversational Practice: AI chat partner via Foundation Models
with 10 scenario types, saved to cloud container
Other changes:
- Add Conversation model to SharedModels and cloud container
- Add Info.plist keys for speech recognition and microphone
- Skip speech auth on simulator to prevent crash
- Fix preparing data screen to only show during seed/migration
- Extract courseDataVersion to static property on DataLoader
- Add "How Features Work" reference page in Settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generate one-paragraph Spanish stories on-device using Foundation Models,
matched to user's level and enabled tenses. Every word is tappable —
pre-annotated words show instantly, others get a quick on-device AI
lookup with caching. English translation hidden by default behind a
toggle. Comprehension quiz with 3 multiple-choice questions. Stories
saved to cloud container for sync and persistence across resets.
Closes#9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add two grammar notes under new "Irregular Verbs" category:
- Most Common Irregular Verbs: 15 essential verbs with present
and preterite forms plus example sentences
- Types of Irregular Verbs: spelling changes, stem changes, and
unique irregulars with patterns and examples
Closes#5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>