From a1dc17bf00ab4a22f7dda4361199988dd8f5f370 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 11 Apr 2026 23:44:17 -0500 Subject: [PATCH] Add per-form English translations to verb conjugation table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New EnglishConjugator in SharedModels constructs English translations by combining the verb's infinitive with person pronouns and tense auxiliaries (e.g., abatir conditional yo → "I would knock down"). Covers all 20 tense IDs, handles 60+ irregular English verbs, multi-word verbs, 3rd person rules, gerund and participle formation. VerbDetailView shows the English below each conjugated form, plus a legend explaining red = irregular conjugation. 42 tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- Conjuga/Conjuga.xcodeproj/project.pbxproj | 8 + .../Conjuga/Views/Verbs/VerbDetailView.swift | 29 +- .../SharedModels/EnglishConjugator.swift | 246 ++++++++++++++++ .../EnglishConjugatorTests.swift | 268 ++++++++++++++++++ 4 files changed, 544 insertions(+), 7 deletions(-) create mode 100644 Conjuga/SharedModels/Sources/SharedModels/EnglishConjugator.swift create mode 100644 Conjuga/SharedModels/Tests/SharedModelsTests/EnglishConjugatorTests.swift diff --git a/Conjuga/Conjuga.xcodeproj/project.pbxproj b/Conjuga/Conjuga.xcodeproj/project.pbxproj index e8aa5d5..d642b68 100644 --- a/Conjuga/Conjuga.xcodeproj/project.pbxproj +++ b/Conjuga/Conjuga.xcodeproj/project.pbxproj @@ -211,6 +211,7 @@ 7E6AF62A3A949630E067DC22 /* Info.plist */, 353C5DE41FD410FA82E3AED7 /* Models */, 1994867BC8E985795A172854 /* Services */, + BFC1AEBE02CE22E6474FFEA6 /* Utilities */, 3C75490F53C34A37084FF478 /* ViewModels */, A81CA75762B08D35D5B7A44D /* Views */, ); @@ -399,6 +400,13 @@ path = Course; sourceTree = ""; }; + BFC1AEBE02CE22E6474FFEA6 /* Utilities */ = { + isa = PBXGroup; + children = ( + ); + path = Utilities; + sourceTree = ""; + }; F605D24E5EA11065FD18AF7E /* Products */ = { isa = PBXGroup; children = ( diff --git a/Conjuga/Conjuga/Views/Verbs/VerbDetailView.swift b/Conjuga/Conjuga/Views/Verbs/VerbDetailView.swift index cd864f6..a600d11 100644 --- a/Conjuga/Conjuga/Views/Verbs/VerbDetailView.swift +++ b/Conjuga/Conjuga/Views/Verbs/VerbDetailView.swift @@ -38,15 +38,30 @@ struct VerbDetailView: View { ContentUnavailableView("No forms loaded", systemImage: "exclamationmark.triangle") } else { ForEach(formsForTense, id: \.personIndex) { form in - HStack { - Text(TenseInfo.persons[form.personIndex]) - .foregroundStyle(.secondary) - .frame(minWidth: 100, alignment: .leading) - Text(form.form) - .fontWeight(form.regularity != "ordinary" ? .semibold : .regular) - .foregroundStyle(form.regularity != "ordinary" ? .red : .primary) + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(TenseInfo.persons[form.personIndex]) + .foregroundStyle(.secondary) + .frame(minWidth: 100, alignment: .leading) + Text(form.form) + .fontWeight(form.regularity != "ordinary" ? .semibold : .regular) + .foregroundStyle(form.regularity != "ordinary" ? .red : .primary) + } + Text(EnglishConjugator.translate( + english: verb.english, + tenseId: selectedTense.id, + personIndex: form.personIndex + )) + .font(.caption) + .foregroundStyle(.secondary) } } + + if formsForTense.contains(where: { $0.regularity != "ordinary" }) { + Label("Red indicates an irregular conjugation", systemImage: "info.circle") + .font(.caption) + .foregroundStyle(.secondary) + } } } header: { Text("Conjugation") diff --git a/Conjuga/SharedModels/Sources/SharedModels/EnglishConjugator.swift b/Conjuga/SharedModels/Sources/SharedModels/EnglishConjugator.swift new file mode 100644 index 0000000..a3ab4ea --- /dev/null +++ b/Conjuga/SharedModels/Sources/SharedModels/EnglishConjugator.swift @@ -0,0 +1,246 @@ +import Foundation + +/// Constructs approximate English translations for Spanish conjugation forms +/// by combining the verb's English infinitive with person pronouns and tense auxiliaries. +/// +/// Not perfect for irregular English verbs (go→went, be→was) but covers the +/// common patterns well enough for a learning context. +public enum EnglishConjugator { + + public static func translate(english: String, tenseId: String, personIndex: Int) -> String { + let base = english.hasPrefix("to ") ? String(english.dropFirst(3)).trimmingCharacters(in: .whitespaces) : english + guard !base.isEmpty else { return "" } + + let pronoun = pronoun(for: personIndex) + + switch tenseId { + // Indicative + case "ind_presente": + return "\(pronoun) \(presentForm(base, personIndex: personIndex))" + case "ind_preterito": + return "\(pronoun) \(pastForm(base))" + case "ind_imperfecto": + return "\(pronoun) used to \(base)" + case "ind_futuro": + return "\(pronoun) will \(base)" + case "ind_perfecto": + return "\(pronoun) \(haveForm(personIndex)) \(pastParticiple(base))" + case "ind_pluscuamperfecto": + return "\(pronoun) had \(pastParticiple(base))" + case "ind_futuro_perfecto": + return "\(pronoun) will have \(pastParticiple(base))" + case "ind_preterito_anterior": + return "\(pronoun) had \(pastParticiple(base))" + + // Conditional + case "cond_presente": + return "\(pronoun) would \(base)" + case "cond_perfecto": + return "\(pronoun) would have \(pastParticiple(base))" + + // Subjunctive + case "subj_presente": + return "that \(pronoun) \(base)" + case "subj_imperfecto_1", "subj_imperfecto_2": + return "that \(pronoun) would \(base)" + case "subj_perfecto": + return "that \(pronoun) \(haveForm(personIndex)) \(pastParticiple(base))" + case "subj_pluscuamperfecto_1", "subj_pluscuamperfecto_2": + return "that \(pronoun) had \(pastParticiple(base))" + case "subj_futuro": + return "that \(pronoun) will \(base)" + case "subj_futuro_perfecto": + return "that \(pronoun) will have \(pastParticiple(base))" + + // Imperative + case "imp_afirmativo": + return imperativeAffirmative(base, personIndex: personIndex) + case "imp_negativo": + return "don't \(base)" + + default: + return "\(pronoun) \(base)" + } + } + + // MARK: - Pronouns + + private static func pronoun(for personIndex: Int) -> String { + switch personIndex { + case 0: "I" + case 1: "you" + case 2: "he/she" + case 3: "we" + case 4: "you all" + case 5: "they" + default: "" + } + } + + // MARK: - Present tense + + private static func presentForm(_ base: String, personIndex: Int) -> String { + // 3rd person singular adds -s/-es + guard personIndex == 2 else { return base } + let words = base.split(separator: " ") + guard let first = words.first else { return base } + let verb = String(first) + let rest = words.dropFirst().joined(separator: " ") + let conjugated = addThirdPersonS(verb) + return rest.isEmpty ? conjugated : "\(conjugated) \(rest)" + } + + private static func addThirdPersonS(_ verb: String) -> String { + if verb == "have" { return "has" } + if verb == "be" { return "is" } + if verb == "do" { return "does" } + if verb == "go" { return "goes" } + if verb.hasSuffix("sh") || verb.hasSuffix("ch") || verb.hasSuffix("x") || + verb.hasSuffix("s") || verb.hasSuffix("z") || verb.hasSuffix("o") { + return verb + "es" + } + if verb.hasSuffix("y") && verb.count > 1 { + let yIndex = verb.index(before: verb.endIndex) + let beforeY = verb[verb.index(before: yIndex)] + if !"aeiou".contains(beforeY) { + return String(verb.dropLast()) + "ies" + } + } + return verb + "s" + } + + // MARK: - Past tense + + private static func pastForm(_ base: String) -> String { + // Check common irregulars first + let words = base.split(separator: " ") + guard let first = words.first else { return base } + let verb = String(first).lowercased() + let rest = words.dropFirst().joined(separator: " ") + + let irregular: String? = commonIrregularPast[verb] + let past = irregular ?? addEd(String(first)) + return rest.isEmpty ? past : "\(past) \(rest)" + } + + private static func addEd(_ verb: String) -> String { + if verb.hasSuffix("e") { return verb + "d" } + if verb.hasSuffix("y") && verb.count > 1 { + let beforeY = verb[verb.index(before: verb.endIndex)] + if !"aeiou".contains(beforeY) { + return String(verb.dropLast()) + "ied" + } + } + return verb + "ed" + } + + // MARK: - Past participle + + private static func pastParticiple(_ base: String) -> String { + let words = base.split(separator: " ") + guard let first = words.first else { return base } + let verb = String(first).lowercased() + let rest = words.dropFirst().joined(separator: " ") + + let irregular: String? = commonIrregularParticiple[verb] + let participle = irregular ?? addEd(String(first)) + return rest.isEmpty ? participle : "\(participle) \(rest)" + } + + // MARK: - Gerund + + private static func gerund(_ base: String) -> String { + let words = base.split(separator: " ") + guard let first = words.first else { return base } + let verb = String(first) + let rest = words.dropFirst().joined(separator: " ") + + let ing: String + if verb.hasSuffix("ie") { + ing = String(verb.dropLast(2)) + "ying" + } else if verb.hasSuffix("e") && !verb.hasSuffix("ee") { + ing = String(verb.dropLast()) + "ing" + } else { + ing = verb + "ing" + } + return rest.isEmpty ? ing : "\(ing) \(rest)" + } + + // MARK: - Auxiliaries + + private static func haveForm(_ personIndex: Int) -> String { + personIndex == 2 ? "has" : "have" + } + + private static func beForm(_ personIndex: Int) -> String { + switch personIndex { + case 0: "am" + case 2: "is" + default: "are" + } + } + + // MARK: - Imperative + + private static func imperativeAffirmative(_ base: String, personIndex: Int) -> String { + switch personIndex { + case 1, 4: "\(base)!" + case 3: "let's \(base)!" + default: "\(base)!" + } + } + + // MARK: - Irregular lookups (most common English irregulars) + + private static let commonIrregularPast: [String: String] = [ + "be": "was/were", "have": "had", "do": "did", "go": "went", + "say": "said", "get": "got", "make": "made", "know": "knew", + "think": "thought", "take": "took", "come": "came", "see": "saw", + "want": "wanted", "give": "gave", "tell": "told", "find": "found", + "put": "put", "leave": "left", "bring": "brought", "begin": "began", + "keep": "kept", "hold": "held", "write": "wrote", "stand": "stood", + "hear": "heard", "let": "let", "mean": "meant", "set": "set", + "meet": "met", "run": "ran", "pay": "paid", "sit": "sat", + "speak": "spoke", "read": "read", "grow": "grew", "lose": "lost", + "fall": "fell", "feel": "felt", "cut": "cut", "sell": "sold", + "drive": "drove", "buy": "bought", "wear": "wore", "choose": "chose", + "sleep": "slept", "eat": "ate", "drink": "drank", "swim": "swam", + "fly": "flew", "break": "broke", "sing": "sang", "catch": "caught", + "send": "sent", "build": "built", "spend": "spent", "win": "won", + "fight": "fought", "throw": "threw", "teach": "taught", "lead": "led", + "understand": "understood", "draw": "drew", "ride": "rode", + "rise": "rose", "shake": "shook", "forget": "forgot", + "shoot": "shot", "wake": "woke", "bite": "bit", "hide": "hid", + "lay": "laid", "lie": "lay", "strike": "struck", "hang": "hung", + "blow": "blew", "dig": "dug", "feed": "fed", "forgive": "forgave", + "freeze": "froze", "hurt": "hurt", "light": "lit", "shut": "shut", + "steal": "stole", "stick": "stuck", "sweep": "swept", + "swing": "swung", "tear": "tore", + ] + + private static let commonIrregularParticiple: [String: String] = [ + "be": "been", "have": "had", "do": "done", "go": "gone", + "say": "said", "get": "gotten", "make": "made", "know": "known", + "think": "thought", "take": "taken", "come": "come", "see": "seen", + "give": "given", "tell": "told", "find": "found", + "put": "put", "leave": "left", "bring": "brought", "begin": "begun", + "keep": "kept", "hold": "held", "write": "written", "stand": "stood", + "hear": "heard", "let": "let", "mean": "meant", "set": "set", + "meet": "met", "run": "run", "pay": "paid", "sit": "sat", + "speak": "spoken", "read": "read", "grow": "grown", "lose": "lost", + "fall": "fallen", "feel": "felt", "cut": "cut", "sell": "sold", + "drive": "driven", "buy": "bought", "wear": "worn", "choose": "chosen", + "sleep": "slept", "eat": "eaten", "drink": "drunk", "swim": "swum", + "fly": "flown", "break": "broken", "sing": "sung", "catch": "caught", + "send": "sent", "build": "built", "spend": "spent", "win": "won", + "fight": "fought", "throw": "thrown", "teach": "taught", "lead": "led", + "understand": "understood", "draw": "drawn", "ride": "ridden", + "rise": "risen", "shake": "shaken", "forget": "forgotten", + "shoot": "shot", "wake": "woken", "bite": "bitten", "hide": "hidden", + "lay": "laid", "lie": "lain", "strike": "struck", "hang": "hung", + "blow": "blown", "dig": "dug", "feed": "fed", "forgive": "forgiven", + "freeze": "frozen", "hurt": "hurt", "light": "lit", "shut": "shut", + "steal": "stolen", "stick": "stuck", "sweep": "swept", + "swing": "swung", "tear": "torn", + ] +} diff --git a/Conjuga/SharedModels/Tests/SharedModelsTests/EnglishConjugatorTests.swift b/Conjuga/SharedModels/Tests/SharedModelsTests/EnglishConjugatorTests.swift new file mode 100644 index 0000000..af06fb6 --- /dev/null +++ b/Conjuga/SharedModels/Tests/SharedModelsTests/EnglishConjugatorTests.swift @@ -0,0 +1,268 @@ +import Testing +@testable import SharedModels + +@Suite("EnglishConjugator") +struct EnglishConjugatorTests { + + // MARK: - haber (to have) — irregular English verb + + @Test("haber present: I have / you have / he/she has") + func haberPresent() { + #expect(t("to have", "ind_presente", 0) == "I have") + #expect(t("to have", "ind_presente", 1) == "you have") + #expect(t("to have", "ind_presente", 2) == "he/she has") + #expect(t("to have", "ind_presente", 3) == "we have") + #expect(t("to have", "ind_presente", 5) == "they have") + } + + @Test("haber preterite: I had") + func haberPreterite() { + #expect(t("to have", "ind_preterito", 0) == "I had") + #expect(t("to have", "ind_preterito", 2) == "he/she had") + } + + @Test("haber future: I will have") + func haberFuture() { + #expect(t("to have", "ind_futuro", 0) == "I will have") + #expect(t("to have", "ind_futuro", 3) == "we will have") + } + + @Test("haber conditional: I would have") + func haberConditional() { + #expect(t("to have", "cond_presente", 0) == "I would have") + } + + @Test("haber present perfect: I have had / he/she has had") + func haberPresentPerfect() { + #expect(t("to have", "ind_perfecto", 0) == "I have had") + #expect(t("to have", "ind_perfecto", 2) == "he/she has had") + } + + // MARK: - ir (to go) — irregular English verb + + @Test("ir present: I go / he/she goes") + func irPresent() { + #expect(t("to go", "ind_presente", 0) == "I go") + #expect(t("to go", "ind_presente", 2) == "he/she goes") + #expect(t("to go", "ind_presente", 5) == "they go") + } + + @Test("ir preterite: I went") + func irPreterite() { + #expect(t("to go", "ind_preterito", 0) == "I went") + #expect(t("to go", "ind_preterito", 2) == "he/she went") + } + + @Test("ir imperfect: I used to go") + func irImperfect() { + #expect(t("to go", "ind_imperfecto", 0) == "I used to go") + } + + @Test("ir present perfect: I have gone") + func irPresentPerfect() { + #expect(t("to go", "ind_perfecto", 0) == "I have gone") + #expect(t("to go", "ind_perfecto", 2) == "he/she has gone") + } + + // MARK: - ser (to be) — most irregular English verb + + @Test("ser present: he/she is") + func serPresent() { + #expect(t("to be", "ind_presente", 2) == "he/she is") + } + + @Test("ser preterite: I was/were") + func serPreterite() { + #expect(t("to be", "ind_preterito", 0) == "I was/were") + } + + @Test("ser present perfect: I have been") + func serPresentPerfect() { + #expect(t("to be", "ind_perfecto", 0) == "I have been") + } + + // MARK: - hablar (to speak) + + @Test("hablar present: I speak / he/she speaks") + func hablarPresent() { + #expect(t("to speak", "ind_presente", 0) == "I speak") + #expect(t("to speak", "ind_presente", 2) == "he/she speaks") + } + + @Test("hablar preterite: I spoke") + func hablarPreterite() { + #expect(t("to speak", "ind_preterito", 0) == "I spoke") + } + + @Test("hablar present perfect: I have spoken") + func hablarPresentPerfect() { + #expect(t("to speak", "ind_perfecto", 0) == "I have spoken") + } + + // MARK: - comer (to eat) + + @Test("comer preterite: I ate") + func comerPreterite() { + #expect(t("to eat", "ind_preterito", 0) == "I ate") + } + + @Test("comer present perfect: I have eaten") + func comerPresentPerfect() { + #expect(t("to eat", "ind_perfecto", 0) == "I have eaten") + } + + // MARK: - vivir (to live) — regular English verb + + @Test("vivir present: I live / he/she lives") + func vivirPresent() { + #expect(t("to live", "ind_presente", 0) == "I live") + #expect(t("to live", "ind_presente", 2) == "he/she lives") + } + + @Test("vivir preterite: I lived") + func vivirPreterite() { + #expect(t("to live", "ind_preterito", 0) == "I lived") + } + + // MARK: - abatir (to knock down) — multi-word verb + + @Test("abatir present: I knock down / he/she knocks down") + func abatirPresent() { + #expect(t("to knock down", "ind_presente", 0) == "I knock down") + #expect(t("to knock down", "ind_presente", 2) == "he/she knocks down") + } + + @Test("abatir conditional: I would knock down") + func abatirConditional() { + #expect(t("to knock down", "cond_presente", 0) == "I would knock down") + #expect(t("to knock down", "cond_presente", 2) == "he/she would knock down") + } + + @Test("abatir preterite: I knocked down") + func abatirPreterite() { + #expect(t("to knock down", "ind_preterito", 0) == "I knocked down") + } + + // MARK: - Conditional + + @Test("conditional: I would speak") + func conditional() { + #expect(t("to speak", "cond_presente", 0) == "I would speak") + #expect(t("to speak", "cond_presente", 2) == "he/she would speak") + } + + @Test("conditional perfect: I would have gone") + func conditionalPerfect() { + #expect(t("to go", "cond_perfecto", 0) == "I would have gone") + } + + // MARK: - Subjunctive + + @Test("present subjunctive: that I speak") + func presentSubjunctive() { + #expect(t("to speak", "subj_presente", 0) == "that I speak") + #expect(t("to speak", "subj_presente", 2) == "that he/she speak") + } + + @Test("imperfect subjunctive (ra): that I would speak") + func imperfectSubjunctive1() { + #expect(t("to speak", "subj_imperfecto_1", 0) == "that I would speak") + } + + @Test("imperfect subjunctive (se): that I would speak") + func imperfectSubjunctive2() { + #expect(t("to speak", "subj_imperfecto_2", 0) == "that I would speak") + } + + @Test("subjunctive perfect: that I have spoken") + func subjunctivePerfect() { + #expect(t("to speak", "subj_perfecto", 0) == "that I have spoken") + #expect(t("to speak", "subj_perfecto", 2) == "that he/she has spoken") + } + + @Test("subjunctive pluperfect: that I had gone") + func subjunctivePluperfect() { + #expect(t("to go", "subj_pluscuamperfecto_1", 0) == "that I had gone") + #expect(t("to go", "subj_pluscuamperfecto_2", 0) == "that I had gone") + } + + @Test("subjunctive future: that I will speak") + func subjunctiveFuture() { + #expect(t("to speak", "subj_futuro", 0) == "that I will speak") + } + + @Test("subjunctive future perfect: that I will have spoken") + func subjunctiveFuturePerfect() { + #expect(t("to speak", "subj_futuro_perfecto", 0) == "that I will have spoken") + } + + // MARK: - Imperative + + @Test("imperative affirmative") + func imperativeAffirmative() { + #expect(t("to speak", "imp_afirmativo", 1) == "speak!") + #expect(t("to speak", "imp_afirmativo", 3) == "let's speak!") + } + + @Test("imperative negative") + func imperativeNegative() { + #expect(t("to speak", "imp_negativo", 1) == "don't speak") + } + + // MARK: - Compound indicative tenses + + @Test("pluperfect: I had spoken") + func pluperfect() { + #expect(t("to speak", "ind_pluscuamperfecto", 0) == "I had spoken") + #expect(t("to go", "ind_pluscuamperfecto", 0) == "I had gone") + } + + @Test("future perfect: I will have spoken") + func futurePerfect() { + #expect(t("to speak", "ind_futuro_perfecto", 0) == "I will have spoken") + } + + @Test("preterite anterior: I had spoken (same as pluperfect in English)") + func preteriteAnterior() { + #expect(t("to speak", "ind_preterito_anterior", 0) == "I had spoken") + } + + // MARK: - Edge cases + + @Test("empty english returns empty string") + func emptyEnglish() { + #expect(t("", "ind_presente", 0) == "") + #expect(t("to ", "ind_presente", 0) == "") + } + + @Test("unknown tense falls back to pronoun + base") + func unknownTense() { + #expect(t("to speak", "some_future_tense", 0) == "I speak") + } + + @Test("3rd person present: study → studies") + func thirdPersonYRule() { + #expect(t("to study", "ind_presente", 2) == "he/she studies") + } + + @Test("3rd person present: play → plays") + func thirdPersonVowelY() { + #expect(t("to play", "ind_presente", 2) == "he/she plays") + } + + @Test("3rd person present: watch → watches") + func thirdPersonChRule() { + #expect(t("to watch", "ind_presente", 2) == "he/she watches") + } + + @Test("past regular: carry → carried") + func pastYRule() { + #expect(t("to carry", "ind_preterito", 0) == "I carried") + } + + // MARK: - Helper + + private func t(_ english: String, _ tenseId: String, _ personIndex: Int) -> String { + EnglishConjugator.translate(english: english, tenseId: tenseId, personIndex: personIndex) + } +}