Fixes #28 — Curated reflexive verb list on detail + practice filter
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>
This commit is contained in:
@@ -63,9 +63,11 @@
|
|||||||
8DC1CB93333F94C5297D33BF /* GrammarExerciseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F24ED76059609D6857EC97 /* GrammarExerciseView.swift */; };
|
8DC1CB93333F94C5297D33BF /* GrammarExerciseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F24ED76059609D6857EC97 /* GrammarExerciseView.swift */; };
|
||||||
90B76E34F195223580F7CCCF /* DictionaryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BE2A08EC694FF784ED5575 /* DictionaryService.swift */; };
|
90B76E34F195223580F7CCCF /* DictionaryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BE2A08EC694FF784ED5575 /* DictionaryService.swift */; };
|
||||||
943A94A8C71919F3EFC0E8FA /* UserProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E536AD1180FE10576EAC884A /* UserProgress.swift */; };
|
943A94A8C71919F3EFC0E8FA /* UserProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E536AD1180FE10576EAC884A /* UserProgress.swift */; };
|
||||||
|
97A2088134FC6CB41C507182 /* reflexive_verbs.json in Resources */ = {isa = PBXBuildFile; fileRef = 3644B5ED77F29A65877D926A /* reflexive_verbs.json */; };
|
||||||
97EFCF6724CE59DC4F0274FD /* AchievementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C42EA0EBD4CB1E10A82BA25 /* AchievementService.swift */; };
|
97EFCF6724CE59DC4F0274FD /* AchievementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C42EA0EBD4CB1E10A82BA25 /* AchievementService.swift */; };
|
||||||
9D9FD3853C5C969C62AE9999 /* StartupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B95B276C054DBFE508C4D1 /* StartupCoordinator.swift */; };
|
9D9FD3853C5C969C62AE9999 /* StartupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B95B276C054DBFE508C4D1 /* StartupCoordinator.swift */; };
|
||||||
9F0ACDC1F4ACB1E0D331283D /* CheckpointExamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C67DD1A1CB9B8B5A2BDCED /* CheckpointExamView.swift */; };
|
9F0ACDC1F4ACB1E0D331283D /* CheckpointExamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C67DD1A1CB9B8B5A2BDCED /* CheckpointExamView.swift */; };
|
||||||
|
A4DA8CF1957A4FE161830AB2 /* ReflexiveVerbStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940826D9ED5C18D2C4E7B2C7 /* ReflexiveVerbStore.swift */; };
|
||||||
A7DF435F99E66E067F2B33E1 /* ListeningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D1904DF07E0A6816134CF3 /* ListeningView.swift */; };
|
A7DF435F99E66E067F2B33E1 /* ListeningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D1904DF07E0A6816134CF3 /* ListeningView.swift */; };
|
||||||
A9959AE6C87B4AD21554E401 /* FullTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711CB7539EF5887F6F7B8B82 /* FullTableView.swift */; };
|
A9959AE6C87B4AD21554E401 /* FullTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711CB7539EF5887F6F7B8B82 /* FullTableView.swift */; };
|
||||||
AAC6F85A1C3B6C1186E1656A /* TenseEndingTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D98E1564C6538056D81200 /* TenseEndingTable.swift */; };
|
AAC6F85A1C3B6C1186E1656A /* TenseEndingTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D98E1564C6538056D81200 /* TenseEndingTable.swift */; };
|
||||||
@@ -153,6 +155,7 @@
|
|||||||
2931634BEB33B93429CE254F /* VocabFlashcardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VocabFlashcardView.swift; sourceTree = "<group>"; };
|
2931634BEB33B93429CE254F /* VocabFlashcardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VocabFlashcardView.swift; sourceTree = "<group>"; };
|
||||||
30EF2362D9FFF9B07A45CE6D /* StreakCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreakCalendarView.swift; sourceTree = "<group>"; };
|
30EF2362D9FFF9B07A45CE6D /* StreakCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreakCalendarView.swift; sourceTree = "<group>"; };
|
||||||
34C67DD1A1CB9B8B5A2BDCED /* CheckpointExamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckpointExamView.swift; sourceTree = "<group>"; };
|
34C67DD1A1CB9B8B5A2BDCED /* CheckpointExamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckpointExamView.swift; sourceTree = "<group>"; };
|
||||||
|
3644B5ED77F29A65877D926A /* reflexive_verbs.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = reflexive_verbs.json; sourceTree = "<group>"; };
|
||||||
3695075616689E72DBB26D4C /* HandwritingRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandwritingRecognizer.swift; sourceTree = "<group>"; };
|
3695075616689E72DBB26D4C /* HandwritingRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandwritingRecognizer.swift; sourceTree = "<group>"; };
|
||||||
39908548430FDF01D76201FB /* TextbookChapterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextbookChapterView.swift; sourceTree = "<group>"; };
|
39908548430FDF01D76201FB /* TextbookChapterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextbookChapterView.swift; sourceTree = "<group>"; };
|
||||||
3A96C065B8787DEC6818E497 /* ConversationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationService.swift; sourceTree = "<group>"; };
|
3A96C065B8787DEC6818E497 /* ConversationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationService.swift; sourceTree = "<group>"; };
|
||||||
@@ -197,6 +200,7 @@
|
|||||||
8C2D88FF9A3B0590B22C7837 /* conjuga_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = conjuga_data.json; sourceTree = "<group>"; };
|
8C2D88FF9A3B0590B22C7837 /* conjuga_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = conjuga_data.json; sourceTree = "<group>"; };
|
||||||
8C935ECDF8A5D8D6FA541E20 /* GuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideView.swift; sourceTree = "<group>"; };
|
8C935ECDF8A5D8D6FA541E20 /* GuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideView.swift; sourceTree = "<group>"; };
|
||||||
8E9BCDBB9BC24F5C8117767E /* WordOfDayWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordOfDayWidget.swift; sourceTree = "<group>"; };
|
8E9BCDBB9BC24F5C8117767E /* WordOfDayWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordOfDayWidget.swift; sourceTree = "<group>"; };
|
||||||
|
940826D9ED5C18D2C4E7B2C7 /* ReflexiveVerbStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflexiveVerbStore.swift; sourceTree = "<group>"; };
|
||||||
9708FF3CF33E4765DB225F93 /* ConjugaWidgetExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = ConjugaWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
9708FF3CF33E4765DB225F93 /* ConjugaWidgetExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = ConjugaWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
9E1FB35614B709E6B1D1D017 /* Conjuga.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Conjuga.entitlements; sourceTree = "<group>"; };
|
9E1FB35614B709E6B1D1D017 /* Conjuga.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Conjuga.entitlements; sourceTree = "<group>"; };
|
||||||
A014EEC3EE08E945FBBA5335 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
A014EEC3EE08E945FBBA5335 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
1EB4830F9289AACC82D753F8 /* ConjugaApp.swift */,
|
1EB4830F9289AACC82D753F8 /* ConjugaApp.swift */,
|
||||||
BC273716CD14A99EFF8206CA /* course_data.json */,
|
BC273716CD14A99EFF8206CA /* course_data.json */,
|
||||||
7E6AF62A3A949630E067DC22 /* Info.plist */,
|
7E6AF62A3A949630E067DC22 /* Info.plist */,
|
||||||
|
3644B5ED77F29A65877D926A /* reflexive_verbs.json */,
|
||||||
353C5DE41FD410FA82E3AED7 /* Models */,
|
353C5DE41FD410FA82E3AED7 /* Models */,
|
||||||
1994867BC8E985795A172854 /* Services */,
|
1994867BC8E985795A172854 /* Services */,
|
||||||
BFC1AEBE02CE22E6474FFEA6 /* Utilities */,
|
BFC1AEBE02CE22E6474FFEA6 /* Utilities */,
|
||||||
@@ -298,6 +303,7 @@
|
|||||||
842DB48F8570C39CDCFF2F57 /* PracticeSessionService.swift */,
|
842DB48F8570C39CDCFF2F57 /* PracticeSessionService.swift */,
|
||||||
4AE3D1D8723D5C41D3501774 /* PronunciationService.swift */,
|
4AE3D1D8723D5C41D3501774 /* PronunciationService.swift */,
|
||||||
777C696A841803D5B775B678 /* ReferenceStore.swift */,
|
777C696A841803D5B775B678 /* ReferenceStore.swift */,
|
||||||
|
940826D9ED5C18D2C4E7B2C7 /* ReflexiveVerbStore.swift */,
|
||||||
CBCF6FCFA6B00151C2371E77 /* ReviewStore.swift */,
|
CBCF6FCFA6B00151C2371E77 /* ReviewStore.swift */,
|
||||||
49E3AD244327CBF24B7A2752 /* SpeechService.swift */,
|
49E3AD244327CBF24B7A2752 /* SpeechService.swift */,
|
||||||
5C0E6EAFC0D24928BA956FA5 /* SRSEngine.swift */,
|
5C0E6EAFC0D24928BA956FA5 /* SRSEngine.swift */,
|
||||||
@@ -606,6 +612,7 @@
|
|||||||
F59655A8B8FCE6264315DD33 /* Assets.xcassets in Resources */,
|
F59655A8B8FCE6264315DD33 /* Assets.xcassets in Resources */,
|
||||||
CF9E48ADF0501FB79F3DDB7B /* conjuga_data.json in Resources */,
|
CF9E48ADF0501FB79F3DDB7B /* conjuga_data.json in Resources */,
|
||||||
2B5B2D63DC9C290F66890A4A /* course_data.json in Resources */,
|
2B5B2D63DC9C290F66890A4A /* course_data.json in Resources */,
|
||||||
|
97A2088134FC6CB41C507182 /* reflexive_verbs.json in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -663,6 +670,7 @@
|
|||||||
0D0D3B5CC128D1A1D1252282 /* PronunciationService.swift in Sources */,
|
0D0D3B5CC128D1A1D1252282 /* PronunciationService.swift in Sources */,
|
||||||
53A0AC57EAC44B676C997374 /* QuizType.swift in Sources */,
|
53A0AC57EAC44B676C997374 /* QuizType.swift in Sources */,
|
||||||
DF82C2579F9889DDB06362CC /* ReferenceStore.swift in Sources */,
|
DF82C2579F9889DDB06362CC /* ReferenceStore.swift in Sources */,
|
||||||
|
A4DA8CF1957A4FE161830AB2 /* ReflexiveVerbStore.swift in Sources */,
|
||||||
FC7873F97017532C215DAD34 /* ReviewCard.swift in Sources */,
|
FC7873F97017532C215DAD34 /* ReviewCard.swift in Sources */,
|
||||||
728702D9AA7A8BDABBA62513 /* ReviewStore.swift in Sources */,
|
728702D9AA7A8BDABBA62513 /* ReviewStore.swift in Sources */,
|
||||||
51D072AF30F4B12CD3E8F918 /* SRSEngine.swift in Sources */,
|
51D072AF30F4B12CD3E8F918 /* SRSEngine.swift in Sources */,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ struct ConjugaApp: App {
|
|||||||
@State private var studyTimer = StudyTimerService()
|
@State private var studyTimer = StudyTimerService()
|
||||||
@State private var dictionary = DictionaryService()
|
@State private var dictionary = DictionaryService()
|
||||||
@State private var verbExampleCache = VerbExampleCache()
|
@State private var verbExampleCache = VerbExampleCache()
|
||||||
|
@State private var reflexiveStore = ReflexiveVerbStore()
|
||||||
|
|
||||||
let localContainer: ModelContainer
|
let localContainer: ModelContainer
|
||||||
let cloudContainer: ModelContainer
|
let cloudContainer: ModelContainer
|
||||||
@@ -115,6 +116,7 @@ struct ConjugaApp: App {
|
|||||||
.environment(studyTimer)
|
.environment(studyTimer)
|
||||||
.environment(dictionary)
|
.environment(dictionary)
|
||||||
.environment(verbExampleCache)
|
.environment(verbExampleCache)
|
||||||
|
.environment(reflexiveStore)
|
||||||
.task {
|
.task {
|
||||||
let needsSeed = await DataLoader.needsSeeding(container: localContainer)
|
let needsSeed = await DataLoader.needsSeeding(container: localContainer)
|
||||||
if needsSeed {
|
if needsSeed {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final class UserProgress {
|
|||||||
var selectedLevel: String = "basic"
|
var selectedLevel: String = "basic"
|
||||||
var showVosotros: Bool = true
|
var showVosotros: Bool = true
|
||||||
var autoFillStem: Bool = false
|
var autoFillStem: Bool = false
|
||||||
|
var showReflexiveVerbsOnly: Bool = false
|
||||||
|
|
||||||
// Legacy CloudKit array-backed fields retained for migration compatibility.
|
// Legacy CloudKit array-backed fields retained for migration compatibility.
|
||||||
var enabledTenses: [String] = []
|
var enabledTenses: [String] = []
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ struct PracticeSettings: Sendable {
|
|||||||
let enabledTenses: Set<String>
|
let enabledTenses: Set<String>
|
||||||
let enabledIrregularCategories: Set<IrregularSpan.SpanCategory>
|
let enabledIrregularCategories: Set<IrregularSpan.SpanCategory>
|
||||||
let showVosotros: Bool
|
let showVosotros: Bool
|
||||||
|
let showReflexiveVerbsOnly: Bool
|
||||||
|
let reflexiveBaseInfinitives: Set<String>
|
||||||
|
|
||||||
init(progress: UserProgress?) {
|
init(progress: UserProgress?, reflexiveBaseInfinitives: Set<String> = []) {
|
||||||
let resolvedTenses = progress?.enabledTenseIDs ?? []
|
let resolvedTenses = progress?.enabledTenseIDs ?? []
|
||||||
let resolvedLevels = progress?.selectedVerbLevels ?? []
|
let resolvedLevels = progress?.selectedVerbLevels ?? []
|
||||||
self.selectedLevel = progress?.selectedLevel ?? VerbLevel.basic.rawValue
|
self.selectedLevel = progress?.selectedLevel ?? VerbLevel.basic.rawValue
|
||||||
@@ -17,6 +19,8 @@ struct PracticeSettings: Sendable {
|
|||||||
self.enabledTenses = Set(resolvedTenses)
|
self.enabledTenses = Set(resolvedTenses)
|
||||||
self.enabledIrregularCategories = progress?.enabledIrregularCategories ?? []
|
self.enabledIrregularCategories = progress?.enabledIrregularCategories ?? []
|
||||||
self.showVosotros = progress?.showVosotros ?? true
|
self.showVosotros = progress?.showVosotros ?? true
|
||||||
|
self.showReflexiveVerbsOnly = progress?.showReflexiveVerbsOnly ?? false
|
||||||
|
self.reflexiveBaseInfinitives = reflexiveBaseInfinitives
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectionTenseIDs: [String] {
|
var selectionTenseIDs: [String] {
|
||||||
@@ -41,16 +45,25 @@ struct FullTablePrompt {
|
|||||||
struct PracticeSessionService {
|
struct PracticeSessionService {
|
||||||
let localContext: ModelContext
|
let localContext: ModelContext
|
||||||
let cloudContext: ModelContext
|
let cloudContext: ModelContext
|
||||||
|
let reflexiveBaseInfinitives: Set<String>
|
||||||
private let referenceStore: ReferenceStore
|
private let referenceStore: ReferenceStore
|
||||||
|
|
||||||
init(localContext: ModelContext, cloudContext: ModelContext) {
|
init(
|
||||||
|
localContext: ModelContext,
|
||||||
|
cloudContext: ModelContext,
|
||||||
|
reflexiveBaseInfinitives: Set<String> = []
|
||||||
|
) {
|
||||||
self.localContext = localContext
|
self.localContext = localContext
|
||||||
self.cloudContext = cloudContext
|
self.cloudContext = cloudContext
|
||||||
|
self.reflexiveBaseInfinitives = reflexiveBaseInfinitives
|
||||||
self.referenceStore = ReferenceStore(context: localContext)
|
self.referenceStore = ReferenceStore(context: localContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func settings() -> PracticeSettings {
|
func settings() -> PracticeSettings {
|
||||||
PracticeSettings(progress: ReviewStore.fetchOrCreateUserProgress(context: cloudContext))
|
PracticeSettings(
|
||||||
|
progress: ReviewStore.fetchOrCreateUserProgress(context: cloudContext),
|
||||||
|
reflexiveBaseInfinitives: reflexiveBaseInfinitives
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextCard(for focusMode: FocusMode, excludingVerbId lastVerbId: Int? = nil) -> PracticeCardLoad? {
|
func nextCard(for focusMode: FocusMode, excludingVerbId lastVerbId: Int? = nil) -> PracticeCardLoad? {
|
||||||
@@ -86,7 +99,10 @@ struct PracticeSessionService {
|
|||||||
let settings = settings()
|
let settings = settings()
|
||||||
// Full Table practice is regular-only, so the irregular-category setting is
|
// Full Table practice is regular-only, so the irregular-category setting is
|
||||||
// deliberately ignored here (applying it would empty the pool).
|
// deliberately ignored here (applying it would empty the pool).
|
||||||
let verbs = referenceStore.fetchVerbs(selectedLevels: settings.selectedLevels)
|
let verbs = applyReflexiveFilter(
|
||||||
|
to: referenceStore.fetchVerbs(selectedLevels: settings.selectedLevels),
|
||||||
|
settings: settings
|
||||||
|
)
|
||||||
guard !verbs.isEmpty else { return nil }
|
guard !verbs.isEmpty else { return nil }
|
||||||
|
|
||||||
for _ in 0..<40 {
|
for _ in 0..<40 {
|
||||||
@@ -143,6 +159,27 @@ struct PracticeSessionService {
|
|||||||
return buildCardLoad(verb: verb, form: form)
|
return buildCardLoad(verb: verb, form: form)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When the user has "Reflexive verbs only" enabled, restrict the allowed
|
||||||
|
/// verb-id set to IDs whose infinitive is in the curated list.
|
||||||
|
/// No-op otherwise.
|
||||||
|
private func applyReflexiveFilter(to ids: Set<Int>, settings: PracticeSettings) -> Set<Int> {
|
||||||
|
guard settings.showReflexiveVerbsOnly, !settings.reflexiveBaseInfinitives.isEmpty else {
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
let matching = ids.filter { id in
|
||||||
|
guard let verb = referenceStore.fetchVerb(id: id) else { return false }
|
||||||
|
return settings.reflexiveBaseInfinitives.contains(verb.infinitive.lowercased())
|
||||||
|
}
|
||||||
|
return matching
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyReflexiveFilter(to verbs: [Verb], settings: PracticeSettings) -> [Verb] {
|
||||||
|
guard settings.showReflexiveVerbsOnly, !settings.reflexiveBaseInfinitives.isEmpty else {
|
||||||
|
return verbs
|
||||||
|
}
|
||||||
|
return verbs.filter { settings.reflexiveBaseInfinitives.contains($0.infinitive.lowercased()) }
|
||||||
|
}
|
||||||
|
|
||||||
private func buildCardLoad(verb: Verb, form: VerbForm) -> PracticeCardLoad {
|
private func buildCardLoad(verb: Verb, form: VerbForm) -> PracticeCardLoad {
|
||||||
let spans = referenceStore.fetchSpans(
|
let spans = referenceStore.fetchSpans(
|
||||||
verbId: form.verbId,
|
verbId: form.verbId,
|
||||||
@@ -164,9 +201,12 @@ struct PracticeSessionService {
|
|||||||
|
|
||||||
private func fetchDueCard(excluding lastVerbId: Int?) -> ReviewCard? {
|
private func fetchDueCard(excluding lastVerbId: Int?) -> ReviewCard? {
|
||||||
let settings = settings()
|
let settings = settings()
|
||||||
let allowedVerbIds = referenceStore.allowedVerbIDs(
|
let allowedVerbIds = applyReflexiveFilter(
|
||||||
selectedLevels: settings.selectedLevels,
|
to: referenceStore.allowedVerbIDs(
|
||||||
irregularCategories: settings.enabledIrregularCategories
|
selectedLevels: settings.selectedLevels,
|
||||||
|
irregularCategories: settings.enabledIrregularCategories
|
||||||
|
),
|
||||||
|
settings: settings
|
||||||
)
|
)
|
||||||
let now = Date()
|
let now = Date()
|
||||||
var descriptor = FetchDescriptor<ReviewCard>(
|
var descriptor = FetchDescriptor<ReviewCard>(
|
||||||
@@ -194,9 +234,12 @@ struct PracticeSessionService {
|
|||||||
|
|
||||||
private func pickWeakForm() -> VerbForm? {
|
private func pickWeakForm() -> VerbForm? {
|
||||||
let settings = settings()
|
let settings = settings()
|
||||||
let allowedVerbIds = referenceStore.allowedVerbIDs(
|
let allowedVerbIds = applyReflexiveFilter(
|
||||||
selectedLevels: settings.selectedLevels,
|
to: referenceStore.allowedVerbIDs(
|
||||||
irregularCategories: settings.enabledIrregularCategories
|
selectedLevels: settings.selectedLevels,
|
||||||
|
irregularCategories: settings.enabledIrregularCategories
|
||||||
|
),
|
||||||
|
settings: settings
|
||||||
)
|
)
|
||||||
|
|
||||||
let descriptor = FetchDescriptor<ReviewCard>(
|
let descriptor = FetchDescriptor<ReviewCard>(
|
||||||
@@ -221,9 +264,12 @@ struct PracticeSessionService {
|
|||||||
let settings = settings()
|
let settings = settings()
|
||||||
// Focus mode explicitly selects one irregular category, so the user's
|
// Focus mode explicitly selects one irregular category, so the user's
|
||||||
// settings-level irregular filter is deliberately skipped here.
|
// settings-level irregular filter is deliberately skipped here.
|
||||||
let allowedVerbIds = referenceStore.allowedVerbIDs(
|
let allowedVerbIds = applyReflexiveFilter(
|
||||||
selectedLevels: settings.selectedLevels,
|
to: referenceStore.allowedVerbIDs(
|
||||||
irregularCategories: []
|
selectedLevels: settings.selectedLevels,
|
||||||
|
irregularCategories: []
|
||||||
|
),
|
||||||
|
settings: settings
|
||||||
)
|
)
|
||||||
let typeRange: ClosedRange<Int>
|
let typeRange: ClosedRange<Int>
|
||||||
|
|
||||||
@@ -261,9 +307,12 @@ struct PracticeSessionService {
|
|||||||
private func pickCommonTenseForm() -> VerbForm? {
|
private func pickCommonTenseForm() -> VerbForm? {
|
||||||
let settings = settings()
|
let settings = settings()
|
||||||
let coreTenseIDs = TenseID.coreTenseIDs
|
let coreTenseIDs = TenseID.coreTenseIDs
|
||||||
let verbs = referenceStore.fetchVerbs(
|
let verbs = applyReflexiveFilter(
|
||||||
selectedLevels: settings.selectedLevels,
|
to: referenceStore.fetchVerbs(
|
||||||
irregularCategories: settings.enabledIrregularCategories
|
selectedLevels: settings.selectedLevels,
|
||||||
|
irregularCategories: settings.enabledIrregularCategories
|
||||||
|
),
|
||||||
|
settings: settings
|
||||||
)
|
)
|
||||||
guard let verb = verbs.randomElement() else { return nil }
|
guard let verb = verbs.randomElement() else { return nil }
|
||||||
|
|
||||||
@@ -277,9 +326,12 @@ struct PracticeSessionService {
|
|||||||
|
|
||||||
private func pickRandomForm() -> VerbForm? {
|
private func pickRandomForm() -> VerbForm? {
|
||||||
let settings = settings()
|
let settings = settings()
|
||||||
let verbs = referenceStore.fetchVerbs(
|
let verbs = applyReflexiveFilter(
|
||||||
selectedLevels: settings.selectedLevels,
|
to: referenceStore.fetchVerbs(
|
||||||
irregularCategories: settings.enabledIrregularCategories
|
selectedLevels: settings.selectedLevels,
|
||||||
|
irregularCategories: settings.enabledIrregularCategories
|
||||||
|
),
|
||||||
|
settings: settings
|
||||||
)
|
)
|
||||||
guard let verb = verbs.randomElement() else { return nil }
|
guard let verb = verbs.randomElement() else { return nil }
|
||||||
|
|
||||||
|
|||||||
59
Conjuga/Conjuga/Services/ReflexiveVerbStore.swift
Normal file
59
Conjuga/Conjuga/Services/ReflexiveVerbStore.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
import SharedModels
|
||||||
|
|
||||||
|
/// Loads and queries the curated reflexive-verb list bundled with the app
|
||||||
|
/// (Gitea issue #28). One JSON load at init; in-memory lookup thereafter.
|
||||||
|
///
|
||||||
|
/// `entries(for:)` returns a list because a single base infinitive may map to
|
||||||
|
/// multiple reflexive entries — e.g., `ponerse` covers both "to put on
|
||||||
|
/// (clothing) / to become" and "to come to an agreement (with)".
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class ReflexiveVerbStore {
|
||||||
|
|
||||||
|
/// Process-wide accessor for services that can't use @Environment injection
|
||||||
|
/// (e.g. PracticeSessionService called from ViewModels). Views should still
|
||||||
|
/// prefer @Environment(ReflexiveVerbStore.self) for consistency.
|
||||||
|
static let shared = ReflexiveVerbStore()
|
||||||
|
|
||||||
|
private(set) var entries: [ReflexiveVerb] = []
|
||||||
|
private var indexByBase: [String: [ReflexiveVerb]] = [:]
|
||||||
|
|
||||||
|
/// Set of base infinitives present in the list. Cheap lookup for filters.
|
||||||
|
private(set) var baseInfinitives: Set<String> = []
|
||||||
|
|
||||||
|
init(bundle: Bundle = .main) {
|
||||||
|
load(from: bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All reflexive entries whose base infinitive matches (case-insensitive).
|
||||||
|
func entries(for baseInfinitive: String) -> [ReflexiveVerb] {
|
||||||
|
indexByBase[baseInfinitive.lowercased()] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience — true when the verb's bare infinitive appears in the list.
|
||||||
|
func isReflexive(baseInfinitive: String) -> Bool {
|
||||||
|
baseInfinitives.contains(baseInfinitive.lowercased())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load(from bundle: Bundle) {
|
||||||
|
guard let url = bundle.url(forResource: "reflexive_verbs", withExtension: "json"),
|
||||||
|
let data = try? Data(contentsOf: url) else {
|
||||||
|
print("[ReflexiveVerbStore] bundled reflexive_verbs.json not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode([ReflexiveVerb].self, from: data)
|
||||||
|
entries = decoded
|
||||||
|
var index: [String: [ReflexiveVerb]] = [:]
|
||||||
|
for entry in decoded {
|
||||||
|
index[entry.baseInfinitive.lowercased(), default: []].append(entry)
|
||||||
|
}
|
||||||
|
indexByBase = index
|
||||||
|
baseInfinitives = Set(index.keys)
|
||||||
|
print("[ReflexiveVerbStore] loaded \(decoded.count) entries (\(baseInfinitives.count) distinct base infinitives)")
|
||||||
|
} catch {
|
||||||
|
print("[ReflexiveVerbStore] decode failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,7 +96,11 @@ final class PracticeViewModel {
|
|||||||
currentSpans = []
|
currentSpans = []
|
||||||
hasCards = true
|
hasCards = true
|
||||||
isLoading = true
|
isLoading = true
|
||||||
let service = PracticeSessionService(localContext: localContext, cloudContext: cloudContext)
|
let service = PracticeSessionService(
|
||||||
|
localContext: localContext,
|
||||||
|
cloudContext: cloudContext,
|
||||||
|
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
|
||||||
|
)
|
||||||
guard let cardLoad = service.nextCard(for: focusMode, excludingVerbId: currentVerb?.id) else {
|
guard let cardLoad = service.nextCard(for: focusMode, excludingVerbId: currentVerb?.id) else {
|
||||||
clearCurrentCard()
|
clearCurrentCard()
|
||||||
hasCards = false
|
hasCards = false
|
||||||
|
|||||||
@@ -243,7 +243,11 @@ struct FullTableView: View {
|
|||||||
results = Array(repeating: nil, count: 6)
|
results = Array(repeating: nil, count: 6)
|
||||||
correctForms = []
|
correctForms = []
|
||||||
drawings = Array(repeating: PKDrawing(), count: 6)
|
drawings = Array(repeating: PKDrawing(), count: 6)
|
||||||
let service = PracticeSessionService(localContext: modelContext, cloudContext: cloudModelContext)
|
let service = PracticeSessionService(
|
||||||
|
localContext: modelContext,
|
||||||
|
cloudContext: cloudModelContext,
|
||||||
|
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
|
||||||
|
)
|
||||||
guard let prompt = service.randomFullTablePrompt() else {
|
guard let prompt = service.randomFullTablePrompt() else {
|
||||||
currentVerb = nil
|
currentVerb = nil
|
||||||
currentTense = nil
|
currentTense = nil
|
||||||
@@ -312,7 +316,11 @@ struct FullTableView: View {
|
|||||||
if allCorrect { sessionCorrect += 1 }
|
if allCorrect { sessionCorrect += 1 }
|
||||||
|
|
||||||
if let verb = currentVerb, let tense = currentTense {
|
if let verb = currentVerb, let tense = currentTense {
|
||||||
let service = PracticeSessionService(localContext: modelContext, cloudContext: cloudModelContext)
|
let service = PracticeSessionService(
|
||||||
|
localContext: modelContext,
|
||||||
|
cloudContext: cloudModelContext,
|
||||||
|
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
|
||||||
|
)
|
||||||
let reviewResults = Dictionary(uniqueKeysWithValues: personsToShow.map { ($0.index, results[$0.index] == true) })
|
let reviewResults = Dictionary(uniqueKeysWithValues: personsToShow.map { ($0.index, results[$0.index] == true) })
|
||||||
_ = service.recordFullTableReview(verbId: verb.id, tenseId: tense.id, results: reviewResults)
|
_ = service.recordFullTableReview(verbId: verb.id, tenseId: tense.id, results: reviewResults)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,20 @@ struct SettingsView: View {
|
|||||||
Text("Leave all off to include regular and irregular verbs. Enable any to restrict practice to those irregularity types.")
|
Text("Leave all off to include regular and irregular verbs. Enable any to restrict practice to those irregularity types.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle("Reflexive verbs only", isOn: Binding(
|
||||||
|
get: { progress?.showReflexiveVerbsOnly ?? false },
|
||||||
|
set: { enabled in
|
||||||
|
progress?.showReflexiveVerbsOnly = enabled
|
||||||
|
saveProgress()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
} header: {
|
||||||
|
Text("Reflexive")
|
||||||
|
} footer: {
|
||||||
|
Text("When on, practice pulls only from the curated list of common reflexive verbs.")
|
||||||
|
}
|
||||||
|
|
||||||
Section("Stats") {
|
Section("Stats") {
|
||||||
if let progress {
|
if let progress {
|
||||||
LabeledContent("Total Reviewed", value: "\(progress.totalReviewed)")
|
LabeledContent("Total Reviewed", value: "\(progress.totalReviewed)")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SwiftData
|
|||||||
struct VerbDetailView: View {
|
struct VerbDetailView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
@Environment(VerbExampleCache.self) private var exampleCache
|
@Environment(VerbExampleCache.self) private var exampleCache
|
||||||
|
@Environment(ReflexiveVerbStore.self) private var reflexiveStore
|
||||||
@State private var speechService = SpeechService()
|
@State private var speechService = SpeechService()
|
||||||
let verb: Verb
|
let verb: Verb
|
||||||
@State private var selectedTense: TenseInfo = TenseInfo.all[0]
|
@State private var selectedTense: TenseInfo = TenseInfo.all[0]
|
||||||
@@ -33,6 +34,10 @@ struct VerbDetailView: View {
|
|||||||
ReferenceStore(context: modelContext).fetchForms(verbId: verb.id, tenseId: selectedTense.id)
|
ReferenceStore(context: modelContext).fetchForms(verbId: verb.id, tenseId: selectedTense.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var reflexiveEntries: [ReflexiveVerb] {
|
||||||
|
reflexiveStore.entries(for: verb.infinitive)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
@@ -46,6 +51,10 @@ struct VerbDetailView: View {
|
|||||||
Text("Info")
|
Text("Info")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !reflexiveEntries.isEmpty {
|
||||||
|
reflexiveSection
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker("Tense", selection: $selectedTense) {
|
Picker("Tense", selection: $selectedTense) {
|
||||||
ForEach(TenseInfo.all) { tense in
|
ForEach(TenseInfo.all) { tense in
|
||||||
@@ -106,6 +115,38 @@ struct VerbDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Reflexive
|
||||||
|
|
||||||
|
private var reflexiveSection: some View {
|
||||||
|
Section {
|
||||||
|
ForEach(Array(reflexiveEntries.enumerated()), id: \.offset) { _, entry in
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
|
Text(entry.infinitive)
|
||||||
|
.font(.body.weight(.semibold))
|
||||||
|
.italic()
|
||||||
|
if let hint = entry.usageHint, !hint.isEmpty {
|
||||||
|
Text(hint)
|
||||||
|
.font(.caption.weight(.medium))
|
||||||
|
.foregroundStyle(.tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(entry.english)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Reflexive")
|
||||||
|
} footer: {
|
||||||
|
if reflexiveEntries.contains(where: { $0.usageHint != nil }) {
|
||||||
|
Text("Highlighted words are prepositions or phrases this verb commonly pairs with.")
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Examples
|
// MARK: - Examples
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -198,4 +239,5 @@ struct VerbDetailView: View {
|
|||||||
}
|
}
|
||||||
.modelContainer(for: [Verb.self, VerbForm.self], inMemory: true)
|
.modelContainer(for: [Verb.self, VerbForm.self], inMemory: true)
|
||||||
.environment(VerbExampleCache())
|
.environment(VerbExampleCache())
|
||||||
|
.environment(ReflexiveVerbStore())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ enum IrregularityCategory: String, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
struct VerbListView: View {
|
struct VerbListView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
@Environment(ReflexiveVerbStore.self) private var reflexiveStore
|
||||||
@State private var verbs: [Verb] = []
|
@State private var verbs: [Verb] = []
|
||||||
@State private var irregularityByVerbId: [Int: Set<IrregularityCategory>] = [:]
|
@State private var irregularityByVerbId: [Int: Set<IrregularityCategory>] = [:]
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@State private var selectedLevel: String?
|
@State private var selectedLevel: String?
|
||||||
@State private var selectedIrregularity: IrregularityCategory?
|
@State private var selectedIrregularity: IrregularityCategory?
|
||||||
|
@State private var reflexiveOnly: Bool = false
|
||||||
@State private var selectedVerb: Verb?
|
@State private var selectedVerb: Verb?
|
||||||
|
|
||||||
private var filteredVerbs: [Verb] {
|
private var filteredVerbs: [Verb] {
|
||||||
@@ -40,6 +42,9 @@ struct VerbListView: View {
|
|||||||
return category == .anyIrregular ? !cats.isEmpty : cats.contains(category)
|
return category == .anyIrregular ? !cats.isEmpty : cats.contains(category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if reflexiveOnly {
|
||||||
|
result = result.filter { reflexiveStore.isReflexive(baseInfinitive: $0.infinitive) }
|
||||||
|
}
|
||||||
if !searchText.isEmpty {
|
if !searchText.isEmpty {
|
||||||
let query = searchText.lowercased()
|
let query = searchText.lowercased()
|
||||||
result = result.filter {
|
result = result.filter {
|
||||||
@@ -98,6 +103,14 @@ struct VerbListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("Reflexive") {
|
||||||
|
Button {
|
||||||
|
reflexiveOnly.toggle()
|
||||||
|
} label: {
|
||||||
|
Label("Reflexive verbs only", systemImage: reflexiveOnly ? "checkmark" : "arrow.triangle.2.circlepath")
|
||||||
|
}
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("Filter", systemImage: hasActiveFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
|
Label("Filter", systemImage: hasActiveFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
|
||||||
}
|
}
|
||||||
@@ -115,7 +128,7 @@ struct VerbListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var hasActiveFilter: Bool {
|
private var hasActiveFilter: Bool {
|
||||||
selectedLevel != nil || selectedIrregularity != nil
|
selectedLevel != nil || selectedIrregularity != nil || reflexiveOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -131,6 +144,11 @@ struct VerbListView: View {
|
|||||||
selectedIrregularity = nil
|
selectedIrregularity = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if reflexiveOnly {
|
||||||
|
filterChip(text: "Reflexive", systemImage: "arrow.triangle.2.circlepath") {
|
||||||
|
reflexiveOnly = false
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(filteredVerbs.count)")
|
Text("\(filteredVerbs.count)")
|
||||||
.font(.caption.monospacedDigit())
|
.font(.caption.monospacedDigit())
|
||||||
|
|||||||
104
Conjuga/Conjuga/reflexive_verbs.json
Normal file
104
Conjuga/Conjuga/reflexive_verbs.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
[
|
||||||
|
{"infinitive": "aburrirse", "baseInfinitive": "aburrir", "english": "to get bored"},
|
||||||
|
{"infinitive": "acercarse", "baseInfinitive": "acercar", "english": "to get close to", "usageHint": "a"},
|
||||||
|
{"infinitive": "acordarse", "baseInfinitive": "acordar", "english": "to remember", "usageHint": "de"},
|
||||||
|
{"infinitive": "acostarse", "baseInfinitive": "acostar", "english": "to lay down / to go to bed"},
|
||||||
|
{"infinitive": "acostumbrarse", "baseInfinitive": "acostumbrar", "english": "to get used to", "usageHint": "a"},
|
||||||
|
{"infinitive": "afeitarse", "baseInfinitive": "afeitar", "english": "to shave"},
|
||||||
|
{"infinitive": "alegrarse", "baseInfinitive": "alegrar", "english": "to be glad / happy / pleased"},
|
||||||
|
{"infinitive": "alejarse", "baseInfinitive": "alejar", "english": "to get away from", "usageHint": "de"},
|
||||||
|
{"infinitive": "animarse", "baseInfinitive": "animar", "english": "to cheer up / to dare to do something", "usageHint": "a"},
|
||||||
|
{"infinitive": "apurarse", "baseInfinitive": "apurar", "english": "to hurry"},
|
||||||
|
{"infinitive": "aprovecharse", "baseInfinitive": "aprovechar", "english": "to take advantage of", "usageHint": "de"},
|
||||||
|
{"infinitive": "asustarse", "baseInfinitive": "asustar", "english": "to get or become afraid"},
|
||||||
|
{"infinitive": "atreverse", "baseInfinitive": "atrever", "english": "to dare to", "usageHint": "a"},
|
||||||
|
{"infinitive": "bañarse", "baseInfinitive": "bañar", "english": "to take a bath / shower"},
|
||||||
|
{"infinitive": "burlarse", "baseInfinitive": "burlar", "english": "to make fun of", "usageHint": "de"},
|
||||||
|
{"infinitive": "caerse", "baseInfinitive": "caer", "english": "to fall down"},
|
||||||
|
{"infinitive": "calmarse", "baseInfinitive": "calmar", "english": "to calm down"},
|
||||||
|
{"infinitive": "cansarse", "baseInfinitive": "cansar", "english": "to get tired (of)", "usageHint": "(de)"},
|
||||||
|
{"infinitive": "casarse", "baseInfinitive": "casar", "english": "to marry", "usageHint": "con"},
|
||||||
|
{"infinitive": "cepillarse", "baseInfinitive": "cepillar", "english": "to brush (hair, teeth)"},
|
||||||
|
{"infinitive": "deprimirse", "baseInfinitive": "deprimir", "english": "to become depressed"},
|
||||||
|
{"infinitive": "conformarse", "baseInfinitive": "conformar", "english": "to resign oneself to", "usageHint": "con"},
|
||||||
|
{"infinitive": "volverse", "baseInfinitive": "volver", "english": "to become / to turn into / to return"},
|
||||||
|
{"infinitive": "darse", "baseInfinitive": "dar", "english": "to realize", "usageHint": "cuenta de"},
|
||||||
|
{"infinitive": "dedicarse", "baseInfinitive": "dedicar", "english": "to dedicate oneself to / to do for a living", "usageHint": "a"},
|
||||||
|
{"infinitive": "despedirse", "baseInfinitive": "despedir", "english": "to say goodbye", "usageHint": "(de)"},
|
||||||
|
{"infinitive": "despertarse", "baseInfinitive": "despertar", "english": "to wake up"},
|
||||||
|
{"infinitive": "desvestirse", "baseInfinitive": "desvestir", "english": "to undress"},
|
||||||
|
{"infinitive": "dirigirse", "baseInfinitive": "dirigir", "english": "to go to / make one's way toward / to address", "usageHint": "a"},
|
||||||
|
{"infinitive": "hacerse", "baseInfinitive": "hacer", "english": "to become / to pretend"},
|
||||||
|
{"infinitive": "divertirse", "baseInfinitive": "divertir", "english": "to have fun"},
|
||||||
|
{"infinitive": "dormirse", "baseInfinitive": "dormir", "english": "to fall asleep / to oversleep"},
|
||||||
|
{"infinitive": "ducharse", "baseInfinitive": "duchar", "english": "to shower"},
|
||||||
|
{"infinitive": "echarse", "baseInfinitive": "echar", "english": "to begin (usually suddenly) to do something / to break into", "usageHint": "a"},
|
||||||
|
{"infinitive": "enamorarse", "baseInfinitive": "enamorar", "english": "to fall in love with", "usageHint": "de"},
|
||||||
|
{"infinitive": "encargarse", "baseInfinitive": "encargar", "english": "to take charge of or be responsible for", "usageHint": "de"},
|
||||||
|
{"infinitive": "encogerse", "baseInfinitive": "encoger", "english": "to shrug (shoulders)", "usageHint": "(de hombros)"},
|
||||||
|
{"infinitive": "encontrarse", "baseInfinitive": "encontrar", "english": "to meet with / to run into someone", "usageHint": "(con)"},
|
||||||
|
{"infinitive": "enfermarse", "baseInfinitive": "enfermar", "english": "to get sick"},
|
||||||
|
{"infinitive": "enojarse", "baseInfinitive": "enojar", "english": "to get or become angry"},
|
||||||
|
{"infinitive": "enterarse", "baseInfinitive": "enterar", "english": "to find out, to realize", "usageHint": "de"},
|
||||||
|
{"infinitive": "exponerse", "baseInfinitive": "exponer", "english": "to expose oneself to or run the risk of", "usageHint": "a"},
|
||||||
|
{"infinitive": "fijarse", "baseInfinitive": "fijar", "english": "to pay attention to / to take a look"},
|
||||||
|
{"infinitive": "jugarse", "baseInfinitive": "jugar", "english": "to risk"},
|
||||||
|
{"infinitive": "lastimarse", "baseInfinitive": "lastimar", "english": "to get hurt or hurt oneself"},
|
||||||
|
{"infinitive": "lavarse", "baseInfinitive": "lavar", "english": "to wash (a body part)"},
|
||||||
|
{"infinitive": "levantarse", "baseInfinitive": "levantar", "english": "to get up"},
|
||||||
|
{"infinitive": "maquillarse", "baseInfinitive": "maquillar", "english": "to put makeup on"},
|
||||||
|
{"infinitive": "meterse", "baseInfinitive": "meter", "english": "to get into / to pick on / to pick a fight with", "usageHint": "en / con"},
|
||||||
|
{"infinitive": "motivarse", "baseInfinitive": "motivar", "english": "to become or get motivated to"},
|
||||||
|
{"infinitive": "moverse", "baseInfinitive": "mover", "english": "to move oneself"},
|
||||||
|
{"infinitive": "mudarse", "baseInfinitive": "mudar", "english": "to move (change residence)"},
|
||||||
|
{"infinitive": "negarse", "baseInfinitive": "negar", "english": "to refuse to", "usageHint": "a"},
|
||||||
|
{"infinitive": "obsesionarse", "baseInfinitive": "obsesionar", "english": "to be or get obsessed with", "usageHint": "con"},
|
||||||
|
{"infinitive": "ocuparse", "baseInfinitive": "ocupar", "english": "to look after", "usageHint": "de"},
|
||||||
|
{"infinitive": "olvidarse", "baseInfinitive": "olvidar", "english": "to forget", "usageHint": "de"},
|
||||||
|
{"infinitive": "parecerse", "baseInfinitive": "parecer", "english": "to look like someone or something", "usageHint": "a"},
|
||||||
|
{"infinitive": "peinarse", "baseInfinitive": "peinar", "english": "to comb your hair"},
|
||||||
|
{"infinitive": "ponerse", "baseInfinitive": "poner", "english": "to put on (clothing) / to get or become"},
|
||||||
|
{"infinitive": "ponerse", "baseInfinitive": "poner", "english": "to come to an agreement with someone", "usageHint": "de acuerdo"},
|
||||||
|
{"infinitive": "preocuparse", "baseInfinitive": "preocupar", "english": "to worry about", "usageHint": "por"},
|
||||||
|
{"infinitive": "prepararse", "baseInfinitive": "preparar", "english": "to prepare to"},
|
||||||
|
{"infinitive": "probarse", "baseInfinitive": "probar", "english": "to try on"},
|
||||||
|
{"infinitive": "quebrarse", "baseInfinitive": "quebrar", "english": "to break (an arm, leg, etc.)"},
|
||||||
|
{"infinitive": "quejarse", "baseInfinitive": "quejar", "english": "to complain about", "usageHint": "de"},
|
||||||
|
{"infinitive": "quedarse", "baseInfinitive": "quedar", "english": "to remain / to stay"},
|
||||||
|
{"infinitive": "quemarse", "baseInfinitive": "quemar", "english": "to burn oneself / one's body"},
|
||||||
|
{"infinitive": "quitarse", "baseInfinitive": "quitar", "english": "to take off (clothing, etc.)"},
|
||||||
|
{"infinitive": "reírse", "baseInfinitive": "reír", "english": "to laugh about", "usageHint": "de"},
|
||||||
|
{"infinitive": "resignarse", "baseInfinitive": "resignar", "english": "to resign oneself to", "usageHint": "a"},
|
||||||
|
{"infinitive": "romperse", "baseInfinitive": "romper", "english": "to break (an arm, leg, etc.)"},
|
||||||
|
{"infinitive": "secarse", "baseInfinitive": "secar", "english": "to dry (a body part)"},
|
||||||
|
{"infinitive": "sentarse", "baseInfinitive": "sentar", "english": "to sit down"},
|
||||||
|
{"infinitive": "sentirse", "baseInfinitive": "sentir", "english": "to feel"},
|
||||||
|
{"infinitive": "servirse", "baseInfinitive": "servir", "english": "to help oneself to (food)"},
|
||||||
|
{"infinitive": "suicidarse", "baseInfinitive": "suicidar", "english": "to commit suicide"},
|
||||||
|
{"infinitive": "tratarse", "baseInfinitive": "tratar", "english": "to be about", "usageHint": "de"},
|
||||||
|
{"infinitive": "vestirse", "baseInfinitive": "vestir", "english": "to get dressed"},
|
||||||
|
{"infinitive": "marearse", "baseInfinitive": "marear", "english": "to get sick, to get dizzy"},
|
||||||
|
{"infinitive": "irse", "baseInfinitive": "ir", "english": "to leave"},
|
||||||
|
{"infinitive": "imaginarse", "baseInfinitive": "imaginar", "english": "to imagine"},
|
||||||
|
{"infinitive": "preguntarse", "baseInfinitive": "preguntar", "english": "to wonder"},
|
||||||
|
{"infinitive": "llamarse", "baseInfinitive": "llamar", "english": "to be called"},
|
||||||
|
{"infinitive": "verse", "baseInfinitive": "ver", "english": "to look or appear"},
|
||||||
|
{"infinitive": "distraerse", "baseInfinitive": "distraer", "english": "to get distracted"},
|
||||||
|
{"infinitive": "concentrarse", "baseInfinitive": "concentrar", "english": "to focus"},
|
||||||
|
{"infinitive": "rendirse", "baseInfinitive": "rendir", "english": "to give up"},
|
||||||
|
{"infinitive": "relajarse", "baseInfinitive": "relajar", "english": "to relax"},
|
||||||
|
{"infinitive": "merecerse", "baseInfinitive": "merecer", "english": "to deserve"},
|
||||||
|
{"infinitive": "suponerse", "baseInfinitive": "suponer", "english": "to suppose"},
|
||||||
|
{"infinitive": "conectarse", "baseInfinitive": "conectar", "english": "to connect"},
|
||||||
|
{"infinitive": "destacarse", "baseInfinitive": "destacar", "english": "to stand out"},
|
||||||
|
{"infinitive": "recibirse", "baseInfinitive": "recibir", "english": "to graduate"},
|
||||||
|
{"infinitive": "graduarse", "baseInfinitive": "graduar", "english": "to graduate"},
|
||||||
|
{"infinitive": "perderse", "baseInfinitive": "perder", "english": "to get lost"},
|
||||||
|
{"infinitive": "cambiarse", "baseInfinitive": "cambiar", "english": "to change (clothing)", "usageHint": "(de ropa)"},
|
||||||
|
{"infinitive": "adaptarse", "baseInfinitive": "adaptar", "english": "to adapt, to adjust", "usageHint": "a"},
|
||||||
|
{"infinitive": "salirse", "baseInfinitive": "salir", "english": "to get away with", "usageHint": "con (la suya)"},
|
||||||
|
{"infinitive": "subirse", "baseInfinitive": "subir", "english": "to get on (the bus, etc.)", "usageHint": "a"},
|
||||||
|
{"infinitive": "tranquilizarse", "baseInfinitive": "tranquilizar", "english": "to relax"},
|
||||||
|
{"infinitive": "equivocarse", "baseInfinitive": "equivocar", "english": "to get something wrong / confused"},
|
||||||
|
{"infinitive": "confundirse", "baseInfinitive": "confundir", "english": "to get something wrong / confused"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// A single entry from the curated "100 most common reflexive verbs" list
|
||||||
|
/// (Gitea issue #28). Sourced from spanishwithdaniel.com.
|
||||||
|
///
|
||||||
|
/// `baseInfinitive` is the stem without the reflexive "-se" suffix, used to
|
||||||
|
/// match this entry to the app's Verb records (which store bare infinitives).
|
||||||
|
/// `usageHint` captures trailing prepositions or set-phrase completions — e.g.,
|
||||||
|
/// "a" for `acercarse a`, "de acuerdo" for `ponerse de acuerdo`. Nil when the
|
||||||
|
/// reflexive form has no commonly paired preposition.
|
||||||
|
public struct ReflexiveVerb: Codable, Hashable, Sendable {
|
||||||
|
public let infinitive: String
|
||||||
|
public let baseInfinitive: String
|
||||||
|
public let english: String
|
||||||
|
public let usageHint: String?
|
||||||
|
|
||||||
|
public init(infinitive: String, baseInfinitive: String, english: String, usageHint: String? = nil) {
|
||||||
|
self.infinitive = infinitive
|
||||||
|
self.baseInfinitive = baseInfinitive
|
||||||
|
self.english = english
|
||||||
|
self.usageHint = usageHint
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,8 @@ targets:
|
|||||||
buildPhase: resources
|
buildPhase: resources
|
||||||
- path: Conjuga/course_data.json
|
- path: Conjuga/course_data.json
|
||||||
buildPhase: resources
|
buildPhase: resources
|
||||||
|
- path: Conjuga/reflexive_verbs.json
|
||||||
|
buildPhase: resources
|
||||||
info:
|
info:
|
||||||
path: Conjuga/Info.plist
|
path: Conjuga/Info.plist
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
Reference in New Issue
Block a user