Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Adds a globe button between the wordmark and the spacer in HomeView's header. Tapping it presents LanguageSwitchSheetView via .sheet(item:); onCommit writes the new l1Identifier and saves the model context. Promotes @Environment(\.modelContext) to unconditional (removing the #if DEBUG guard) so the save call is available in non-debug builds. Adds HomeViewLanguageSwitchTests smoke-testing the a11y label string. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Completes the multi‑L1 i18n stack by narrowing the onboarding age picker and adding an in‑app L1 language switcher accessible from Home.
Changes:
- Narrow Step 2 age picker to ages 6/7/8 with default age 7, and adjust tile layout/font sizing.
- Add an in-app language switch sheet (model + SwiftUI view) and wire it to a new Home header globe button.
- Extend
MoraStringswith four new fields and populate them across JP/KO/EN tables, with tests updated accordingly.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Packages/MoraUI/Tests/MoraUITests/LanguageSwitchSheetTests.swift | Adds unit tests for the language switch sheet model callbacks and disabled/enabled confirm logic. |
| Packages/MoraUI/Tests/MoraUITests/LanguageAgeFlowTests.swift | Pins narrowed age options and default age via tests. |
| Packages/MoraUI/Tests/MoraUITests/HomeViewLanguageSwitchTests.swift | Adds a basic test asserting the new string field value for the Home globe button a11y label. |
| Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageSwitchSheet.swift | Introduces the sheet model + view for switching L1 from Home. |
| Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePickerView.swift | Refactors onboarding Step 1 to reuse the extracted LanguagePicker component. |
| Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguagePicker.swift | Adds reusable language picker row list UI used by onboarding and the new sheet. |
| Packages/MoraUI/Sources/MoraUI/LanguageAge/LanguageAgeFlow.swift | Adds ageOptions and defaultAge, updates state default selection. |
| Packages/MoraUI/Sources/MoraUI/LanguageAge/AgePickerView.swift | Updates Step 2 UI to render only the narrowed age options and larger tile font. |
| Packages/MoraUI/Sources/MoraUI/Home/HomeView.swift | Adds globe button + sheet presentation; commits updated l1Identifier to SwiftData. |
| Packages/MoraCore/Tests/MoraCoreTests/LocaleScriptBudgetTests.swift | Extends string-field enumeration to include the four new MoraStrings fields. |
| Packages/MoraCore/Sources/MoraCore/MoraStrings.swift | Adds four new MoraStrings fields and wires them through the initializer. |
| Packages/MoraCore/Sources/MoraCore/KoreanL1Profile.swift | Supplies the four new in-app language-switch strings for KO. |
| Packages/MoraCore/Sources/MoraCore/JapaneseL1Profile.swift | Supplies the four new in-app language-switch strings for JP entry/core/advanced tables. |
| Packages/MoraCore/Sources/MoraCore/EnglishL1Profile.swift | Supplies the four new in-app language-switch strings for EN. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| onCommit: { newID in | ||
| profile.l1Identifier = newID | ||
| try? modelContext.save() | ||
| languageSheet = nil | ||
| }, |
There was a problem hiding this comment.
onCommit updates profile.l1Identifier but swallows persistence errors via try? modelContext.save(). If save() throws, the sheet still dismisses and the in-memory change may be lost on next launch. Handle the error (e.g., do/catch with assertionFailure/logging and revert profile.l1Identifier or keep the sheet open) instead of ignoring it.
There was a problem hiding this comment.
Good catch — fixed in 2213f41. Replaced try? modelContext.save() with do/catch; on failure I revert profile.l1Identifier to its previous value, log via Logger (tech.reenable.Mora / HomeView category), and leave the sheet open so the user sees the change didn't take. Sheet still dismisses on success.
| public func simulateSelect(identifier: String) { | ||
| pickedID = identifier | ||
| } | ||
|
|
||
| public func simulateConfirm() { | ||
| guard !isConfirmDisabled else { return } | ||
| onCommit(pickedID) | ||
| } | ||
|
|
||
| public func simulateCancel() { | ||
| onCancel() | ||
| } |
There was a problem hiding this comment.
The methods simulateSelect / simulateConfirm / simulateCancel are part of the production LanguageSwitchSheet public API and are invoked by the SwiftUI view. “simulate*” reads like a test-only helper; consider renaming them to user actions (e.g., select(identifier:), confirm(), cancel()) and/or lowering their visibility (internal) so the public surface matches real behavior.
There was a problem hiding this comment.
Agreed — fixed in 2213f41. Renamed simulateSelect/Confirm/Cancel → select(identifier:) / confirm() / cancel(). They're called from the toolbar buttons in LanguageSwitchSheetView, so the names should describe real user actions rather than test simulation. Tests updated to match.
| var step: Step = .language | ||
| var selectedLanguageID: String | ||
| var selectedAge: Int? = 8 // pre-selected per spec §6.2 | ||
| var selectedAge: Int? = LanguageAgeFlow.defaultAge // pre-selected per spec §6.2 |
There was a problem hiding this comment.
The inline comment references spec §6.2, but this change makes the default age driven by LanguageAgeFlow.defaultAge (documented as spec §7.2). Update the comment to avoid pointing future readers at the wrong spec section.
| var selectedAge: Int? = LanguageAgeFlow.defaultAge // pre-selected per spec §6.2 | |
| var selectedAge: Int? = LanguageAgeFlow.defaultAge // pre-selected per spec §7.2 |
There was a problem hiding this comment.
Fixed in 2213f41 — updated the comment to point at spec §7.2 (the new section that covers age narrowing in the multi-L1 i18n design).
- HomeView: replace `try? modelContext.save()` with do/catch; on failure, revert the in-memory `profile.l1Identifier` so it doesn't drift from disk, log via OSLog, and leave the sheet open so the user sees the tap didn't take instead of silently losing it on next launch. - LanguageSwitchSheet: rename `simulateSelect/Confirm/Cancel` → `select/confirm/cancel`. They are part of the production public API driven by the toolbar buttons, not test-only helpers — the names should match real user actions. - LanguageAgeState: fix spec reference §6.2 → §7.2 in the `selectedAge` initializer comment (matches the plan's PR 3 section). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Final PR in the multi-L1 i18n stack. Behavioral changes for the dev install:
MoraStringsfields (homeChangeLanguageButton,languageSwitchSheetTitle,languageSwitchSheetCancel,languageSwitchSheetConfirm) authored across all five tables (JP entry / core / advanced + KO + EN).LanguagePickerview component fromLanguagePickerView's row list, so the same row UI is reused by onboarding Step 1 and the new switch sheet.LanguageSwitchSheet(ObservableObjectmodel +LanguageSwitchSheetView) with confirm-disabled-when-unchanged guard.HomeViewnext to the wordmark; tap presents the sheet, commit writesLearnerProfile.l1Identifierand dismisses without disturbing other profile state (age / level / interests / font preserved).Part of the multi-L1 i18n stack. See
docs/superpowers/plans/2026-04-26-i18n-and-age-difficulty.mdanddocs/superpowers/specs/2026-04-26-i18n-and-age-difficulty-design.md§7.2 (age narrowing) and §7.3 (in-app language switch).Test plan
swift test— MoraCore (149), MoraEngines (1), MoraUI (74), MoraTesting (22). All 0 failures.xcodegen generate && xcodebuild build -project Mora.xcodeproj -scheme Mora -destination 'generic/platform=iOS Simulator' -configuration Debug CODE_SIGNING_ALLOWED=NO→ BUILD SUCCEEDED.swift-format lint --strict --recursive Mora Packages/*/Sources Packages/*/Tests→ clean.🤖 Generated with Claude Code