From 82543a38aa302285ba37c72551810c7364444763 Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Wed, 7 Oct 2026 04:00:53 +0530 Subject: [PATCH 1/7] feat: update data contract fields to use nested nouns and verbs (#714) - Update data contracts in assets with new nouns and verbs structure from Scribe-Data - Update DataContract data classes to support nouns, number pairs, and verbs - Update GenderDataManager, PluralFormsManager, ConjugateDataManager, KeyboardDataHandler, and ConjugationSelectionScreen - Update existing plural forms test and add ContractDataLoaderTest - Update CHANGELOG.md --- CHANGELOG.md | 1 + .../helpers/data/PluralFormsManagerTest.kt | 6 +- .../be/scri/helpers/KeyboardDataHandler.kt | 9 +- app/src/main/assets/data-contracts/de.yaml | 209 +++++------ app/src/main/assets/data-contracts/en.yaml | 336 +++++++++--------- app/src/main/assets/data-contracts/es.yaml | 231 ++++++------ app/src/main/assets/data-contracts/fr.yaml | 252 ++++++------- app/src/main/assets/data-contracts/it.yaml | 202 ++++++----- app/src/main/assets/data-contracts/pt.yaml | 252 ++++++------- app/src/main/assets/data-contracts/ru.yaml | 147 ++++---- app/src/main/assets/data-contracts/sv.yaml | 132 +++---- .../scri/helpers/data/ConjugateDataManager.kt | 4 +- .../be/scri/helpers/data/GenderDataManager.kt | 23 +- .../scri/helpers/data/PluralFormsManager.kt | 9 +- .../main/java/be/scri/models/DataContract.kt | 29 +- .../ui/screens/ConjugationSelectionScreen.kt | 2 +- .../helpers/data/ContractDataLoaderTest.kt | 159 +++++++++ 17 files changed, 1146 insertions(+), 857 deletions(-) create mode 100644 app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3165e79ba..ba8785022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - The user is directed to download data in the keyboard UI if they access it with empty databases ([#581](https://github.com/scribe-org/Scribe-Android/issues/581), [#650](https://github.com/scribe-org/Scribe-Android/issues/650)). - The data download UI was created to download data for any keyboards that have been installed ([#437](https://github.com/scribe-org/Scribe-Android/issues/437), [#439](https://github.com/scribe-org/Scribe-Android/issues/439), [#513](https://github.com/scribe-org/Scribe-Android/issues/513), [#520](https://github.com/scribe-org/Scribe-Android/issues/520), [#554](https://github.com/scribe-org/Scribe-Android/issues/554)). - Network indicators for data request have been added to the application and are shown via toasts ([#651](https://github.com/scribe-org/Scribe-Android/issues/651)). +- Language data contracts were updated to use nested nouns and verbs structures ([#714](https://github.com/scribe-org/Scribe-Android/issues/714)). ### 🎨 Design diff --git a/app/src/androidTestKeyboards/kotlin/be/scri/helpers/data/PluralFormsManagerTest.kt b/app/src/androidTestKeyboards/kotlin/be/scri/helpers/data/PluralFormsManagerTest.kt index cdb5a5bbb..4e268ac14 100644 --- a/app/src/androidTestKeyboards/kotlin/be/scri/helpers/data/PluralFormsManagerTest.kt +++ b/app/src/androidTestKeyboards/kotlin/be/scri/helpers/data/PluralFormsManagerTest.kt @@ -2,6 +2,8 @@ package be.scri.helpers.data import DataContract +import Nouns +import NumberPair import android.database.Cursor import android.database.sqlite.SQLiteDatabase import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -23,10 +25,12 @@ class PluralFormsManagerTest { val mockDatabaseFileManager = mockk() val mockDatabase = mockk() val mockDataContract = mockk() + val mockNouns = mockk() val mockCursor = mockk(relaxed = true) every { mockDatabaseFileManager.getLanguageDatabase("EN") } returns mockDatabase - every { mockDataContract.numbers } returns mapOf("singular" to "plural") + every { mockDataContract.nouns } returns mockNouns + every { mockNouns.numbers } returns mapOf(1 to NumberPair("singular", "plural")) every { mockDatabase.rawQuery( "SELECT `singular`, `plural` FROM nouns WHERE `singular` = ? COLLATE NOCASE", diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt index 1086f223f..a862935a1 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardDataHandler.kt @@ -75,9 +75,12 @@ class KeyboardDataHandler { nounKeywords = dbManagers.genderManager.findGenderOfWord(languageAlias, dataContract) suggestionWords = dbManagers.suggestionManager.getSuggestions(languageAlias) val numbersColumns = - dataContract?.numbers?.let { map -> - (map.keys + map.values).distinct() - } ?: emptyList() + dataContract + ?.nouns + ?.numbers + ?.values + ?.flatMap { listOf(it.singular, it.plural) } + ?.distinct() ?: emptyList() autocompletionManager.loadWords(languageAlias, numbersColumns) caseAnnotation = dbManagers.prepositionManager.getCaseAnnotations(languageAlias) diff --git a/app/src/main/assets/data-contracts/de.yaml b/app/src/main/assets/data-contracts/de.yaml index 65bdad8e5..93f2b01a4 100644 --- a/app/src/main/assets/data-contracts/de.yaml +++ b/app/src/main/assets/data-contracts/de.yaml @@ -1,103 +1,109 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: nominativeSingular + plural: nominativePlural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Präsens + tenses: + 1: + tenseTitle: Präsens + tenseForms: + 1: + label: ich + value: indicativePresentFirstPersonSingular + 2: + label: du + value: indicativePresentSecondPersonSingular + 3: + label: er/sie/es + value: indicativePresentThirdPersonSingular + 4: + label: wir + value: indicativePresentFirstPersonPlural + 5: + label: ihr + value: indicativePresentSecondPersonPlural + 6: + label: sie/Sie + value: indicativePresentThirdPersonPlural + + 2: + sectionTitle: Preterite + tenses: + 1: + tenseTitle: Preterite + tenseForms: + 1: + label: ich + value: indicativePreteriteFirstPersonSingular + 2: + label: du + value: indicativePreteriteSecondPersonSingular + 3: + label: er/sie/es + value: indicativePreteriteThirdPersonSingular + 4: + label: wir + value: indicativePreteriteFirstPersonPlural + 5: + label: ihr + value: indicativePreteriteSecondPersonPlural + 6: + label: sie/Sie + value: indicativePreteriteThirdPersonPlural -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - nominativeSingular: nominativePlural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Präsens - tenses: - 1: - tenseTitle: Präsens - tenseForms: - 1: - label: ich - value: indicativePresentFirstPersonSingular - 2: - label: du - value: indicativePresentSecondPersonSingular - 3: - label: er/sie/es - value: indicativePresentThirdPersonSingular - 4: - label: wir - value: indicativePresentFirstPersonPlural - 5: - label: ihr - value: indicativePresentSecondPersonPlural - 6: - label: sie/Sie - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Preterite - tenses: - 1: - tenseTitle: Preterite - tenseForms: - 1: - label: ich - value: indicativePreteriteFirstPersonSingular - 2: - label: du - value: indicativePreteriteSecondPersonSingular - 3: - label: er/sie/es - value: indicativePreteriteThirdPersonSingular - 4: - label: wir - value: indicativePreteriteFirstPersonPlural - 5: - label: ihr - value: indicativePreteriteSecondPersonPlural - 6: - label: sie/Sie - value: indicativePreteriteThirdPersonPlural - - 3: - sectionTitle: Perfekt - tenses: - 1: - tenseTitle: Perfekt - tenseForms: - 1: - label: ich - value: "[indicativePresentFirstPersonSingular auxiliaryVerb] pastParticiple" - 2: - label: du - value: "[indicativePresentSecondPersonSingular auxiliaryVerb] pastParticiple" - 3: - label: er/sie/es - value: "[indicativePresentThirdPersonSingular auxiliaryVerb] pastParticiple" - 4: - label: wir - value: "[indicativePresentFirstPersonPlural auxiliaryVerb] pastParticiple" - 5: - label: ihr - value: "[indicativePresentSecondPersonPlural auxiliaryVerb] pastParticiple" - 6: - label: sie/Sie - value: "[indicativePresentThirdPersonPlural auxiliaryVerb] pastParticiple" - -# MARK: Translate + 3: + sectionTitle: Perfekt + tenses: + 1: + tenseTitle: Perfekt + tenseForms: + 1: + label: ich + value: "{indicativePresentFirstPersonSingular auxiliaryVerb} pastParticiple" + 2: + label: du + value: "{indicativePresentSecondPersonSingular auxiliaryVerb} pastParticiple" + 3: + label: er/sie/es + value: "{indicativePresentThirdPersonSingular auxiliaryVerb} pastParticiple" + 4: + label: wir + value: "{indicativePresentFirstPersonPlural auxiliaryVerb} pastParticiple" + 5: + label: ihr + value: "{indicativePresentSecondPersonPlural auxiliaryVerb} pastParticiple" + 6: + label: sie/Sie + value: "{indicativePresentThirdPersonPlural auxiliaryVerb} pastParticiple" + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). @@ -112,7 +118,7 @@ translations: adverb: displayValue: Adverb - sectionTitle: Adjektiv Übersetzung auswählen + sectionTitle: Adverb Übersetzung auswählen article: displayValue: Artikel @@ -146,7 +152,12 @@ translations: displayValue: Verb sectionTitle: Verb Übersetzung auswählen -# MARK: Decline +# MARK: Prepositions + +prepositions: + case: grammaticalCase + +# MARK: Declensions # Map of pronoun declensions given cases. declensions: diff --git a/app/src/main/assets/data-contracts/en.yaml b/app/src/main/assets/data-contracts/en.yaml index ae217fa3c..05b069ec5 100644 --- a/app/src/main/assets/data-contracts/en.yaml +++ b/app/src/main/assets/data-contracts/en.yaml @@ -1,170 +1,176 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - singular: plural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Present - tenses: - 1: - tenseTitle: Pr. Simple - tenseForms: - 1: - label: I/you/plural - value: simplePresent - 2: - label: he/she/it - value: simplePresentThirdPersonSingular - - 2: - tenseTitle: Pr. Perfect - tenseForms: - 1: - label: I/you/plural - value: "[have] pastParticiple" - 2: - label: he/she/it - value: "[has] pastParticiple" - - 3: - tenseTitle: Pr. Continuous - tenseForms: - 1: - label: I - value: "[am] presentParticiple" - 2: - label: you/plural - value: "[are] presentParticiple" - 3: - label: he/she/it - value: "[is] presentParticiple" - - 4: - tenseTitle: Pr. Perf. Continuous - tenseForms: - 1: - label: I/you/plural - value: "[have been] presentParticiple" - 2: - label: he/she/it - value: "[has been] presentParticiple" - - 2: - sectionTitle: Past - tenses: - 1: - tenseTitle: Past Simple - tenseForms: - 1: - label: all - value: simplePast - - 2: - tenseTitle: Past Perfect - tenseForms: - 1: - label: all - value: "[had] pastParticiple" - - 3: - tenseTitle: Past Continuous - tenseForms: - 1: - label: I/he/she/it - value: "[was] presentParticiple" - 2: - label: you/plural - value: "[were] presentParticiple" - - 4: - tenseTitle: Past Perf. Continuous - tenseForms: - 1: - label: all - value: "[had been] presentParticiple" - - 3: - sectionTitle: Future - tenses: - 1: - tenseTitle: Fut. Simple - tenseForms: - 1: - label: all - value: "[will] infinitive" - - 2: - tenseTitle: Fut. Perfect - tenseForms: - 1: - label: all - value: "[will have] pastParticiple" - - 3: - tenseTitle: Fut. Continuous - tenseForms: - 1: - label: all - value: "[will be] presentParticiple" - - 4: - tenseTitle: Fut. Perf. Continuous - tenseForms: - 1: - label: all - value: "[will have been] presentParticiple" - - 4: - sectionTitle: Conditional - tenses: - 1: - tenseTitle: Cond. Simple - tenseForms: - 1: - label: all - value: "[would] infinitive" - - 2: - tenseTitle: Cond. Perfect - tenseForms: - 1: - label: all - value: "[would have] pastParticiple" - - 3: - tenseTitle: Cond. Continuous - tenseForms: - 1: - label: all - value: "[would be] presentParticiple" - - 4: - tenseTitle: Cond. Perf. Continuous - tenseForms: - 1: - label: all - value: "[would have been] presentParticiple" - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: singular + plural: plural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Present + tenses: + 1: + tenseTitle: Pr. Simple + tenseForms: + 1: + label: I/you/plural + value: simplePresent + 2: + label: he/she/it + value: simplePresentThirdPersonSingular + + 2: + tenseTitle: Pr. Perfect + tenseForms: + 1: + label: I/you/plural + value: "[have] englishPastParticiple" + 2: + label: he/she/it + value: "[has] englishPastParticiple" + + 3: + tenseTitle: Pr. Continuous + tenseForms: + 1: + label: I + value: "[am] presentParticiple" + 2: + label: you/plural + value: "[are] presentParticiple" + 3: + label: he/she/it + value: "[is] presentParticiple" + + 4: + tenseTitle: Pr. Perf. Continuous + tenseForms: + 1: + label: I/you/plural + value: "[have been] presentParticiple" + 2: + label: he/she/it + value: "[has been] presentParticiple" + + 2: + sectionTitle: Past + tenses: + 1: + tenseTitle: Past Simple + tenseForms: + 1: + label: all + value: simplePast + + 2: + tenseTitle: Past Perfect + tenseForms: + 1: + label: all + value: "[had] englishPastParticiple" + + 3: + tenseTitle: Past Continuous + tenseForms: + 1: + label: I/he/she/it + value: "[was] presentParticiple" + 2: + label: you/plural + value: "[were] presentParticiple" + + 4: + tenseTitle: Past Perf. Continuous + tenseForms: + 1: + label: all + value: "[had been] presentParticiple" + + 3: + sectionTitle: Future + tenses: + 1: + tenseTitle: Fut. Simple + tenseForms: + 1: + label: all + value: "[will] infinitive" + + 2: + tenseTitle: Fut. Perfect + tenseForms: + 1: + label: all + value: "[will have] englishPastParticiple" + + 3: + tenseTitle: Fut. Continuous + tenseForms: + 1: + label: all + value: "[will be] presentParticiple" + + 4: + tenseTitle: Fut. Perf. Continuous + tenseForms: + 1: + label: all + value: "[will have been] presentParticiple" + + 4: + sectionTitle: Conditional + tenses: + 1: + tenseTitle: Cond. Simple + tenseForms: + 1: + label: all + value: "[would] infinitive" + + 2: + tenseTitle: Cond. Perfect + tenseForms: + 1: + label: all + value: "[would have] englishPastParticiple" + + 3: + tenseTitle: Cond. Continuous + tenseForms: + 1: + label: all + value: "[would be] presentParticiple" + + 4: + tenseTitle: Cond. Perf. Continuous + tenseForms: + 1: + label: all + value: "[would have been] presentParticiple" + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). diff --git a/app/src/main/assets/data-contracts/es.yaml b/app/src/main/assets/data-contracts/es.yaml index 58d7d1deb..2362f031b 100644 --- a/app/src/main/assets/data-contracts/es.yaml +++ b/app/src/main/assets/data-contracts/es.yaml @@ -1,104 +1,137 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [] - feminines: [feminineSingular] - masculines: [masculineSingular] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - feminineSingular: femininePlural - masculineSingular: masculinePlural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Presente - tenses: - 1: - tenseTitle: Presente - tenseForms: - 1: - label: yo - value: indicativePresentFirstPersonSingular - 2: - label: tú - value: indicativePresentSecondPersonSingular - 3: - label: él/ella/Ud. - value: indicativePresentThirdPersonSingular - 4: - label: nosotros - value: indicativePresentFirstPersonPlural - 5: - label: vosotros - value: indicativePresentSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Pretérito - tenses: - 1: - tenseTitle: Pretérito - tenseForms: - 1: - label: yo - value: preteriteFirstPersonSingular - 2: - label: tú - value: preteriteSecondPersonSingular - 3: - label: él/ella/Ud. - value: preteriteThirdPersonSingular - 4: - label: nosotros - value: preteriteFirstPersonPlural - 5: - label: vosotros - value: preteriteSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: preteriteThirdPersonPlural - - 3: - sectionTitle: Imperfecto - tenses: - 1: - tenseTitle: Imperfecto - tenseForms: - 1: - label: yo - value: pastImperfectFirstPersonSingular - 2: - label: tú - value: pastImperfectSecondPersonSingular - 3: - label: él/ella/Ud. - value: pastImperfectThirdPersonSingular - 4: - label: nosotros - value: pastImperfectFirstPersonPlural - 5: - label: vosotros - value: pastImperfectSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: pastImperfectThirdPersonPlural - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [] + feminines: [feminineSingular] + masculines: [masculineSingular] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: feminineSingular + plural: femininePlural + 2: + singular: masculineSingular + plural: masculinePlural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Presente + tenses: + 1: + tenseTitle: Presente + tenseForms: + 1: + label: yo + value: indicativePresentFirstPersonSingular + 2: + label: tú + value: indicativePresentSecondPersonSingular + 3: + label: él/ella/Ud. + value: indicativePresentThirdPersonSingular + 4: + label: nosotros + value: indicativePresentFirstPersonPlural + 5: + label: vosotros + value: indicativePresentSecondPersonPlural + 6: + label: ellos/ellas/Uds. + value: indicativePresentThirdPersonPlural + + 2: + sectionTitle: Pretérito + tenses: + 1: + tenseTitle: Pretérito + tenseForms: + 1: + label: yo + value: preteriteFirstPersonSingular + 2: + label: tú + value: preteriteSecondPersonSingular + 3: + label: él/ella/Ud. + value: preteriteThirdPersonSingular + 4: + label: nosotros + value: preteriteFirstPersonPlural + 5: + label: vosotros + value: preteriteSecondPersonPlural + 6: + label: ellos/ellas/Uds. + value: preteriteThirdPersonPlural + + 3: + sectionTitle: Imperfecto + tenses: + 1: + tenseTitle: Imperfecto + tenseForms: + 1: + label: yo + value: imperfectFirstPersonSingular + 2: + label: tú + value: imperfectSecondPersonSingular + 3: + label: él/ella/Ud. + value: imperfectThirdPersonSingular + 4: + label: nosotros + value: imperfectFirstPersonPlural + 5: + label: vosotros + value: imperfectSecondPersonPlural + 6: + label: ellos/ellas/Uds. + value: imperfectThirdPersonPlural + + 4: + sectionTitle: Futuro + tenses: + 1: + tenseTitle: Futuro + tenseForms: + 1: + label: yo + value: indicativeFutureFirstPersonSingular + 2: + label: tú + value: indicativeFutureSecondPersonSingular + 3: + label: él/ella/Ud. + value: indicativeFutureThirdPersonSingular + 4: + label: nosotros + value: indicativeFutureFirstPersonPlural + 5: + label: vosotros + value: indicativeFutureSecondPersonPlural + 6: + label: ellos/ellas/Uds. + value: indicativeFutureThirdPersonPlural + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). diff --git a/app/src/main/assets/data-contracts/fr.yaml b/app/src/main/assets/data-contracts/fr.yaml index aede2e980..da14f3f50 100644 --- a/app/src/main/assets/data-contracts/fr.yaml +++ b/app/src/main/assets/data-contracts/fr.yaml @@ -1,128 +1,134 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - singular: plural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Présent - tenses: - 1: - tenseTitle: Présent - tenseForms: - 1: - label: je - value: indicativePresentFirstPersonSingular - 2: - label: tu - value: indicativePresentSecondPersonSingular - 3: - label: il/elle - value: indicativePresentThirdPersonSingular - 4: - label: nous - value: indicativePresentFirstPersonPlural - 5: - label: vous - value: indicativePresentSecondPersonPlural - 6: - label: ils/elles - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Passé simple - tenses: - 1: - tenseTitle: Passé simple - tenseForms: - 1: - label: je - value: indicativePreteriteFirstPersonSingular - 2: - label: tu - value: indicativePreteriteSecondPersonSingular - 3: - label: il/elle - value: indicativePreteriteThirdPersonSingular - 4: - label: nous - value: indicativePreteriteFirstPersonPlural - 5: - label: vous - value: indicativePreteriteSecondPersonPlural - 6: - label: ils/elles - value: indicativePreteriteThirdPersonPlural - - 3: - sectionTitle: Imparfait - tenses: - 1: - tenseTitle: Imparfait - tenseForms: - 1: - label: je - value: indicativeImperfectFirstPersonSingular - 2: - label: tu - value: indicativeImperfectSecondPersonSingular - 3: - label: il/elle - value: indicativeImperfectThirdPersonSingular - 4: - label: nous - value: indicativeImperfectFirstPersonPlural - 5: - label: vous - value: indicativeImperfectSecondPersonPlural - 6: - label: ils/elles - value: indicativeImperfectThirdPersonPlural - - 4: - sectionTitle: Futur - tenses: - 1: - tenseTitle: Futur - tenseForms: - 1: - label: je - value: indicativeSimpleFutureFirstPersonSingular - 2: - label: tu - value: indicativeSimpleFutureSecondPersonSingular - 3: - label: il/elle - value: indicativeSimpleFutureThirdPersonSingular - 4: - label: nous - value: indicativeSimpleFutureFirstPersonPlural - 5: - label: vous - value: indicativeSimpleFutureSecondPersonPlural - 6: - label: ils/elles - value: indicativeSimpleFutureThirdPersonPlural - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: singular + plural: plural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Présent + tenses: + 1: + tenseTitle: Présent + tenseForms: + 1: + label: je + value: indicativePresentFirstPersonSingular + 2: + label: tu + value: indicativePresentSecondPersonSingular + 3: + label: il/elle + value: indicativePresentThirdPersonSingular + 4: + label: nous + value: indicativePresentFirstPersonPlural + 5: + label: vous + value: indicativePresentSecondPersonPlural + 6: + label: ils/elles + value: indicativePresentThirdPersonPlural + + 2: + sectionTitle: Passé simple + tenses: + 1: + tenseTitle: Passé simple + tenseForms: + 1: + label: je + value: indicativePreteriteFirstPersonSingular + 2: + label: tu + value: indicativePreteriteSecondPersonSingular + 3: + label: il/elle + value: indicativePreteriteThirdPersonSingular + 4: + label: nous + value: indicativePreteriteFirstPersonPlural + 5: + label: vous + value: indicativePreteriteSecondPersonPlural + 6: + label: ils/elles + value: indicativePreteriteThirdPersonPlural + + 3: + sectionTitle: Imparfait + tenses: + 1: + tenseTitle: Imparfait + tenseForms: + 1: + label: je + value: indicativeImperfectFirstPersonSingular + 2: + label: tu + value: indicativeImperfectSecondPersonSingular + 3: + label: il/elle + value: indicativeImperfectThirdPersonSingular + 4: + label: nous + value: indicativeImperfectFirstPersonPlural + 5: + label: vous + value: indicativeImperfectSecondPersonPlural + 6: + label: ils/elles + value: indicativeImperfectThirdPersonPlural + + 4: + sectionTitle: Futur + tenses: + 1: + tenseTitle: Futur + tenseForms: + 1: + label: je + value: indicativeSimpleFutureFirstPersonSingular + 2: + label: tu + value: indicativeSimpleFutureSecondPersonSingular + 3: + label: il/elle + value: indicativeSimpleFutureThirdPersonSingular + 4: + label: nous + value: indicativeSimpleFutureFirstPersonPlural + 5: + label: vous + value: indicativeSimpleFutureSecondPersonPlural + 6: + label: ils/elles + value: indicativeSimpleFutureThirdPersonPlural + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). diff --git a/app/src/main/assets/data-contracts/it.yaml b/app/src/main/assets/data-contracts/it.yaml index ec2106349..8da2289f5 100644 --- a/app/src/main/assets/data-contracts/it.yaml +++ b/app/src/main/assets/data-contracts/it.yaml @@ -1,103 +1,109 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - singular: plural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Presente - tenses: - 1: - tenseTitle: Presente - tenseForms: - 1: - label: io - value: presentIndicativeFirstPersonSingular - 2: - label: tu - value: presentIndicativeSecondPersonSingular - 3: - label: lei/lui - value: presentIndicativeThirdPersonSingular - 4: - label: noi - value: presentIndicativeFirstPersonPlural - 5: - label: voi - value: presentIndicativeSecondPersonPlural - 6: - label: loro - value: presentIndicativeThirdPersonPlural - - 2: - sectionTitle: Preterito - tenses: - 1: - tenseTitle: Preterito - tenseForms: - 1: - label: io - value: preteriteFirstPersonSingular - 2: - label: tu - value: preteriteSecondPersonSingular - 3: - label: lei/lui - value: preteriteThirdPersonSingular - 4: - label: noi - value: preteriteFirstPersonPlural - 5: - label: voi - value: preteriteSecondPersonPlural - 6: - label: loro - value: preteriteThirdPersonPlural - - 3: - sectionTitle: Imperfetto - tenses: - 1: - tenseTitle: Imperfetto - tenseForms: - 1: - label: io - value: pastImperfectFirstPersonSingular - 2: - label: tu - value: pastImperfectSecondPersonSingular - 3: - label: lei/lui - value: pastImperfectThirdPersonSingular - 4: - label: noi - value: pastImperfectFirstPersonPlural - 5: - label: voi - value: pastImperfectSecondPersonPlural - 6: - label: loro - value: pastImperfectThirdPersonPlural - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: singular + plural: plural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Presente + tenses: + 1: + tenseTitle: Presente + tenseForms: + 1: + label: io + value: presentIndicativeFirstPersonSingular + 2: + label: tu + value: presentIndicativeSecondPersonSingular + 3: + label: lei/lui + value: presentIndicativeThirdPersonSingular + 4: + label: noi + value: presentIndicativeFirstPersonPlural + 5: + label: voi + value: presentIndicativeSecondPersonPlural + 6: + label: loro + value: presentIndicativeThirdPersonPlural + + 2: + sectionTitle: Preterito + tenses: + 1: + tenseTitle: Preterito + tenseForms: + 1: + label: io + value: indicativePreteriteFirstPersonSingular + 2: + label: tu + value: indicativePreteriteSecondPersonSingular + 3: + label: lei/lui + value: indicativePreteriteThirdPersonSingular + 4: + label: noi + value: indicativePreteriteFirstPersonPlural + 5: + label: voi + value: indicativePreteriteSecondPersonPlural + 6: + label: loro + value: indicativePreteriteThirdPersonPlural + + 3: + sectionTitle: Imperfetto + tenses: + 1: + tenseTitle: Imperfetto + tenseForms: + 1: + label: io + value: indicativeImperfectFirstPersonSingular + 2: + label: tu + value: indicativeImperfectSecondPersonSingular + 3: + label: lei/lui + value: indicativeImperfectThirdPersonSingular + 4: + label: noi + value: indicativeImperfectFirstPersonPlural + 5: + label: voi + value: indicativeImperfectSecondPersonPlural + 6: + label: loro + value: indicativeImperfectThirdPersonPlural + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). diff --git a/app/src/main/assets/data-contracts/pt.yaml b/app/src/main/assets/data-contracts/pt.yaml index 3665c3cd0..384e60f78 100644 --- a/app/src/main/assets/data-contracts/pt.yaml +++ b/app/src/main/assets/data-contracts/pt.yaml @@ -1,128 +1,134 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - singular: plural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Presente - tenses: - 1: - tenseTitle: Presente - tenseForms: - 1: - label: eu - value: indicativePresentFirstPersonSingular - 2: - label: tu - value: indicativePresentSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePresentThirdPersonSingular - 4: - label: nós - value: indicativePresentFirstPersonPlural - 5: - label: vós - value: indicativePresentSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Pretérito Perfeito - tenses: - 1: - tenseTitle: Pretérito Perfeito - tenseForms: - 1: - label: eu - value: indicativePastPerfectFirstPersonSingular - 2: - label: tu - value: indicativePastPerfectSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePastPerfectThirdPersonSingular - 4: - label: nós - value: indicativePastPerfectFirstPersonPlural - 5: - label: vós - value: indicativePastPerfectSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePastPerfectThirdPersonPlural - - 3: - sectionTitle: Pretérito Imperfeito - tenses: - 1: - tenseTitle: Pretérito Imperfeito - tenseForms: - 1: - label: eu - value: indicativePastImperfectFirstPersonSingular - 2: - label: tu - value: indicativePastImperfectSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePastImperfectThirdPersonSingular - 4: - label: nós - value: indicativePastImperfectFirstPersonPlural - 5: - label: vós - value: indicativePastImperfectSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePastImperfectThirdPersonPlural - - 4: - sectionTitle: Futuro Simples - tenses: - 1: - tenseTitle: Futuro Simples - tenseForms: - 1: - label: eu - value: indicativePluperfectFirstPersonSingular - 2: - label: tu - value: indicativePluperfectSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePluperfectThirdPersonSingular - 4: - label: nós - value: indicativePluperfectFirstPersonPlural - 5: - label: vós - value: indicativePluperfectSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePluperfectThirdPersonPlural - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: singular + plural: plural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Presente + tenses: + 1: + tenseTitle: Presente + tenseForms: + 1: + label: eu + value: indicativePresentFirstPersonSingular + 2: + label: tu + value: indicativePresentSecondPersonSingular + 3: + label: ele/ela/você + value: indicativePresentThirdPersonSingular + 4: + label: nós + value: indicativePresentFirstPersonPlural + 5: + label: vós + value: indicativePresentSecondPersonPlural + 6: + label: eles/elas/vocês + value: indicativePresentThirdPersonPlural + + 2: + sectionTitle: Pretérito Perfeito + tenses: + 1: + tenseTitle: Pretérito Perfeito + tenseForms: + 1: + label: eu + value: indicativePastPerfectFirstPersonSingular + 2: + label: tu + value: indicativePastPerfectSecondPersonSingular + 3: + label: ele/ela/você + value: indicativePastPerfectThirdPersonSingular + 4: + label: nós + value: indicativePastPerfectFirstPersonPlural + 5: + label: vós + value: indicativePastPerfectSecondPersonPlural + 6: + label: eles/elas/vocês + value: indicativePastPerfectThirdPersonPlural + + 3: + sectionTitle: Pretérito Imperfeito + tenses: + 1: + tenseTitle: Pretérito Imperfeito + tenseForms: + 1: + label: eu + value: indicativePastImperfectFirstPersonSingular + 2: + label: tu + value: indicativePastImperfectSecondPersonSingular + 3: + label: ele/ela/você + value: indicativePastImperfectThirdPersonSingular + 4: + label: nós + value: indicativePastImperfectFirstPersonPlural + 5: + label: vós + value: indicativePastImperfectSecondPersonPlural + 6: + label: eles/elas/vocês + value: indicativePastImperfectThirdPersonPlural + + 4: + sectionTitle: Futuro Simples + tenses: + 1: + tenseTitle: Futuro Simples + tenseForms: + 1: + label: eu + value: indicativePluperfectFirstPersonSingular + 2: + label: tu + value: indicativePluperfectSecondPersonSingular + 3: + label: ele/ela/você + value: indicativePluperfectThirdPersonSingular + 4: + label: nós + value: indicativePluperfectFirstPersonPlural + 5: + label: vós + value: indicativePluperfectSecondPersonPlural + 6: + label: eles/elas/vocês + value: indicativePluperfectThirdPersonPlural + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). diff --git a/app/src/main/assets/data-contracts/ru.yaml b/app/src/main/assets/data-contracts/ru.yaml index ff16b2a80..9dadc1696 100644 --- a/app/src/main/assets/data-contracts/ru.yaml +++ b/app/src/main/assets/data-contracts/ru.yaml @@ -1,72 +1,78 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - nominativeSingular: nominativePlural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Настоящее - tenses: - 1: - tenseTitle: Indicative Present - tenseForms: - 1: - label: я - value: indicativePresentFirstPersonSingular - 2: - label: ты - value: indicativePresentSecondPersonSingular - 3: - label: он/она/оно - value: indicativePresentThirdPersonSingular - 4: - label: мы - value: indicativePresentFirstPersonPlural - 5: - label: вы - value: indicativePresentSecondPersonPlural - 6: - label: они - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Прошедшее - tenses: - 1: - tenseTitle: Indicative Past - tenseForms: - 1: - label: я/ты/она - value: feminineIndicativePast - 2: - label: я/ты/он - value: masculineIndicativePast - 3: - label: оно - value: neuterIndicativePast - 4: - label: мы/вы/они - value: indicativePastPlural - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: nominativeSingular + plural: nominativePlural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Настоящее + tenses: + 1: + tenseTitle: Indicative Present + tenseForms: + 1: + label: я + value: indicativePresentFirstPersonSingular + 2: + label: ты + value: indicativePresentSecondPersonSingular + 3: + label: он/она/оно + value: indicativePresentThirdPersonSingular + 4: + label: мы + value: indicativePresentFirstPersonPlural + 5: + label: вы + value: indicativePresentSecondPersonPlural + 6: + label: они + value: indicativePresentThirdPersonPlural + + 2: + sectionTitle: Прошедшее + tenses: + 1: + tenseTitle: Indicative Past + tenseForms: + 1: + label: я/ты/она + value: feminineIndicativePast + 2: + label: я/ты/он + value: masculineIndicativePast + 3: + label: оно + value: neuterIndicativePast + 4: + label: мы/вы/они + value: indicativePastPlural + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). @@ -115,7 +121,12 @@ translations: displayValue: глагол sectionTitle: Выберите инфинитив глагола -# MARK: Decline +# MARK: Prepositions + +prepositions: + case: grammaticalCase + +# MARK: Declensions # Map of pronoun declensions given cases. declensions: diff --git a/app/src/main/assets/data-contracts/sv.yaml b/app/src/main/assets/data-contracts/sv.yaml index 53c1dbb21..f6a8c073a 100644 --- a/app/src/main/assets/data-contracts/sv.yaml +++ b/app/src/main/assets/data-contracts/sv.yaml @@ -1,67 +1,75 @@ # Database column mappings that are read by Scribe applications to structure how data is displayed to the user. # Includes interface titles and the order that values should be presented (chronological or alphabetical). - -# MARK: Noun Gender - -# A map of which columns in the nouns table contain gender information. -genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - -# MARK: Plural - -# Key-value pairs that link the key of a noun singular to its plural for the Plural command. -numbers: - nominativeIndefiniteSingular: nominativeIndefinitePlural - nominativeDefiniteSingular: nominativeDefinitePlural - -# MARK: Conjugate - -# Map of verb tenses to their conjugations for the Conjugate command. -# Sections provide meta tense selections if there is more than one tense included. -conjugations: - 1: - sectionTitle: Aktiv - tenses: - 1: - tenseTitle: Aktiv - tenseForms: - 1: - label: infinitiv - value: activeInfinitive - 2: - label: presens - value: activePresent - 3: - label: preteritum - value: activePreterite - 4: - label: supinum - value: activeSupine - - 2: - sectionTitle: Passiv - tenses: - 1: - tenseTitle: Passiv - tenseForms: - 1: - label: infinitiv - value: passiveInfinitive - 2: - label: presens - value: passivePresent - 3: - label: preteritum - value: passivePreterite - 4: - label: supinum - value: passiveSupine - -# MARK: Translate +# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. +# - If the nodes have "value" as a key, then all values for this key are used in query generation +# - If the nodes don't have "value" as a key, then all end values are used in query generation +# Note that {lexemeForm} values are included but [word] values are not. + +# MARK: Nouns + +nouns: + # A map of which columns in the nouns table contain gender information. + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + + # Key-value pairs that link the key of a noun singular to its plural for the Plural command. + numbers: + 1: + singular: nominativeIndefiniteSingular + plural: nominativeIndefinitePlural + 2: + singular: nominativeDefiniteSingular + plural: nominativeDefinitePlural + +# MARK: Verbs + +verbs: + # Map of verb tenses to their conjugations for the Conjugate command. + # Sections provide meta tense selections if there is more than one tense included. + conjugations: + 1: + sectionTitle: Aktiv + tenses: + 1: + tenseTitle: Aktiv + tenseForms: + 1: + label: infinitiv + value: activeInfinitive + 2: + label: presens + value: activePresent + 3: + label: preteritum + value: activePreterite + 4: + label: supinum + value: activeSupine + + 2: + sectionTitle: Passiv + tenses: + 1: + tenseTitle: Passiv + tenseForms: + 1: + label: infinitiv + value: passiveInfinitive + 2: + label: presens + value: passivePresent + 3: + label: preteritum + value: passivePreterite + 4: + label: supinum + value: passiveSupine + +# MARK: Translations # Map of interface titles to display for the Translate command. # Needed database columns are the same across languages (descriptions are labels for values). diff --git a/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt b/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt index 090874735..f7a5c221a 100644 --- a/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt +++ b/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt @@ -33,7 +33,7 @@ class ConjugateDataManager( word: String, ): MutableMap>>? { val finalOutput: MutableMap>> = mutableMapOf() - yamlData?.conjugations?.values?.forEach { tenseGroup -> + yamlData?.verbs?.conjugations?.values?.forEach { tenseGroup -> val conjugateForms: MutableMap> = mutableMapOf() tenseGroup.tenses.values.forEach { conjugationCategory -> val forms = @@ -65,7 +65,7 @@ class ConjugateDataManager( word: String, ): Set { val allFormKeys = mutableSetOf() - yamlData?.conjugations?.values?.forEach { tenseGroup -> + yamlData?.verbs?.conjugations?.values?.forEach { tenseGroup -> tenseGroup.tenses.values.forEach { conjugationCategory -> conjugationCategory.tenseForms.values.forEach { form -> allFormKeys.add(form.label) diff --git a/app/src/main/java/be/scri/helpers/data/GenderDataManager.kt b/app/src/main/java/be/scri/helpers/data/GenderDataManager.kt index 329e4694c..8c835886b 100644 --- a/app/src/main/java/be/scri/helpers/data/GenderDataManager.kt +++ b/app/src/main/java/be/scri/helpers/data/GenderDataManager.kt @@ -51,20 +51,29 @@ class GenderDataManager( hasCanonicalGender(contract) -> processGenders( db = db, - nounColumn = contract.numbers.keys.firstOrNull(), - genderColumn = contract.genders.canonical.firstOrNull(), + nounColumn = + contract.nouns.numbers.values + .firstOrNull() + ?.singular, + genderColumn = + contract.nouns.genders.canonical + .firstOrNull(), genderMap = genderMap, ) hasMasculineFeminine(contract) -> { processGenders( db = db, - nounColumn = contract.genders.masculines.firstOrNull(), + nounColumn = + contract.nouns.genders.masculines + .firstOrNull(), genderMap = genderMap, defaultGender = "masculine", ) processGenders( db = db, - nounColumn = contract.genders.feminines.firstOrNull(), + nounColumn = + contract.nouns.genders.feminines + .firstOrNull(), genderMap = genderMap, defaultGender = "feminine", ) @@ -82,7 +91,7 @@ class GenderDataManager( * @return true if a canonical gender column is specified, false otherwise. */ private fun hasCanonicalGender(contract: DataContract): Boolean = - contract.genders.canonical + contract.nouns.genders.canonical .firstOrNull() ?.isNotEmpty() == true @@ -94,8 +103,8 @@ class GenderDataManager( * @return true if both masculine and feminine columns are specified, false otherwise. */ private fun hasMasculineFeminine(contract: DataContract): Boolean { - val masculineList = contract.genders.masculines - val feminineList = contract.genders.feminines + val masculineList = contract.nouns.genders.masculines + val feminineList = contract.nouns.genders.feminines val hasMasculine = masculineList.isNotEmpty() val hasFeminine = feminineList.isNotEmpty() diff --git a/app/src/main/java/be/scri/helpers/data/PluralFormsManager.kt b/app/src/main/java/be/scri/helpers/data/PluralFormsManager.kt index 0d4b84f2c..e094c3e0b 100644 --- a/app/src/main/java/be/scri/helpers/data/PluralFormsManager.kt +++ b/app/src/main/java/be/scri/helpers/data/PluralFormsManager.kt @@ -26,7 +26,7 @@ class PluralFormsManager( language: String, yamlData: DataContract?, ): List? = - yamlData?.numbers?.values?.toList()?.takeIf { it.isNotEmpty() }?.let { pluralForms -> + yamlData?.nouns?.numbers?.values?.map { it.plural }?.takeIf { it.isNotEmpty() }?.let { pluralForms -> fileManager.getLanguageDatabase(language)?.use { db -> queryAllPluralForms(db, pluralForms) } @@ -47,9 +47,10 @@ class PluralFormsManager( yamlData: DataContract?, noun: String, ): Map = - yamlData?.numbers?.let { numbers -> - val singularCol = numbers.keys.firstOrNull() - val pluralCol = numbers.values.firstOrNull() + yamlData?.nouns?.numbers?.let { numbers -> + val firstPair = numbers.values.firstOrNull() + val singularCol = firstPair?.singular + val pluralCol = firstPair?.plural if (singularCol != null && pluralCol != null) { val wasCapitalized = isWordCapitalized(noun) diff --git a/app/src/main/java/be/scri/models/DataContract.kt b/app/src/main/java/be/scri/models/DataContract.kt index 250cecf68..f6ce93c97 100644 --- a/app/src/main/java/be/scri/models/DataContract.kt +++ b/app/src/main/java/be/scri/models/DataContract.kt @@ -12,10 +12,35 @@ import kotlinx.serialization.Serializable */ @Serializable data class DataContract( - val numbers: Map, + val nouns: Nouns, + val verbs: Verbs, + val translations: Translations, +) + +/** + * Represents noun grammar metadata (genders and number representations). + */ +@Serializable +data class Nouns( val genders: Genders, + val numbers: Map, +) + +/** + * Represents a singular-plural column mapping pair for nouns. + */ +@Serializable +data class NumberPair( + val singular: String, + val plural: String, +) + +/** + * Represents verb grammar metadata (conjugations). + */ +@Serializable +data class Verbs( val conjugations: Map, - val translations: Translations, ) /** diff --git a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt index c9481417e..cbc97503a 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt @@ -90,7 +90,7 @@ fun ConjugationSelectionScreen( val manager = ConjugateDataManager(fileManager) val structuredData = mutableMapOf>>>() - contract.conjugations.values.forEach { tenseGroup -> + contract.verbs.conjugations.values.forEach { tenseGroup -> val categories = mutableMapOf>>() tenseGroup.tenses.values.forEach { conjugationCategory -> val pairs = diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt new file mode 100644 index 000000000..a822bf91f --- /dev/null +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.helpers.data + +import android.content.Context +import android.content.res.AssetManager +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.IOException + +class ContractDataLoaderTest { + private lateinit var context: Context + private lateinit var assetManager: AssetManager + private lateinit var loader: ContractDataLoader + + @BeforeEach + fun setup() { + context = mockk(relaxed = true) + assetManager = mockk(relaxed = true) + every { context.assets } returns assetManager + loader = ContractDataLoader(context) + } + + @Test + fun loadContract_validYaml_parsesNounsVerbsTranslations() { + val yamlContent = + """ + nouns: + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + numbers: + 1: + singular: nominativeSingular + plural: nominativePlural + verbs: + conjugations: + 1: + sectionTitle: Present + tenses: + 1: + tenseTitle: Pr. Simple + tenseForms: + 1: + label: I/you/plural + value: simplePresent + translations: + wordType: + sectionTitle: Word Types + adjective: + displayValue: Adjective + sectionTitle: Adjectives + adverb: + displayValue: Adverb + sectionTitle: Adverbs + article: + displayValue: Article + sectionTitle: Articles + conjunction: + displayValue: Conjunction + sectionTitle: Conjunctions + noun: + displayValue: Noun + sectionTitle: Nouns + postposition: + displayValue: Postposition + sectionTitle: Postpositions + preposition: + displayValue: Preposition + sectionTitle: Prepositions + proper_noun: + displayValue: Proper Noun + sectionTitle: Proper Nouns + pronoun: + displayValue: Pronoun + sectionTitle: Pronouns + verb: + displayValue: Verb + sectionTitle: Verbs + """.trimIndent() + + every { assetManager.open("data-contracts/en.yaml") } returns ByteArrayInputStream(yamlContent.toByteArray()) + + val contract = loader.loadContract("EN") + + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + assertEquals( + "nominativeSingular", + contract + ?.nouns + ?.numbers + ?.get(1) + ?.singular, + ) + assertEquals( + "nominativePlural", + contract + ?.nouns + ?.numbers + ?.get(1) + ?.plural, + ) + assertEquals( + "Present", + contract + ?.verbs + ?.conjugations + ?.get(1) + ?.sectionTitle, + ) + assertEquals("Word Types", contract?.translations?.wordType?.sectionTitle) + } + + @Test + fun loadContract_fileNotFound_returnsNull() { + every { assetManager.open(any()) } throws IOException("File not found") + + val contract = loader.loadContract("NON_EXISTENT") + + assertNull(contract) + } + + @Test + fun loadContract_malformedYaml_returnsNull() { + val invalidYaml = "nouns: [invalid: yaml: :" + every { assetManager.open(any()) } returns ByteArrayInputStream(invalidYaml.toByteArray()) + + val contract = loader.loadContract("EN") + + assertNull(contract) + } + + @Test + fun allAssetContracts_parseSuccessfully() { + val languages = listOf("de", "en", "es", "fr", "it", "pt", "ru", "sv") + for (lang in languages) { + val file = java.io.File("src/main/assets/data-contracts/$lang.yaml") + val content = file.readText() + every { assetManager.open("data-contracts/$lang.yaml") } returns ByteArrayInputStream(content.toByteArray()) + + val contract = loader.loadContract(lang) + assertNotNull(contract, "Failed to parse contract for language: $lang") + assertNotNull(contract?.nouns, "Nouns is null for language: $lang") + assertNotNull(contract?.verbs, "Verbs is null for language: $lang") + assertNotNull(contract?.translations, "Translations is null for language: $lang") + assertNotNull(contract?.nouns?.numbers, "Numbers is null for language: $lang") + assertNotNull(contract?.verbs?.conjugations, "Conjugations is null for language: $lang") + } + } +} From 9c57c4598b5b3229bd58d7e6ee9908ec9453d160 Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:02:31 +0530 Subject: [PATCH 2/7] feat: load data contracts dynamically from local storage and remove bundled assets (#714) - Remove bundled YAML contracts from assets/data-contracts preserving .gitkeep - Update ContractDataLoader to load from local app storage with assets fallback - Add saveContract helper in ContractDataLoader to persist downloaded contracts - Update ContractDataLoaderTest and CHANGELOG.md --- CHANGELOG.md | 2 +- app/src/main/assets/data-contracts/de.yaml | 969 ------------------ app/src/main/assets/data-contracts/en.yaml | 220 ---- app/src/main/assets/data-contracts/es.yaml | 181 ---- app/src/main/assets/data-contracts/fr.yaml | 178 ---- app/src/main/assets/data-contracts/it.yaml | 153 --- app/src/main/assets/data-contracts/pt.yaml | 178 ---- app/src/main/assets/data-contracts/ru.yaml | 326 ------ app/src/main/assets/data-contracts/sv.yaml | 119 --- .../scri/helpers/data/ContractDataLoader.kt | 78 +- .../helpers/data/ContractDataLoaderTest.kt | 191 ++-- 11 files changed, 182 insertions(+), 2413 deletions(-) delete mode 100644 app/src/main/assets/data-contracts/de.yaml delete mode 100644 app/src/main/assets/data-contracts/en.yaml delete mode 100644 app/src/main/assets/data-contracts/es.yaml delete mode 100644 app/src/main/assets/data-contracts/fr.yaml delete mode 100644 app/src/main/assets/data-contracts/it.yaml delete mode 100644 app/src/main/assets/data-contracts/pt.yaml delete mode 100644 app/src/main/assets/data-contracts/ru.yaml delete mode 100644 app/src/main/assets/data-contracts/sv.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8785022..25bee5713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - The user is directed to download data in the keyboard UI if they access it with empty databases ([#581](https://github.com/scribe-org/Scribe-Android/issues/581), [#650](https://github.com/scribe-org/Scribe-Android/issues/650)). - The data download UI was created to download data for any keyboards that have been installed ([#437](https://github.com/scribe-org/Scribe-Android/issues/437), [#439](https://github.com/scribe-org/Scribe-Android/issues/439), [#513](https://github.com/scribe-org/Scribe-Android/issues/513), [#520](https://github.com/scribe-org/Scribe-Android/issues/520), [#554](https://github.com/scribe-org/Scribe-Android/issues/554)). - Network indicators for data request have been added to the application and are shown via toasts ([#651](https://github.com/scribe-org/Scribe-Android/issues/651)). -- Language data contracts were updated to use nested nouns and verbs structures ([#714](https://github.com/scribe-org/Scribe-Android/issues/714)). +- Language data contracts were updated to use nested nouns and verbs structures, and bundled contract assets were removed to support dynamic server-provided contracts ([#714](https://github.com/scribe-org/Scribe-Android/issues/714)). ### 🎨 Design diff --git a/app/src/main/assets/data-contracts/de.yaml b/app/src/main/assets/data-contracts/de.yaml deleted file mode 100644 index 93f2b01a4..000000000 --- a/app/src/main/assets/data-contracts/de.yaml +++ /dev/null @@ -1,969 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: nominativeSingular - plural: nominativePlural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Präsens - tenses: - 1: - tenseTitle: Präsens - tenseForms: - 1: - label: ich - value: indicativePresentFirstPersonSingular - 2: - label: du - value: indicativePresentSecondPersonSingular - 3: - label: er/sie/es - value: indicativePresentThirdPersonSingular - 4: - label: wir - value: indicativePresentFirstPersonPlural - 5: - label: ihr - value: indicativePresentSecondPersonPlural - 6: - label: sie/Sie - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Preterite - tenses: - 1: - tenseTitle: Preterite - tenseForms: - 1: - label: ich - value: indicativePreteriteFirstPersonSingular - 2: - label: du - value: indicativePreteriteSecondPersonSingular - 3: - label: er/sie/es - value: indicativePreteriteThirdPersonSingular - 4: - label: wir - value: indicativePreteriteFirstPersonPlural - 5: - label: ihr - value: indicativePreteriteSecondPersonPlural - 6: - label: sie/Sie - value: indicativePreteriteThirdPersonPlural - - 3: - sectionTitle: Perfekt - tenses: - 1: - tenseTitle: Perfekt - tenseForms: - 1: - label: ich - value: "{indicativePresentFirstPersonSingular auxiliaryVerb} pastParticiple" - 2: - label: du - value: "{indicativePresentSecondPersonSingular auxiliaryVerb} pastParticiple" - 3: - label: er/sie/es - value: "{indicativePresentThirdPersonSingular auxiliaryVerb} pastParticiple" - 4: - label: wir - value: "{indicativePresentFirstPersonPlural auxiliaryVerb} pastParticiple" - 5: - label: ihr - value: "{indicativePresentSecondPersonPlural auxiliaryVerb} pastParticiple" - 6: - label: sie/Sie - value: "{indicativePresentThirdPersonPlural auxiliaryVerb} pastParticiple" - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Übersetzungsworttyp auswählen - - adjective: - displayValue: Adjektiv - sectionTitle: Adjektiv Übersetzung auswählen - - adverb: - displayValue: Adverb - sectionTitle: Adverb Übersetzung auswählen - - article: - displayValue: Artikel - sectionTitle: Artikel Übersetzung auswählen - - conjunction: - displayValue: Konjunktion - sectionTitle: Konjunktion Übersetzung auswählen - - noun: - displayValue: Nomen - sectionTitle: Nomen Übersetzung auswählen - - postposition: - displayValue: Postposition - sectionTitle: Postposition Übersetzung auswählen - - preposition: - displayValue: Preposition - sectionTitle: Preposition Übersetzung auswählen - - proper_noun: - displayValue: Eigenname - sectionTitle: Eigenname Übersetzung auswählen - - pronoun: - displayValue: Pronomen - sectionTitle: Pronomen Übersetzung auswählen - - verb: - displayValue: Verb - sectionTitle: Verb Übersetzung auswählen - -# MARK: Prepositions - -prepositions: - case: grammaticalCase - -# MARK: Declensions - -# Map of pronoun declensions given cases. -declensions: - 1: - sectionTitle: Akkusativ Definitpronomen - declensionForms: - 1: - label: M - value: den - 2: - label: F - value: die - 3: - label: N - value: das - 4: - label: PL - value: die - - 2: - sectionTitle: Akkusativ Indefinitpronomen - declensionForms: - 1: - label: M - value: einen - 2: - label: F - value: eine - 3: - label: N - value: ein - 4: - label: PL - value: welche - - 3: - sectionTitle: Akkusativ Personalpronomen - declensionForms: - 1: - label: ich - value: mich - - 2: - label: du/Sie - displayValue: dich/Sie - title: Formalität des Subjekts? - declensionForms: - 1: - label: informell - value: dich - 2: - label: formell - value: Sie - - 3: - label: er/sie/es - displayValue: ihn/sie/es - title: Geschlecht des Subjekts? - declensionForms: - 1: - label: er - value: ihn - 2: - label: sie - value: sie - 3: - label: es - value: es - - 4: - label: wir - value: uns - - 5: - label: ihr - value: euch - - 6: - label: sie - value: sie - - 4: - sectionTitle: Akkusativ Possessivpronomen - declensionForms: - 1: - label: ich - displayValue: mein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: meinen - 2: - label: F - value: meine - 3: - label: N - value: mein - 4: - label: PL - value: meine - - 2: - label: du/Sie - displayValue: dein*/Ihr* - title: Formalität des Subjekts? - declensionForms: - 1: - label: informell - displayValue: dein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: deinen - 2: - label: F - value: deine - 3: - label: N - value: dein - 4: - label: PL - value: deine - - 2: - label: formell - displayValue: Ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: Ihren - 2: - label: F - value: Ihre - 3: - label: N - value: Ihr - 4: - label: PL - value: Ihre - - 3: - label: er/sie/es - displayValue: sein*/ihr*/sein* - title: Geschlecht des Subjekts? - declensionForms: - 1: - label: er - displayValue: sein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: seinen - 2: - label: F - value: seine - 3: - label: N - value: sein - 4: - label: PL - value: seine - - 2: - label: sie - displayValue: ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: ihren - 2: - label: F - value: ihre - 3: - label: N - value: ihr - 4: - label: PL - value: ihre - - 3: - label: es - displayValue: sein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: seinen - 2: - label: F - value: seine - 3: - label: N - value: sein - 4: - label: PL - value: seine - - 4: - label: wir - displayValue: unser* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: unsren - 2: - label: F - value: unsre - 3: - label: N - value: unser - 4: - label: PL - value: unsre - - 5: - label: ihr - displayValue: euer* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: euren - 2: - label: F - value: eure - 3: - label: N - value: euer - 4: - label: PL - value: eure - - 6: - label: sie - displayValue: ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: ihren - 2: - label: F - value: ihre - 3: - label: N - value: ihr - 4: - label: PL - value: ihre - - 5: - sectionTitle: Akkusativ Demonstrativpronomen - declensionForms: - 1: - label: M - value: diesen - 2: - label: F - value: diese - 3: - label: N - value: dieses - 4: - label: PL - value: diese - - 6: - sectionTitle: Dativ Definitpronomen - declensionForms: - 1: - label: M - value: dem - 2: - label: F - value: der - 3: - label: N - value: dem - 4: - label: PL - value: den - - 7: - sectionTitle: Dativ Indefinitpronomen - declensionForms: - 1: - label: M - value: einem - 2: - label: F - value: einer - 3: - label: N - value: einem - 4: - label: PL - value: welchen - - 8: - sectionTitle: Dativ Personalpronomen - declensionForms: - 1: - label: ich - value: mir - - 2: - label: du/Sie - displayValue: dir/Ihnen - title: Formalität des Subjekts? - declensionForms: - 1: - label: informell - value: dir - 2: - label: formell - value: Ihnen - - 3: - label: er/sie/es - displayValue: ihm/ihr/ihm - title: Geschlecht des Subjekts? - declensionForms: - 1: - label: er - value: ihm - 2: - label: sie - value: ihr - 3: - label: es - value: ihm - - 4: - label: wir - value: uns - - 5: - label: ihr - value: euch - - 6: - label: sie - value: ihnen - - 9: - sectionTitle: Dativ Possessivpronomen - declensionForms: - 1: - label: ich - displayValue: mein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: meinem - 2: - label: F - value: meiner - 3: - label: N - value: meinem - 4: - label: PL - value: meinen - - 2: - label: du/Sie - displayValue: dein*/Ihr* - title: Formalität des Subjekts? - declensionForms: - 1: - label: informell - displayValue: dein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: deinem - 2: - label: F - value: deiner - 3: - label: N - value: deinem - 4: - label: PL - value: deinen - - 2: - label: formell - displayValue: Ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: Ihrem - 2: - label: F - value: Ihrer - 3: - label: N - value: Ihrem - 4: - label: PL - value: Ihren - - 3: - label: er/sie/es - displayValue: sein*/ihr*/sein* - title: Geschlecht des Subjekts? - declensionForms: - 1: - label: er - displayValue: sein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: seinem - 2: - label: F - value: seiner - 3: - label: N - value: seinem - 4: - label: PL - value: seinen - - 2: - label: sie - displayValue: ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: ihrem - 2: - label: F - value: ihrer - 3: - label: N - value: ihrem - 4: - label: PL - value: ihren - - 3: - label: es - displayValue: sein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: seinem - 2: - label: F - value: seiner - 3: - label: N - value: seinem - 4: - label: PL - value: seinen - - 4: - label: wir - displayValue: unser* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: unsrem - 2: - label: F - value: unsrer - 3: - label: N - value: unsrem - 4: - label: PL - value: unsren - - 5: - label: ihr - displayValue: euer* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: eurem - 2: - label: F - value: eurer - 3: - label: N - value: eurem - 4: - label: PL - value: euren - - 6: - label: sie - displayValue: ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: ihrem - 2: - label: F - value: ihrer - 3: - label: N - value: ihrem - 4: - label: PL - value: ihren - - 10: - sectionTitle: Dativ Demonstrativpronomen - declensionForms: - 1: - label: M - value: diesem - 2: - label: F - value: dieser - 3: - label: N - value: diesem - 4: - label: PL - value: diesen - - 11: - sectionTitle: Genitiv Definitpronomen - declensionForms: - 1: - label: M - value: des - 2: - label: F - value: der - 3: - label: N - value: des - 4: - label: PL - value: der - - 12: - sectionTitle: Genitiv Indefinitpronomen - declensionForms: - 1: - label: M - value: eines - 2: - label: F - value: einer - 3: - label: N - value: eines - 4: - label: PL - value: welcher - - 13: - sectionTitle: Genitiv Personalpronomen - declensionForms: - 1: - label: ich - value: meiner - - 2: - label: du/Sie - displayValue: deiner/Ihrer - title: Formalität des Subjekts? - declensionForms: - 1: - label: informell - value: deiner - 2: - label: formell - value: Ihrer - - 3: - label: er/sie/es - displayValue: seiner/ihrer/seiner - title: Geschlecht des Subjekts? - declensionForms: - 1: - label: er - value: seiner - 2: - label: sie - value: ihrer - 3: - label: es - value: seiner - - 4: - label: wir - value: unser - - 5: - label: ihr - value: euer - - 6: - label: sie - value: ihrer - - 14: - sectionTitle: Genitiv Possessivpronomen - declensionForms: - 1: - label: ich - displayValue: mein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: meines - 2: - label: F - value: meiner - 3: - label: N - value: meines - 4: - label: PL - value: meiner - - 2: - label: du/Sie - displayValue: dein*/Ihr* - title: Formalität des Subjekts? - declensionForms: - 1: - label: informell - displayValue: dein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: deines - 2: - label: F - value: deiner - 3: - label: N - value: deines - 4: - label: PL - value: deiner - - 2: - label: formell - displayValue: Ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: Ihres - 2: - label: F - value: Ihrer - 3: - label: N - value: Ihres - 4: - label: PL - value: Ihrer - - 3: - label: er/sie/es - displayValue: sein*/ihr*/sein* - title: Geschlecht des Subjekts? - declensionForms: - 1: - label: er - displayValue: sein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: seines - 2: - label: F - value: seiner - 3: - label: N - value: seines - 4: - label: PL - value: seiner - - 2: - label: sie - displayValue: ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: ihres - 2: - label: F - value: ihr - 3: - label: N - value: ihres - 4: - label: PL - value: ihr - - 3: - label: es - displayValue: sein* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: seines - 2: - label: F - value: seiner - 3: - label: N - value: seines - 4: - label: PL - value: seiner - - 4: - label: wir - displayValue: unser* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: unsres - 2: - label: F - value: unsrer - 3: - label: N - value: unsres - 4: - label: PL - value: unsrer - - 5: - label: ihr - displayValue: euer* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: eures - 2: - label: F - value: eurer - 3: - label: N - value: eures - 4: - label: PL - value: eurer - - 6: - label: sie - displayValue: ihr* - title: Geschlecht des Objekts? - declensionForms: - 1: - label: M - value: ihres - 2: - label: F - value: ihrer - 3: - label: N - value: ihres - 4: - label: PL - value: ihrer - - 15: - sectionTitle: Genitiv Demonstrativpronomen - declensionForms: - 1: - label: M - value: dieses - 2: - label: F - value: dieser - 3: - label: N - value: dieses - 4: - label: PL - value: dieser diff --git a/app/src/main/assets/data-contracts/en.yaml b/app/src/main/assets/data-contracts/en.yaml deleted file mode 100644 index 05b069ec5..000000000 --- a/app/src/main/assets/data-contracts/en.yaml +++ /dev/null @@ -1,220 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: singular - plural: plural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Present - tenses: - 1: - tenseTitle: Pr. Simple - tenseForms: - 1: - label: I/you/plural - value: simplePresent - 2: - label: he/she/it - value: simplePresentThirdPersonSingular - - 2: - tenseTitle: Pr. Perfect - tenseForms: - 1: - label: I/you/plural - value: "[have] englishPastParticiple" - 2: - label: he/she/it - value: "[has] englishPastParticiple" - - 3: - tenseTitle: Pr. Continuous - tenseForms: - 1: - label: I - value: "[am] presentParticiple" - 2: - label: you/plural - value: "[are] presentParticiple" - 3: - label: he/she/it - value: "[is] presentParticiple" - - 4: - tenseTitle: Pr. Perf. Continuous - tenseForms: - 1: - label: I/you/plural - value: "[have been] presentParticiple" - 2: - label: he/she/it - value: "[has been] presentParticiple" - - 2: - sectionTitle: Past - tenses: - 1: - tenseTitle: Past Simple - tenseForms: - 1: - label: all - value: simplePast - - 2: - tenseTitle: Past Perfect - tenseForms: - 1: - label: all - value: "[had] englishPastParticiple" - - 3: - tenseTitle: Past Continuous - tenseForms: - 1: - label: I/he/she/it - value: "[was] presentParticiple" - 2: - label: you/plural - value: "[were] presentParticiple" - - 4: - tenseTitle: Past Perf. Continuous - tenseForms: - 1: - label: all - value: "[had been] presentParticiple" - - 3: - sectionTitle: Future - tenses: - 1: - tenseTitle: Fut. Simple - tenseForms: - 1: - label: all - value: "[will] infinitive" - - 2: - tenseTitle: Fut. Perfect - tenseForms: - 1: - label: all - value: "[will have] englishPastParticiple" - - 3: - tenseTitle: Fut. Continuous - tenseForms: - 1: - label: all - value: "[will be] presentParticiple" - - 4: - tenseTitle: Fut. Perf. Continuous - tenseForms: - 1: - label: all - value: "[will have been] presentParticiple" - - 4: - sectionTitle: Conditional - tenses: - 1: - tenseTitle: Cond. Simple - tenseForms: - 1: - label: all - value: "[would] infinitive" - - 2: - tenseTitle: Cond. Perfect - tenseForms: - 1: - label: all - value: "[would have] englishPastParticiple" - - 3: - tenseTitle: Cond. Continuous - tenseForms: - 1: - label: all - value: "[would be] presentParticiple" - - 4: - tenseTitle: Cond. Perf. Continuous - tenseForms: - 1: - label: all - value: "[would have been] presentParticiple" - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Select translation word type - - adjective: - displayValue: adjective - sectionTitle: Select adjective translation - - adverb: - displayValue: adverb - sectionTitle: Select adverb translation - - article: - displayValue: article - sectionTitle: Select article translation - - conjunction: - displayValue: conjunction - sectionTitle: Select conjunction translation - - noun: - displayValue: noun - sectionTitle: Select noun translation - - postposition: - displayValue: postposition - sectionTitle: Select postposition translation - - preposition: - displayValue: preposition - sectionTitle: Select preposition translation - - proper_noun: - displayValue: proper noun - sectionTitle: Select proper noun translation - - pronoun: - displayValue: pronoun - sectionTitle: Select pronoun translation - - verb: - displayValue: verb - sectionTitle: Select verb infinitive diff --git a/app/src/main/assets/data-contracts/es.yaml b/app/src/main/assets/data-contracts/es.yaml deleted file mode 100644 index 2362f031b..000000000 --- a/app/src/main/assets/data-contracts/es.yaml +++ /dev/null @@ -1,181 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [] - feminines: [feminineSingular] - masculines: [masculineSingular] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: feminineSingular - plural: femininePlural - 2: - singular: masculineSingular - plural: masculinePlural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Presente - tenses: - 1: - tenseTitle: Presente - tenseForms: - 1: - label: yo - value: indicativePresentFirstPersonSingular - 2: - label: tú - value: indicativePresentSecondPersonSingular - 3: - label: él/ella/Ud. - value: indicativePresentThirdPersonSingular - 4: - label: nosotros - value: indicativePresentFirstPersonPlural - 5: - label: vosotros - value: indicativePresentSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Pretérito - tenses: - 1: - tenseTitle: Pretérito - tenseForms: - 1: - label: yo - value: preteriteFirstPersonSingular - 2: - label: tú - value: preteriteSecondPersonSingular - 3: - label: él/ella/Ud. - value: preteriteThirdPersonSingular - 4: - label: nosotros - value: preteriteFirstPersonPlural - 5: - label: vosotros - value: preteriteSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: preteriteThirdPersonPlural - - 3: - sectionTitle: Imperfecto - tenses: - 1: - tenseTitle: Imperfecto - tenseForms: - 1: - label: yo - value: imperfectFirstPersonSingular - 2: - label: tú - value: imperfectSecondPersonSingular - 3: - label: él/ella/Ud. - value: imperfectThirdPersonSingular - 4: - label: nosotros - value: imperfectFirstPersonPlural - 5: - label: vosotros - value: imperfectSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: imperfectThirdPersonPlural - - 4: - sectionTitle: Futuro - tenses: - 1: - tenseTitle: Futuro - tenseForms: - 1: - label: yo - value: indicativeFutureFirstPersonSingular - 2: - label: tú - value: indicativeFutureSecondPersonSingular - 3: - label: él/ella/Ud. - value: indicativeFutureThirdPersonSingular - 4: - label: nosotros - value: indicativeFutureFirstPersonPlural - 5: - label: vosotros - value: indicativeFutureSecondPersonPlural - 6: - label: ellos/ellas/Uds. - value: indicativeFutureThirdPersonPlural - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Seleccione el tipo de traducción - - adjective: - displayValue: adjetivo - sectionTitle: Seleccione traducción de adjetivo - - adverb: - displayValue: adverbio - sectionTitle: Seleccione traducción de adverbio - - article: - displayValue: artículo - sectionTitle: Seleccione traducción de artículo - - conjunction: - displayValue: conjunción - sectionTitle: Seleccione traducción de conjunción - - noun: - displayValue: sustantivo - sectionTitle: Seleccione traducción de sustantivo - - postposition: - displayValue: posposición - sectionTitle: Seleccione traducción de posposición - - preposition: - displayValue: preposición - sectionTitle: Seleccione traducción de preposición - - proper_noun: - displayValue: nombre propio - sectionTitle: Seleccione traducción de nombre propio - - pronoun: - displayValue: pronombre - sectionTitle: Seleccione traducción de pronombre - - verb: - displayValue: verbo - sectionTitle: Seleccionar verbo infinitivo diff --git a/app/src/main/assets/data-contracts/fr.yaml b/app/src/main/assets/data-contracts/fr.yaml deleted file mode 100644 index da14f3f50..000000000 --- a/app/src/main/assets/data-contracts/fr.yaml +++ /dev/null @@ -1,178 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: singular - plural: plural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Présent - tenses: - 1: - tenseTitle: Présent - tenseForms: - 1: - label: je - value: indicativePresentFirstPersonSingular - 2: - label: tu - value: indicativePresentSecondPersonSingular - 3: - label: il/elle - value: indicativePresentThirdPersonSingular - 4: - label: nous - value: indicativePresentFirstPersonPlural - 5: - label: vous - value: indicativePresentSecondPersonPlural - 6: - label: ils/elles - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Passé simple - tenses: - 1: - tenseTitle: Passé simple - tenseForms: - 1: - label: je - value: indicativePreteriteFirstPersonSingular - 2: - label: tu - value: indicativePreteriteSecondPersonSingular - 3: - label: il/elle - value: indicativePreteriteThirdPersonSingular - 4: - label: nous - value: indicativePreteriteFirstPersonPlural - 5: - label: vous - value: indicativePreteriteSecondPersonPlural - 6: - label: ils/elles - value: indicativePreteriteThirdPersonPlural - - 3: - sectionTitle: Imparfait - tenses: - 1: - tenseTitle: Imparfait - tenseForms: - 1: - label: je - value: indicativeImperfectFirstPersonSingular - 2: - label: tu - value: indicativeImperfectSecondPersonSingular - 3: - label: il/elle - value: indicativeImperfectThirdPersonSingular - 4: - label: nous - value: indicativeImperfectFirstPersonPlural - 5: - label: vous - value: indicativeImperfectSecondPersonPlural - 6: - label: ils/elles - value: indicativeImperfectThirdPersonPlural - - 4: - sectionTitle: Futur - tenses: - 1: - tenseTitle: Futur - tenseForms: - 1: - label: je - value: indicativeSimpleFutureFirstPersonSingular - 2: - label: tu - value: indicativeSimpleFutureSecondPersonSingular - 3: - label: il/elle - value: indicativeSimpleFutureThirdPersonSingular - 4: - label: nous - value: indicativeSimpleFutureFirstPersonPlural - 5: - label: vous - value: indicativeSimpleFutureSecondPersonPlural - 6: - label: ils/elles - value: indicativeSimpleFutureThirdPersonPlural - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Sélectionnez le type à traduire - - adjective: - displayValue: adjectif - sectionTitle: Sélectionner la traduction de l'adjectif - - adverb: - displayValue: adverbe - sectionTitle: Sélectionner la traduction de l'adverbe - - article: - displayValue: article - sectionTitle: Sélectionner la traduction de l'article - - conjunction: - displayValue: conjonction - sectionTitle: Sélectionner la traduction de conjonction - - noun: - displayValue: nom - sectionTitle: Sélectionner la traduction du nom - - postposition: - displayValue: postposition - sectionTitle: Sélectionnez la traduction de la postposition - - preposition: - displayValue: préposition - sectionTitle: Sélectionnez la traduction de la préposition - - proper_noun: - displayValue: nom propre - sectionTitle: Sélectionnez la traduction du nom propre - - pronoun: - displayValue: pronom - sectionTitle: Sélectionner la traduction des pronoms - - verb: - displayValue: verbe - sectionTitle: Sélectionnez l'infinitif du verbe diff --git a/app/src/main/assets/data-contracts/it.yaml b/app/src/main/assets/data-contracts/it.yaml deleted file mode 100644 index 8da2289f5..000000000 --- a/app/src/main/assets/data-contracts/it.yaml +++ /dev/null @@ -1,153 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: singular - plural: plural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Presente - tenses: - 1: - tenseTitle: Presente - tenseForms: - 1: - label: io - value: presentIndicativeFirstPersonSingular - 2: - label: tu - value: presentIndicativeSecondPersonSingular - 3: - label: lei/lui - value: presentIndicativeThirdPersonSingular - 4: - label: noi - value: presentIndicativeFirstPersonPlural - 5: - label: voi - value: presentIndicativeSecondPersonPlural - 6: - label: loro - value: presentIndicativeThirdPersonPlural - - 2: - sectionTitle: Preterito - tenses: - 1: - tenseTitle: Preterito - tenseForms: - 1: - label: io - value: indicativePreteriteFirstPersonSingular - 2: - label: tu - value: indicativePreteriteSecondPersonSingular - 3: - label: lei/lui - value: indicativePreteriteThirdPersonSingular - 4: - label: noi - value: indicativePreteriteFirstPersonPlural - 5: - label: voi - value: indicativePreteriteSecondPersonPlural - 6: - label: loro - value: indicativePreteriteThirdPersonPlural - - 3: - sectionTitle: Imperfetto - tenses: - 1: - tenseTitle: Imperfetto - tenseForms: - 1: - label: io - value: indicativeImperfectFirstPersonSingular - 2: - label: tu - value: indicativeImperfectSecondPersonSingular - 3: - label: lei/lui - value: indicativeImperfectThirdPersonSingular - 4: - label: noi - value: indicativeImperfectFirstPersonPlural - 5: - label: voi - value: indicativeImperfectSecondPersonPlural - 6: - label: loro - value: indicativeImperfectThirdPersonPlural - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Seleziona il tipo da tradurre - - adjective: - displayValue: aggettivo - sectionTitle: Seleziona la traduzione dell'aggettivo - - adverb: - displayValue: avverbio - sectionTitle: Seleziona la traduzione dell'avverbio - - article: - displayValue: articolo - sectionTitle: Seleziona la traduzione dell'articolo - - conjunction: - displayValue: congiunzione - sectionTitle: Seleziona la traduzione della congiunzione - - noun: - displayValue: nome - sectionTitle: Seleziona la traduzione del nome - - postposition: - displayValue: posposizione - sectionTitle: Seleziona la traduzione della posposizione - - preposition: - displayValue: preposizione - sectionTitle: Seleziona la traduzione della preposizione - - proper_noun: - displayValue: nome proprio - sectionTitle: Seleziona la traduzione del nome proprio - - pronoun: - displayValue: pronome - sectionTitle: Seleziona la traduzione del pronome - - verb: - displayValue: verbo - sectionTitle: Seleziona il verbo infinito diff --git a/app/src/main/assets/data-contracts/pt.yaml b/app/src/main/assets/data-contracts/pt.yaml deleted file mode 100644 index 384e60f78..000000000 --- a/app/src/main/assets/data-contracts/pt.yaml +++ /dev/null @@ -1,178 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: singular - plural: plural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Presente - tenses: - 1: - tenseTitle: Presente - tenseForms: - 1: - label: eu - value: indicativePresentFirstPersonSingular - 2: - label: tu - value: indicativePresentSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePresentThirdPersonSingular - 4: - label: nós - value: indicativePresentFirstPersonPlural - 5: - label: vós - value: indicativePresentSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Pretérito Perfeito - tenses: - 1: - tenseTitle: Pretérito Perfeito - tenseForms: - 1: - label: eu - value: indicativePastPerfectFirstPersonSingular - 2: - label: tu - value: indicativePastPerfectSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePastPerfectThirdPersonSingular - 4: - label: nós - value: indicativePastPerfectFirstPersonPlural - 5: - label: vós - value: indicativePastPerfectSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePastPerfectThirdPersonPlural - - 3: - sectionTitle: Pretérito Imperfeito - tenses: - 1: - tenseTitle: Pretérito Imperfeito - tenseForms: - 1: - label: eu - value: indicativePastImperfectFirstPersonSingular - 2: - label: tu - value: indicativePastImperfectSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePastImperfectThirdPersonSingular - 4: - label: nós - value: indicativePastImperfectFirstPersonPlural - 5: - label: vós - value: indicativePastImperfectSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePastImperfectThirdPersonPlural - - 4: - sectionTitle: Futuro Simples - tenses: - 1: - tenseTitle: Futuro Simples - tenseForms: - 1: - label: eu - value: indicativePluperfectFirstPersonSingular - 2: - label: tu - value: indicativePluperfectSecondPersonSingular - 3: - label: ele/ela/você - value: indicativePluperfectThirdPersonSingular - 4: - label: nós - value: indicativePluperfectFirstPersonPlural - 5: - label: vós - value: indicativePluperfectSecondPersonPlural - 6: - label: eles/elas/vocês - value: indicativePluperfectThirdPersonPlural - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Selecione o tipo a ser traduzida - - adjective: - displayValue: adjetivo - sectionTitle: Selecione a tradução do adjetivo - - adverb: - displayValue: advérbio - sectionTitle: Selecione a tradução do advérbio - - article: - displayValue: artigo - sectionTitle: Selecione a tradução do artigo - - conjunction: - displayValue: conjunção - sectionTitle: Selecione a tradução da conjunção - - noun: - displayValue: nome - sectionTitle: Selecione a tradução do substantivo - - postposition: - displayValue: posposição - sectionTitle: Selecione a tradução de posposição - - preposition: - displayValue: preposição - sectionTitle: Selecione a tradução da preposiçãon - - proper_noun: - displayValue: nome próprio - sectionTitle: Selecione a tradução do nome próprio - - pronoun: - displayValue: pronome - sectionTitle: Selecione a tradução do pronome - - verb: - displayValue: verbo - sectionTitle: Selecione o verbo no infinitivo diff --git a/app/src/main/assets/data-contracts/ru.yaml b/app/src/main/assets/data-contracts/ru.yaml deleted file mode 100644 index 9dadc1696..000000000 --- a/app/src/main/assets/data-contracts/ru.yaml +++ /dev/null @@ -1,326 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: nominativeSingular - plural: nominativePlural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Настоящее - tenses: - 1: - tenseTitle: Indicative Present - tenseForms: - 1: - label: я - value: indicativePresentFirstPersonSingular - 2: - label: ты - value: indicativePresentSecondPersonSingular - 3: - label: он/она/оно - value: indicativePresentThirdPersonSingular - 4: - label: мы - value: indicativePresentFirstPersonPlural - 5: - label: вы - value: indicativePresentSecondPersonPlural - 6: - label: они - value: indicativePresentThirdPersonPlural - - 2: - sectionTitle: Прошедшее - tenses: - 1: - tenseTitle: Indicative Past - tenseForms: - 1: - label: я/ты/она - value: feminineIndicativePast - 2: - label: я/ты/он - value: masculineIndicativePast - 3: - label: оно - value: neuterIndicativePast - 4: - label: мы/вы/они - value: indicativePastPlural - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Выберите тип переводимого слова - - adjective: - displayValue: прилагательное - sectionTitle: Выберите перевод прилагательного - - adverb: - displayValue: наречие - sectionTitle: Выберите перевод наречия - - article: - displayValue: статья - sectionTitle: Выберите перевод статьи - - conjunction: - displayValue: соединение - sectionTitle: Выберите перевод союза - - noun: - displayValue: существительное - sectionTitle: Выберите перевод существительного - - postposition: - displayValue: послелог - sectionTitle: Выберите перевод послелога - - preposition: - displayValue: предлог - sectionTitle: Выберите перевод предлога - - proper_noun: - displayValue: имя собственное - sectionTitle: Выберите перевод имени собственного. - - pronoun: - displayValue: местоимение - sectionTitle: Выберите перевод местоимения - - verb: - displayValue: глагол - sectionTitle: Выберите инфинитив глагола - -# MARK: Prepositions - -prepositions: - case: grammaticalCase - -# MARK: Declensions - -# Map of pronoun declensions given cases. -declensions: - 1: - title: Винительные местоимения - declensionForms: - 1: - label: я - value: меня - - 2: - label: ты - value: тебя - - 3: - label: он/она/оно - displayValue: его/её/его - title: Пол объекта? - declensionForms: - 1: - label: он - value: его - 2: - label: она - value: её - 3: - label: оно - value: его - - 4: - label: мы - value: нас - - 5: - label: вы - value: вас - - 6: - label: они - value: их - - 2: - title: Дательные местоимения - declensionForms: - 1: - label: я - value: мне - - 2: - label: ты - value: тебе - - 3: - label: он/она/оно - displayValue: ему/ей/ему - title: Пол объекта? - declensionForms: - 1: - label: он - value: ему - - 2: - label: она - value: ей - - 3: - label: оно - value: ему - - 4: - label: мы - value: нам - - 5: - label: вы - value: вам - 6: - label: они - value: им - - 3: - title: Родительные местоимения - declensionForms: - 1: - label: я - value: меня - - 2: - label: ты - value: тебя - - 3: - label: он/она/оно - displayValue: его/её/его - title: Пол объекта? - declensionForms: - 1: - label: он - value: его - - 2: - label: она - value: её - - 3: - label: оно - value: его - - 4: - label: мы - value: нас - - 5: - label: вы - value: вас - - 6: - label: они - value: их - - 4: - title: Творительные местоимения - declensionForms: - 1: - label: я - value: мной - - 2: - label: ты - value: тобой - - 3: - label: он/она/оно - displayValue: им/ей/им - title: Пол объекта? - declensionForms: - 1: - label: он - value: им - 2: - label: она - value: ей - 3: - label: оно - value: им - - 4: - label: мы - value: нами - - 5: - label: вы - value: вами - - 6: - label: они - value: ими - - 5: - title: Предложные местоимения - declensionForms: - 1: - label: я - value: мне - - 2: - label: ты - value: тебе - - 3: - label: он/она/оно - displayValue: нём/ней/нём - title: Пол объекта? - declensionForms: - 1: - label: он - value: нём - - 2: - label: она - value: ней - - 3: - label: оно - value: нём - - 4: - label: мы - value: нас - - 5: - label: вы - value: вас - - 6: - label: они - value: них diff --git a/app/src/main/assets/data-contracts/sv.yaml b/app/src/main/assets/data-contracts/sv.yaml deleted file mode 100644 index f6a8c073a..000000000 --- a/app/src/main/assets/data-contracts/sv.yaml +++ /dev/null @@ -1,119 +0,0 @@ -# Database column mappings that are read by Scribe applications to structure how data is displayed to the user. -# Includes interface titles and the order that values should be presented (chronological or alphabetical). -# Contracts are also how Scribe-Data generates queries. All data type end values are query lexeme forms. -# - If the nodes have "value" as a key, then all values for this key are used in query generation -# - If the nodes don't have "value" as a key, then all end values are used in query generation -# Note that {lexemeForm} values are included but [word] values are not. - -# MARK: Nouns - -nouns: - # A map of which columns in the nouns table contain gender information. - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - - # Key-value pairs that link the key of a noun singular to its plural for the Plural command. - numbers: - 1: - singular: nominativeIndefiniteSingular - plural: nominativeIndefinitePlural - 2: - singular: nominativeDefiniteSingular - plural: nominativeDefinitePlural - -# MARK: Verbs - -verbs: - # Map of verb tenses to their conjugations for the Conjugate command. - # Sections provide meta tense selections if there is more than one tense included. - conjugations: - 1: - sectionTitle: Aktiv - tenses: - 1: - tenseTitle: Aktiv - tenseForms: - 1: - label: infinitiv - value: activeInfinitive - 2: - label: presens - value: activePresent - 3: - label: preteritum - value: activePreterite - 4: - label: supinum - value: activeSupine - - 2: - sectionTitle: Passiv - tenses: - 1: - tenseTitle: Passiv - tenseForms: - 1: - label: infinitiv - value: passiveInfinitive - 2: - label: presens - value: passivePresent - 3: - label: preteritum - value: passivePreterite - 4: - label: supinum - value: passiveSupine - -# MARK: Translations - -# Map of interface titles to display for the Translate command. -# Needed database columns are the same across languages (descriptions are labels for values). -# Note: Descriptions for article and pronoun translations are their genders. -translations: - wordType: - sectionTitle: Välj översättningsordtyp - - adjective: - displayValue: adjektiv - sectionTitle: Välj adjektivöversättning - - adverb: - displayValue: adverb - sectionTitle: Välj adverböversättning - - article: - displayValue: artikel - sectionTitle: Välj artikelöversättning - - conjunction: - displayValue: konjunktion - sectionTitle: Välj konjunktionsöversättning - - noun: - displayValue: substantiv - sectionTitle: Välj substantivöversättning - - postposition: - displayValue: postposition - sectionTitle: Välj postpositionsöversättning - - preposition: - displayValue: preposition - sectionTitle: Välj prepositionsöversättning - - proper_noun: - displayValue: egennamn - sectionTitle: Välj egennamnöversättning - - pronoun: - displayValue: pronomen - sectionTitle: Välj pronomenöversättning - - verb: - displayValue: verb - sectionTitle: Välj verbinfinitiv diff --git a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt index 31c5945ce..1c829531d 100644 --- a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt +++ b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt @@ -7,18 +7,25 @@ import android.util.Log import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import com.charleskorn.kaml.YamlException +import java.io.File import java.io.IOException /** - * Loads and deserializes contract data from assets. + * Loads and deserializes contract data from local storage or assets. * @param context The application context. */ class ContractDataLoader( private val context: Context, ) { + companion object { + private const val TAG = "ContractDataLoader" + private const val CONTRACTS_DIR = "data-contracts" + } + /** - * Loads and deserializes a data contract from a YAML file in the assets folder. - * It gracefully handles file-not-found and YAML parsing errors by returning null. + * Loads and deserializes a data contract for the specified language. + * Checks internal app storage first, falling back to assets. + * Gracefully handles file-not-found and YAML parsing errors by returning null. * * @param language The language code (e.g., "DE", "EN") used to determine the filename (e.g., "de.yaml"). * @@ -27,23 +34,70 @@ class ContractDataLoader( */ fun loadContract(language: String): DataContract? { val contractName = "${language.lowercase()}.yaml" - Log.d("ContractDataLoader", "Attempting to load contract: $contractName") + Log.d(TAG, "Attempting to load contract: $contractName") + + val localFile = File(File(context.filesDir, CONTRACTS_DIR), contractName) + if (localFile.exists() && localFile.isFile) { + return try { + parseContract(localFile.readText()) + } catch (e: IOException) { + Log.e(TAG, "Error reading local contract file: ${localFile.path}", e) + null + } + } return try { - context.assets.open("data-contracts/$contractName").use { contractFile -> + context.assets.open("$CONTRACTS_DIR/$contractName").use { contractFile -> val content = contractFile.bufferedReader().readText() - val yaml = - Yaml( - configuration = YamlConfiguration(strictMode = false), - ) - yaml.decodeFromString(DataContract.serializer(), content) + parseContract(content) } } catch (e: IOException) { - Log.e("ContractDataLoader", "Error loading contract file: $contractName. It may not exist.", e) + Log.e(TAG, "Error loading contract file: $contractName. It may not exist.", e) null + } + } + + /** + * Parses a YAML string into a [DataContract]. + * + * @param content The YAML content to decode. + * @return The decoded [DataContract] object, or `null` if parsing fails. + */ + fun parseContract(content: String): DataContract? = + try { + val yaml = + Yaml( + configuration = YamlConfiguration(strictMode = false), + ) + yaml.decodeFromString(DataContract.serializer(), content) } catch (e: YamlException) { - Log.e("ContractDataLoader", "Error parsing YAML for contract: $contractName", e) + Log.e(TAG, "Error parsing YAML contract", e) null } + + /** + * Saves a contract YAML string to local app storage. + * + * @param language The language code (e.g., "DE", "EN"). + * @param yamlContent The YAML string content to persist. + * @return True if saved successfully, false otherwise. + */ + fun saveContract( + language: String, + yamlContent: String, + ): Boolean { + val dir = File(context.filesDir, CONTRACTS_DIR) + if (!dir.exists() && !dir.mkdirs()) { + Log.e(TAG, "Failed to create directory: ${dir.path}") + return false + } + val file = File(dir, "${language.lowercase()}.yaml") + return try { + file.writeText(yamlContent) + true + } catch (e: IOException) { + Log.e(TAG, "Failed to write contract file for $language", e) + false + } } } diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt index a822bf91f..a7fc8108f 100644 --- a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -5,89 +5,105 @@ import android.content.Context import android.content.res.AssetManager import io.mockk.every import io.mockk.mockk +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.io.ByteArrayInputStream +import java.io.File import java.io.IOException class ContractDataLoaderTest { private lateinit var context: Context private lateinit var assetManager: AssetManager + private lateinit var tempFilesDir: File private lateinit var loader: ContractDataLoader + private val sampleYaml = + """ + nouns: + genders: + canonical: [gender] + feminines: [] + masculines: [] + commons: [] + neuters: [] + numbers: + 1: + singular: nominativeSingular + plural: nominativePlural + verbs: + conjugations: + 1: + sectionTitle: Present + tenses: + 1: + tenseTitle: Pr. Simple + tenseForms: + 1: + label: I/you/plural + value: simplePresent + translations: + wordType: + sectionTitle: Word Types + adjective: + displayValue: Adjective + sectionTitle: Adjectives + adverb: + displayValue: Adverb + sectionTitle: Adverbs + article: + displayValue: Article + sectionTitle: Articles + conjunction: + displayValue: Conjunction + sectionTitle: Conjunctions + noun: + displayValue: Noun + sectionTitle: Nouns + postposition: + displayValue: Postposition + sectionTitle: Postpositions + preposition: + displayValue: Preposition + sectionTitle: Prepositions + proper_noun: + displayValue: Proper Noun + sectionTitle: Proper Nouns + pronoun: + displayValue: Pronoun + sectionTitle: Pronouns + verb: + displayValue: Verb + sectionTitle: Verbs + """.trimIndent() + @BeforeEach fun setup() { context = mockk(relaxed = true) assetManager = mockk(relaxed = true) + tempFilesDir = + java.nio.file.Files + .createTempDirectory("scribe_test_contracts") + .toFile() + every { context.filesDir } returns tempFilesDir every { context.assets } returns assetManager loader = ContractDataLoader(context) } + @AfterEach + fun tearDown() { + tempFilesDir.deleteRecursively() + } + @Test - fun loadContract_validYaml_parsesNounsVerbsTranslations() { - val yamlContent = - """ - nouns: - genders: - canonical: [gender] - feminines: [] - masculines: [] - commons: [] - neuters: [] - numbers: - 1: - singular: nominativeSingular - plural: nominativePlural - verbs: - conjugations: - 1: - sectionTitle: Present - tenses: - 1: - tenseTitle: Pr. Simple - tenseForms: - 1: - label: I/you/plural - value: simplePresent - translations: - wordType: - sectionTitle: Word Types - adjective: - displayValue: Adjective - sectionTitle: Adjectives - adverb: - displayValue: Adverb - sectionTitle: Adverbs - article: - displayValue: Article - sectionTitle: Articles - conjunction: - displayValue: Conjunction - sectionTitle: Conjunctions - noun: - displayValue: Noun - sectionTitle: Nouns - postposition: - displayValue: Postposition - sectionTitle: Postpositions - preposition: - displayValue: Preposition - sectionTitle: Prepositions - proper_noun: - displayValue: Proper Noun - sectionTitle: Proper Nouns - pronoun: - displayValue: Pronoun - sectionTitle: Pronouns - verb: - displayValue: Verb - sectionTitle: Verbs - """.trimIndent() - - every { assetManager.open("data-contracts/en.yaml") } returns ByteArrayInputStream(yamlContent.toByteArray()) + fun loadContract_fromLocalStorage_parsesSuccessfully() { + val contractsDir = File(tempFilesDir, "data-contracts") + contractsDir.mkdirs() + File(contractsDir, "en.yaml").writeText(sampleYaml) val contract = loader.loadContract("EN") @@ -120,6 +136,34 @@ class ContractDataLoaderTest { assertEquals("Word Types", contract?.translations?.wordType?.sectionTitle) } + @Test + fun loadContract_fallbackToAssets_parsesSuccessfully() { + every { assetManager.open("data-contracts/en.yaml") } returns ByteArrayInputStream(sampleYaml.toByteArray()) + + val contract = loader.loadContract("EN") + + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + assertEquals( + "nominativeSingular", + contract + ?.nouns + ?.numbers + ?.get(1) + ?.singular, + ) + } + + @Test + fun saveContract_writesFileAndCanBeLoaded() { + val saved = loader.saveContract("DE", sampleYaml) + assertTrue(saved) + + val contract = loader.loadContract("DE") + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + } + @Test fun loadContract_fileNotFound_returnsNull() { every { assetManager.open(any()) } throws IOException("File not found") @@ -140,20 +184,15 @@ class ContractDataLoaderTest { } @Test - fun allAssetContracts_parseSuccessfully() { - val languages = listOf("de", "en", "es", "fr", "it", "pt", "ru", "sv") - for (lang in languages) { - val file = java.io.File("src/main/assets/data-contracts/$lang.yaml") - val content = file.readText() - every { assetManager.open("data-contracts/$lang.yaml") } returns ByteArrayInputStream(content.toByteArray()) - - val contract = loader.loadContract(lang) - assertNotNull(contract, "Failed to parse contract for language: $lang") - assertNotNull(contract?.nouns, "Nouns is null for language: $lang") - assertNotNull(contract?.verbs, "Verbs is null for language: $lang") - assertNotNull(contract?.translations, "Translations is null for language: $lang") - assertNotNull(contract?.nouns?.numbers, "Numbers is null for language: $lang") - assertNotNull(contract?.verbs?.conjugations, "Conjugations is null for language: $lang") - } + fun parseContract_validYaml_returnsDataContract() { + val contract = loader.parseContract(sampleYaml) + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + } + + @Test + fun parseContract_invalidYaml_returnsNull() { + val contract = loader.parseContract("invalid: yaml: [") + assertNull(contract) } } From e4b7614bbddcdeaaef4f34af3075fc2904dd5341 Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:37:10 +0530 Subject: [PATCH 3/7] feat: connect remote data contract downloads to local app storage (#714) - Add getContract endpoint in ApiService for Scribe-Server /contracts route - Add downloadAndSaveContract and hasContract helpers in ContractDataLoader - Connect contract download to DataDownloadViewModel and ConjugateDataDownloadViewModel - Add on-demand contract download fallback in ConjugationSelectionScreen - Support JSON contracts and add unit tests in ContractDataLoaderTest - Update CHANGELOG.md --- CHANGELOG.md | 2 +- .../java/be/scri/data/remote/ApiService.kt | 10 +- .../scri/helpers/data/ContractDataLoader.kt | 118 +++++++++++++----- .../ui/screens/ConjugationSelectionScreen.kt | 6 +- .../ConjugateDataDownloadViewModel.kt | 12 +- .../screens/download/DataDownloadViewModel.kt | 11 +- .../helpers/data/ContractDataLoaderTest.kt | 104 +++++++++++++++ 7 files changed, 226 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25bee5713..f4d89621b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - The user is directed to download data in the keyboard UI if they access it with empty databases ([#581](https://github.com/scribe-org/Scribe-Android/issues/581), [#650](https://github.com/scribe-org/Scribe-Android/issues/650)). - The data download UI was created to download data for any keyboards that have been installed ([#437](https://github.com/scribe-org/Scribe-Android/issues/437), [#439](https://github.com/scribe-org/Scribe-Android/issues/439), [#513](https://github.com/scribe-org/Scribe-Android/issues/513), [#520](https://github.com/scribe-org/Scribe-Android/issues/520), [#554](https://github.com/scribe-org/Scribe-Android/issues/554)). - Network indicators for data request have been added to the application and are shown via toasts ([#651](https://github.com/scribe-org/Scribe-Android/issues/651)). -- Language data contracts were updated to use nested nouns and verbs structures, and bundled contract assets were removed to support dynamic server-provided contracts ([#714](https://github.com/scribe-org/Scribe-Android/issues/714)). +- Language data contracts were updated to use nested nouns and verbs structures, bundled contract assets were removed, and dynamic contract downloads from Scribe-Server were connected to local storage ([#714](https://github.com/scribe-org/Scribe-Android/issues/714)). ### 🎨 Design diff --git a/app/src/main/java/be/scri/data/remote/ApiService.kt b/app/src/main/java/be/scri/data/remote/ApiService.kt index 698fc213c..98b798f13 100644 --- a/app/src/main/java/be/scri/data/remote/ApiService.kt +++ b/app/src/main/java/be/scri/data/remote/ApiService.kt @@ -4,11 +4,13 @@ package be.scri.data.remote import be.scri.data.model.DataResponse import be.scri.data.model.DataVersionResponse +import okhttp3.ResponseBody import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query /** - * Defines the API service for fetching data and data version information. + * Defines the API service for fetching data, data version information, and data contracts. */ interface ApiService { @GET("data/{lang}") @@ -20,4 +22,10 @@ interface ApiService { suspend fun getDataVersion( @Path("lang") language: String, ): DataVersionResponse + + @GET("contracts") + suspend fun getContract( + @Query("lang") language: String, + ): ResponseBody } + diff --git a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt index 1c829531d..eead0b03c 100644 --- a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt +++ b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt @@ -4,14 +4,15 @@ package be.scri.helpers.data import DataContract import android.content.Context import android.util.Log +import be.scri.data.remote.RetrofitClient import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration -import com.charleskorn.kaml.YamlException +import kotlinx.serialization.json.Json import java.io.File import java.io.IOException /** - * Loads and deserializes contract data from local storage or assets. + * Loads, deserializes, downloads, and saves contract data from local storage, assets, or network. * @param context The application context. */ class ContractDataLoader( @@ -20,66 +21,97 @@ class ContractDataLoader( companion object { private const val TAG = "ContractDataLoader" private const val CONTRACTS_DIR = "data-contracts" + private val json = + Json { + ignoreUnknownKeys = true + isLenient = true + } + private val yaml = + Yaml( + configuration = YamlConfiguration(strictMode = false), + ) + } + + /** + * Checks if a contract file exists in local app storage for the specified language. + * + * @param language The language code (e.g., "DE", "EN"). + * @return True if a local .yaml or .json contract file exists, false otherwise. + */ + fun hasContract(language: String): Boolean { + val lang = language.lowercase() + val dir = File(context.filesDir, CONTRACTS_DIR) + return File(dir, "$lang.yaml").exists() || File(dir, "$lang.json").exists() } /** * Loads and deserializes a data contract for the specified language. * Checks internal app storage first, falling back to assets. - * Gracefully handles file-not-found and YAML parsing errors by returning null. + * Gracefully handles file-not-found and parsing errors by returning null. * - * @param language The language code (e.g., "DE", "EN") used to determine the filename (e.g., "de.yaml"). + * @param language The language code (e.g., "DE", "EN") used to determine the filename (e.g., "de.yaml" or "de.json"). * * @return The decoded [DataContract] object if successful, or `null` * if the file does not exist or cannot be parsed. */ fun loadContract(language: String): DataContract? { - val contractName = "${language.lowercase()}.yaml" - Log.d(TAG, "Attempting to load contract: $contractName") + val lang = language.lowercase() + Log.d(TAG, "Attempting to load contract for language: $lang") - val localFile = File(File(context.filesDir, CONTRACTS_DIR), contractName) - if (localFile.exists() && localFile.isFile) { - return try { - parseContract(localFile.readText()) - } catch (e: IOException) { - Log.e(TAG, "Error reading local contract file: ${localFile.path}", e) - null + val dir = File(context.filesDir, CONTRACTS_DIR) + val localFiles = listOf(File(dir, "$lang.yaml"), File(dir, "$lang.json")) + for (localFile in localFiles) { + if (localFile.exists() && localFile.isFile) { + try { + val parsed = parseContract(localFile.readText()) + if (parsed != null) return parsed + } catch (e: IOException) { + Log.e(TAG, "Error reading local contract file: ${localFile.path}", e) + } } } - return try { - context.assets.open("$CONTRACTS_DIR/$contractName").use { contractFile -> - val content = contractFile.bufferedReader().readText() - parseContract(content) + val assetExtensions = listOf("yaml", "json") + for (ext in assetExtensions) { + val assetPath = "$CONTRACTS_DIR/$lang.$ext" + try { + context.assets.open(assetPath).use { contractFile -> + val content = contractFile.bufferedReader().readText() + val parsed = parseContract(content) + if (parsed != null) return parsed + } + } catch (_: IOException) { + // Asset not found, try next extension. } - } catch (e: IOException) { - Log.e(TAG, "Error loading contract file: $contractName. It may not exist.", e) - null } + + Log.e(TAG, "Error loading contract file for: $lang. It may not exist.") + return null } /** - * Parses a YAML string into a [DataContract]. + * Parses a YAML or JSON string into a [DataContract]. * - * @param content The YAML content to decode. + * @param content The YAML or JSON content to decode. * @return The decoded [DataContract] object, or `null` if parsing fails. */ fun parseContract(content: String): DataContract? = try { - val yaml = - Yaml( - configuration = YamlConfiguration(strictMode = false), - ) yaml.decodeFromString(DataContract.serializer(), content) - } catch (e: YamlException) { - Log.e(TAG, "Error parsing YAML contract", e) - null + } catch (_: Exception) { + try { + json.decodeFromString(DataContract.serializer(), content) + } catch (e: Exception) { + Log.e(TAG, "Error parsing contract content", e) + null + } } /** - * Saves a contract YAML string to local app storage. + * Saves contract content (YAML or JSON) to local app storage. * * @param language The language code (e.g., "DE", "EN"). - * @param yamlContent The YAML string content to persist. + * @param yamlContent The contract string content to persist. * @return True if saved successfully, false otherwise. */ fun saveContract( @@ -91,7 +123,10 @@ class ContractDataLoader( Log.e(TAG, "Failed to create directory: ${dir.path}") return false } - val file = File(dir, "${language.lowercase()}.yaml") + val lang = language.lowercase() + val isJson = yamlContent.trimStart().startsWith("{") + val extension = if (isJson) "json" else "yaml" + val file = File(dir, "$lang.$extension") return try { file.writeText(yamlContent) true @@ -100,4 +135,23 @@ class ContractDataLoader( false } } + + /** + * Downloads and saves the data contract for the specified language from Scribe-Server. + * + * @param language The language code (e.g., "DE", "EN"). + * @return True if downloaded and saved successfully, false otherwise. + */ + suspend fun downloadAndSaveContract(language: String): Boolean { + val lang = language.lowercase() + return try { + val response = RetrofitClient.apiService.getContract(lang) + val content = response.string() + saveContract(lang, content) + } catch (e: Exception) { + Log.e(TAG, "Failed to download and save contract for $lang", e) + false + } + } } + diff --git a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt index cbc97503a..c1bf2249e 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt @@ -84,7 +84,11 @@ fun ConjugationSelectionScreen( LaunchedEffect(verb, languageAlias) { withContext(Dispatchers.IO) { val loader = ContractDataLoader(context) - val contract = loader.loadContract(languageAlias) + var contract = loader.loadContract(languageAlias) + if (contract == null) { + loader.downloadAndSaveContract(languageAlias) + contract = loader.loadContract(languageAlias) + } if (contract != null) { val fileManager = DatabaseFileManager(context) val manager = ConjugateDataManager(fileManager) diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index 8cb56b882..35d5acc4d 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -15,6 +15,7 @@ import be.scri.data.remote.RetrofitClient import be.scri.helpers.LanguageMappingConstants import be.scri.helpers.NetworkMonitor import be.scri.helpers.StringUtils +import be.scri.helpers.data.ContractDataLoader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException @@ -156,6 +157,9 @@ class ConjugateDataDownloadViewModel( val dbHelper = ConjugateDynamicDbHelper(getApplication(), langCode) dbHelper.syncConjugateDatabase(response) + val contractLoader = ContractDataLoader(getApplication()) + contractLoader.downloadAndSaveContract(langCode) + // Save timestamp. prefs.edit().putString("last_conjugate_update_$langCode", serverLastUpdate).apply() @@ -169,7 +173,12 @@ class ConjugateDataDownloadViewModel( showToast(msg) } } else { - // Already up to date: Skip the DB work. + // Already up to date: Skip the DB work, but ensure contract is downloaded if missing. + val contractLoader = ContractDataLoader(getApplication()) + if (!contractLoader.hasContract(langCode)) { + contractLoader.downloadAndSaveContract(langCode) + } + withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed val msg = @@ -179,6 +188,7 @@ class ConjugateDataDownloadViewModel( showToast(msg) } } + } catch (e: IOException) { val template = getApplication().getString( diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index e404e494d..3d1cf958a 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -15,6 +15,7 @@ import be.scri.data.remote.RetrofitClient import be.scri.helpers.LanguageMappingConstants import be.scri.helpers.NetworkMonitor import be.scri.helpers.StringUtils +import be.scri.helpers.data.ContractDataLoader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException @@ -156,6 +157,9 @@ class DataDownloadViewModel( val dbHelper = DynamicDbHelper(getApplication(), langCode) dbHelper.syncDatabase(response) + val contractLoader = ContractDataLoader(getApplication()) + contractLoader.downloadAndSaveContract(langCode) + // Save timestamp. prefs.edit().putString("last_update_$langCode", serverLastUpdate).apply() @@ -169,7 +173,12 @@ class DataDownloadViewModel( showToast(msg) } } else { - // Already up to date: Skip the DB work. + // Already up to date: Skip the DB work, but ensure contract is downloaded if missing. + val contractLoader = ContractDataLoader(getApplication()) + if (!contractLoader.hasContract(langCode)) { + contractLoader.downloadAndSaveContract(langCode) + } + withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed val msg = diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt index a7fc8108f..dcc94cb4d 100644 --- a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -7,6 +7,7 @@ import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue @@ -22,6 +23,60 @@ class ContractDataLoaderTest { private lateinit var tempFilesDir: File private lateinit var loader: ContractDataLoader + private val sampleJson = + """ + { + "nouns": { + "genders": { + "canonical": ["gender"], + "feminines": [], + "masculines": [], + "commons": [], + "neuters": [] + }, + "numbers": { + "1": { + "singular": "nominativeSingular", + "plural": "nominativePlural" + } + } + }, + "verbs": { + "conjugations": { + "1": { + "sectionTitle": "Present", + "tenses": { + "1": { + "tenseTitle": "Pr. Simple", + "tenseForms": { + "1": { + "label": "I/you/plural", + "value": "simplePresent" + } + } + } + } + } + } + }, + "translations": { + "wordType": { + "sectionTitle": "Word Types", + "adjective": { "displayValue": "Adjective", "sectionTitle": "Adjectives" }, + "adverb": { "displayValue": "Adverb", "sectionTitle": "Adverbs" }, + "article": { "displayValue": "Article", "sectionTitle": "Articles" }, + "conjunction": { "displayValue": "Conjunction", "sectionTitle": "Conjunctions" }, + "noun": { "displayValue": "Noun", "sectionTitle": "Nouns" }, + "postposition": { "displayValue": "Postposition", "sectionTitle": "Postpositions" }, + "preposition": { "displayValue": "Preposition", "sectionTitle": "Prepositions" }, + "proper_noun": { "displayValue": "Proper Noun", "sectionTitle": "Proper Nouns" }, + "pronoun": { "displayValue": "Pronoun", "sectionTitle": "Pronouns" }, + "verb": { "displayValue": "Verb", "sectionTitle": "Verbs" } + } + } + } + """.trimIndent() + private val sampleYaml = """ nouns: @@ -190,9 +245,58 @@ class ContractDataLoaderTest { assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) } + @Test + fun parseContract_validJson_returnsDataContract() { + val contract = loader.parseContract(sampleJson) + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + } + @Test fun parseContract_invalidYaml_returnsNull() { val contract = loader.parseContract("invalid: yaml: [") assertNull(contract) } + + @Test + fun hasContract_returnsTrueWhenFileExists() { + assertFalse(loader.hasContract("EN")) + + val contractsDir = File(tempFilesDir, "data-contracts") + contractsDir.mkdirs() + File(contractsDir, "en.json").writeText(sampleJson) + + assertTrue(loader.hasContract("EN")) + } + + @Test + fun loadContract_fromJsonFile_parsesSuccessfully() { + val contractsDir = File(tempFilesDir, "data-contracts") + contractsDir.mkdirs() + File(contractsDir, "en.json").writeText(sampleJson) + + val contract = loader.loadContract("EN") + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + assertEquals( + "nominativeSingular", + contract + ?.nouns + ?.numbers + ?.get(1) + ?.singular, + ) + } + + @Test + fun saveContract_withJsonContent_savesAsJson() { + val saved = loader.saveContract("ES", sampleJson) + assertTrue(saved) + assertTrue(File(tempFilesDir, "data-contracts/es.json").exists()) + + val contract = loader.loadContract("ES") + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + } } + From d237b853801291fcf9dab03a9dea6cd88e988c9f Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:38:17 +0530 Subject: [PATCH 4/7] fix: resolve CI ktlint, detekt, and compilation issues (#714) - Add kotlinx-serialization-json dependency to app/build.gradle.kts - Refine exception handling in ContractDataLoader for specific parsing and network exceptions - Fix ktlint formatting issues in ApiService, ConjugateDataDownloadViewModel, and tests --- app/build.gradle.kts | 1 + .../java/be/scri/data/remote/ApiService.kt | 1 - .../scri/helpers/data/ContractDataLoader.kt | 33 ++++++++++++++----- .../ConjugateDataDownloadViewModel.kt | 1 - .../helpers/data/ContractDataLoaderTest.kt | 1 - 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eaf0175a4..76c6fb4d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -334,6 +334,7 @@ dependencies { ksp("com.github.bumptech.glide:ksp:4.16.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") implementation("com.charleskorn.kaml:kaml:0.57.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") } diff --git a/app/src/main/java/be/scri/data/remote/ApiService.kt b/app/src/main/java/be/scri/data/remote/ApiService.kt index 98b798f13..e5c31529c 100644 --- a/app/src/main/java/be/scri/data/remote/ApiService.kt +++ b/app/src/main/java/be/scri/data/remote/ApiService.kt @@ -28,4 +28,3 @@ interface ApiService { @Query("lang") language: String, ): ResponseBody } - diff --git a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt index eead0b03c..3393d0ec9 100644 --- a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt +++ b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt @@ -7,7 +7,10 @@ import android.util.Log import be.scri.data.remote.RetrofitClient import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration +import com.charleskorn.kaml.YamlException +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json +import retrofit2.HttpException import java.io.File import java.io.IOException @@ -98,13 +101,23 @@ class ContractDataLoader( fun parseContract(content: String): DataContract? = try { yaml.decodeFromString(DataContract.serializer(), content) - } catch (_: Exception) { - try { - json.decodeFromString(DataContract.serializer(), content) - } catch (e: Exception) { - Log.e(TAG, "Error parsing contract content", e) - null - } + } catch (_: YamlException) { + parseContractAsJson(content) + } catch (_: SerializationException) { + parseContractAsJson(content) + } catch (_: IllegalArgumentException) { + parseContractAsJson(content) + } + + private fun parseContractAsJson(content: String): DataContract? = + try { + json.decodeFromString(DataContract.serializer(), content) + } catch (e: SerializationException) { + Log.e(TAG, "Error parsing contract content as JSON", e) + null + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Error parsing contract content as JSON", e) + null } /** @@ -148,10 +161,12 @@ class ContractDataLoader( val response = RetrofitClient.apiService.getContract(lang) val content = response.string() saveContract(lang, content) - } catch (e: Exception) { + } catch (e: IOException) { Log.e(TAG, "Failed to download and save contract for $lang", e) false + } catch (e: HttpException) { + Log.e(TAG, "Server returned error while fetching contract for $lang", e) + false } } } - diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index 35d5acc4d..f93bce103 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -188,7 +188,6 @@ class ConjugateDataDownloadViewModel( showToast(msg) } } - } catch (e: IOException) { val template = getApplication().getString( diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt index dcc94cb4d..472678fd3 100644 --- a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -299,4 +299,3 @@ class ContractDataLoaderTest { assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) } } - From 1597c702fe44c7440e50b307e0b7a437faf81cdb Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:08:42 +0530 Subject: [PATCH 5/7] fix: validate contract SQL identifiers, atomic save, and handle download failures (#714) - Add SqlIdentifier utility to validate column names and prevent SQL injection - Parse-check and SQL-validate contract before writing, and use temporary file with atomic rename in ContractDataLoader - Handle contract download failures in DataDownloadViewModel and ConjugateDataDownloadViewModel to prevent false success and timestamps - Add backfillMissingContracts in download view models for existing users with downloaded databases - Add unit tests for SqlIdentifier, atomic save, and invalid contract rejection --- .../scri/helpers/data/ContractDataLoader.kt | 84 +++++++++++++++++-- .../be/scri/helpers/data/SqlIdentifier.kt | 22 +++++ .../ConjugateDataDownloadViewModel.kt | 51 ++++++++++- .../screens/download/DataDownloadViewModel.kt | 51 ++++++++++- .../helpers/data/ContractDataLoaderTest.kt | 48 +++++++++++ .../be/scri/helpers/data/SqlIdentifierTest.kt | 51 +++++++++++ 6 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/be/scri/helpers/data/SqlIdentifier.kt create mode 100644 app/src/test/kotlin/be/scri/helpers/data/SqlIdentifierTest.kt diff --git a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt index 3393d0ec9..da66948ee 100644 --- a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt +++ b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt @@ -121,16 +121,69 @@ class ContractDataLoader( } /** - * Saves contract content (YAML or JSON) to local app storage. + * Validates that all database column names specified in [contract] are safe SQL identifiers. + * Prevents SQL injection vulnerabilities when dynamic queries are formed. + * + * @param contract The [DataContract] to inspect. + * @return True if all column names pass [SqlIdentifier.isValid], false otherwise. + */ + fun validateContract(contract: DataContract): Boolean { + for ((_, pair) in contract.nouns.numbers) { + if (!SqlIdentifier.isValid(pair.singular) || !SqlIdentifier.isValid(pair.plural)) { + Log.e(TAG, "Invalid column name in contract numbers: '${pair.singular}', '${pair.plural}'") + return false + } + } + val genderColumns = + contract.nouns.genders.canonical + + contract.nouns.genders.feminines + + contract.nouns.genders.masculines + + contract.nouns.genders.commons + + contract.nouns.genders.neuters + for (col in genderColumns) { + if (!SqlIdentifier.isValid(col)) { + Log.e(TAG, "Invalid column name in contract genders: '$col'") + return false + } + } + for ((_, tenseGroup) in contract.verbs.conjugations) { + for ((_, category) in tenseGroup.tenses) { + for ((_, form) in category.tenseForms) { + val rawCol = form.value.replace(Regex("""\[.*?\]"""), "").trim() + if (rawCol.isNotEmpty() && !SqlIdentifier.isValid(rawCol)) { + Log.e(TAG, "Invalid column name in contract verbs: '$rawCol'") + return false + } + } + } + } + return true + } + + /** + * Validates and saves contract content (YAML or JSON) to local app storage. + * Content is parse-checked and SQL-identifier-validated before writing. + * Writing uses a temporary file and atomic rename so a failed or invalid response + * cannot corrupt or overwrite an existing working contract. * * @param language The language code (e.g., "DE", "EN"). * @param yamlContent The contract string content to persist. - * @return True if saved successfully, false otherwise. + * @return True if validated and saved successfully, false otherwise. */ fun saveContract( language: String, yamlContent: String, ): Boolean { + val parsedContract = parseContract(yamlContent) + if (parsedContract == null) { + Log.e(TAG, "Failed to parse contract content for $language before saving") + return false + } + if (!validateContract(parsedContract)) { + Log.e(TAG, "Contract content for $language contains invalid SQL identifiers") + return false + } + val dir = File(context.filesDir, CONTRACTS_DIR) if (!dir.exists() && !dir.mkdirs()) { Log.e(TAG, "Failed to create directory: ${dir.path}") @@ -139,12 +192,31 @@ class ContractDataLoader( val lang = language.lowercase() val isJson = yamlContent.trimStart().startsWith("{") val extension = if (isJson) "json" else "yaml" - val file = File(dir, "$lang.$extension") + val targetFile = File(dir, "$lang.$extension") + val tempFile = File(dir, "$lang.$extension.tmp") + return try { - file.writeText(yamlContent) - true + tempFile.writeText(yamlContent) + if (tempFile.renameTo(targetFile)) { + true + } else { + try { + java.nio.file.Files.move( + tempFile.toPath(), + targetFile.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING, + java.nio.file.StandardCopyOption.ATOMIC_MOVE, + ) + true + } catch (e: IOException) { + Log.e(TAG, "Failed to move temp contract file to target file for $language", e) + tempFile.delete() + false + } + } } catch (e: IOException) { - Log.e(TAG, "Failed to write contract file for $language", e) + Log.e(TAG, "Failed to write contract to temp file for $language", e) + tempFile.delete() false } } diff --git a/app/src/main/java/be/scri/helpers/data/SqlIdentifier.kt b/app/src/main/java/be/scri/helpers/data/SqlIdentifier.kt new file mode 100644 index 000000000..f92ee769b --- /dev/null +++ b/app/src/main/java/be/scri/helpers/data/SqlIdentifier.kt @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.helpers.data + +/** + * Validates SQLite identifier names (column and table names). + * Protects against SQL injection when identifiers from remote data contracts + * are dynamically interpolated into database queries. + */ +object SqlIdentifier { + private const val MAX_IDENTIFIER_LENGTH = 128 + private val IDENTIFIER_REGEX = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + + /** + * Checks if [name] is a valid SQLite column/table identifier. + * Must be non-blank, <= 128 characters, and contain only alphanumeric characters + * and underscores, starting with a letter or underscore. + * + * @param name The candidate identifier name to validate. + * @return True if valid, false otherwise. + */ + fun isValid(name: String): Boolean = name.isNotBlank() && name.length <= MAX_IDENTIFIER_LENGTH && IDENTIFIER_REGEX.matches(name) +} diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index f93bce103..368230c8f 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -72,8 +72,9 @@ class ConjugateDataDownloadViewModel( } } - // After initializing, check for updates on all Completed languages. + // After initializing, check for updates on all Completed languages and backfill missing contracts. checkAllForUpdates() + backfillMissingContracts() _checkUpdateState.value = CheckUpdateState.Idle } @@ -158,9 +159,18 @@ class ConjugateDataDownloadViewModel( dbHelper.syncConjugateDatabase(response) val contractLoader = ContractDataLoader(getApplication()) - contractLoader.downloadAndSaveContract(langCode) + val contractSaved = contractLoader.downloadAndSaveContract(langCode) + if (!contractSaved) { + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) + val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") + updateErrorState(key, errorMsg) + return@launch + } - // Save timestamp. + // Save timestamp only if both database and contract saved successfully. prefs.edit().putString("last_conjugate_update_$langCode", serverLastUpdate).apply() withContext(Dispatchers.Main) { @@ -176,7 +186,16 @@ class ConjugateDataDownloadViewModel( // Already up to date: Skip the DB work, but ensure contract is downloaded if missing. val contractLoader = ContractDataLoader(getApplication()) if (!contractLoader.hasContract(langCode)) { - contractLoader.downloadAndSaveContract(langCode) + val contractSaved = contractLoader.downloadAndSaveContract(langCode) + if (!contractSaved) { + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) + val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") + updateErrorState(key, errorMsg) + return@launch + } } withContext(Dispatchers.Main) { @@ -300,6 +319,30 @@ class ConjugateDataDownloadViewModel( } } + /** + * Backfills missing data contracts for existing users who already downloaded + * databases but lack contract files in local storage. + */ + private fun backfillMissingContracts() { + if (!NetworkMonitor.isOnline(getApplication())) return + + val contractLoader = ContractDataLoader(getApplication()) + val completedKeys = downloadStates.keys.filter { downloadStates[it] == DownloadState.Completed } + + viewModelScope.launch(Dispatchers.IO) { + completedKeys.forEach { key -> + val langCode = + LanguageMappingConstants + .getLanguageAlias(key.replaceFirstChar { it.uppercase() }) + .lowercase() + + if (!contractLoader.hasContract(langCode)) { + contractLoader.downloadAndSaveContract(langCode) + } + } + } + } + /** * Checks for new data updates for all completed languages. */ diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index 3d1cf958a..3b92e9288 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -72,8 +72,9 @@ class DataDownloadViewModel( } } - // After initializing, check for updates on all Completed languages. + // After initializing, check for updates on all Completed languages and backfill missing contracts. checkAllForUpdates() + backfillMissingContracts() _checkUpdateState.value = CheckUpdateState.Idle } @@ -158,9 +159,18 @@ class DataDownloadViewModel( dbHelper.syncDatabase(response) val contractLoader = ContractDataLoader(getApplication()) - contractLoader.downloadAndSaveContract(langCode) + val contractSaved = contractLoader.downloadAndSaveContract(langCode) + if (!contractSaved) { + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) + val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") + updateErrorState(key, errorMsg) + return@launch + } - // Save timestamp. + // Save timestamp only if both database and contract saved successfully. prefs.edit().putString("last_update_$langCode", serverLastUpdate).apply() withContext(Dispatchers.Main) { @@ -176,7 +186,16 @@ class DataDownloadViewModel( // Already up to date: Skip the DB work, but ensure contract is downloaded if missing. val contractLoader = ContractDataLoader(getApplication()) if (!contractLoader.hasContract(langCode)) { - contractLoader.downloadAndSaveContract(langCode) + val contractSaved = contractLoader.downloadAndSaveContract(langCode) + if (!contractSaved) { + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) + val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") + updateErrorState(key, errorMsg) + return@launch + } } withContext(Dispatchers.Main) { @@ -300,6 +319,30 @@ class DataDownloadViewModel( } } + /** + * Backfills missing data contracts for existing users who already downloaded + * databases but lack contract files in local storage. + */ + private fun backfillMissingContracts() { + if (!NetworkMonitor.isOnline(getApplication())) return + + val contractLoader = ContractDataLoader(getApplication()) + val completedKeys = downloadStates.keys.filter { downloadStates[it] == DownloadState.Completed } + + viewModelScope.launch(Dispatchers.IO) { + completedKeys.forEach { key -> + val langCode = + LanguageMappingConstants + .getLanguageAlias(key.replaceFirstChar { it.uppercase() }) + .lowercase() + + if (!contractLoader.hasContract(langCode)) { + contractLoader.downloadAndSaveContract(langCode) + } + } + } + } + /** * Checks for new data updates for all completed languages. */ diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt index 472678fd3..cb7a88a9c 100644 --- a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -298,4 +298,52 @@ class ContractDataLoaderTest { assertNotNull(contract) assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) } + + @Test + fun saveContract_invalidContent_returnsFalseAndDoesNotOverwriteExisting() { + // Save initial working contract + val initialSave = loader.saveContract("DE", sampleYaml) + assertTrue(initialSave) + + // Try to overwrite with malformed content + val badSave = loader.saveContract("DE", "malformed: [yaml: {") + assertFalse(badSave) + + // Verify the original contract is untouched and still loads + val contract = loader.loadContract("DE") + assertNotNull(contract) + assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) + } + + @Test + fun saveContract_sqlInjectionInColumnName_returnsFalseAndDoesNotOverwriteExisting() { + // Save initial working contract + val initialSave = loader.saveContract("DE", sampleJson) + assertTrue(initialSave) + + // Payload with SQL injection in column name + val injectionJson = + sampleJson.replace("nominativeSingular", "singular_col; DROP TABLE nouns;--") + val injectionSave = loader.saveContract("DE", injectionJson) + assertFalse(injectionSave) + + // Verify original contract is preserved + val contract = loader.loadContract("DE") + assertNotNull(contract) + assertEquals( + "nominativeSingular", + contract + ?.nouns + ?.numbers + ?.get(1) + ?.singular, + ) + } + + @Test + fun validateContract_validContract_returnsTrue() { + val contract = loader.parseContract(sampleJson) + assertNotNull(contract) + assertTrue(loader.validateContract(contract!!)) + } } diff --git a/app/src/test/kotlin/be/scri/helpers/data/SqlIdentifierTest.kt b/app/src/test/kotlin/be/scri/helpers/data/SqlIdentifierTest.kt new file mode 100644 index 000000000..cc2e63d55 --- /dev/null +++ b/app/src/test/kotlin/be/scri/helpers/data/SqlIdentifierTest.kt @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.helpers.data + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SqlIdentifierTest { + @Test + fun isValid_acceptsValidColumnNames() { + assertTrue(SqlIdentifier.isValid("gender")) + assertTrue(SqlIdentifier.isValid("nominativeSingular")) + assertTrue(SqlIdentifier.isValid("nominative_plural")) + assertTrue(SqlIdentifier.isValid("_id")) + assertTrue(SqlIdentifier.isValid("simplePresent1")) + assertTrue(SqlIdentifier.isValid("columnName")) + } + + @Test + fun isValid_rejectsSqlInjectionAndSpecialCharacters() { + assertFalse(SqlIdentifier.isValid("col; DROP TABLE nouns;--")) + assertFalse(SqlIdentifier.isValid("col' OR '1'='1")) + assertFalse(SqlIdentifier.isValid("col\" OR \"1\"=\"1")) + assertFalse(SqlIdentifier.isValid("col`")) + assertFalse(SqlIdentifier.isValid("col-name")) + assertFalse(SqlIdentifier.isValid("col name")) + assertFalse(SqlIdentifier.isValid("col(1)")) + assertFalse(SqlIdentifier.isValid("col[0]")) + assertFalse(SqlIdentifier.isValid("col/name")) + } + + @Test + fun isValid_rejectsEmptyOrBlank() { + assertFalse(SqlIdentifier.isValid("")) + assertFalse(SqlIdentifier.isValid(" ")) + } + + @Test + fun isValid_rejectsStartingWithDigit() { + assertFalse(SqlIdentifier.isValid("1column")) + assertFalse(SqlIdentifier.isValid("0")) + } + + @Test + fun isValid_rejectsOverlyLongIdentifiers() { + val longName = "a".repeat(129) + assertFalse(SqlIdentifier.isValid(longName)) + val maxAllowed = "a".repeat(128) + assertTrue(SqlIdentifier.isValid(maxAllowed)) + } +} From 8b9947dd76d94a40592494d62b38c0a759206c62 Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Tue, 27 Oct 2026 23:23:17 +0530 Subject: [PATCH 6/7] feat: enhance contract loading and downloading logic, streamline data management --- app/build.gradle.kts | 1 - .../java/be/scri/helpers/DatabaseManagers.kt | 7 +- .../scri/helpers/data/ConjugateDataManager.kt | 3 + .../scri/helpers/data/ContractDataLoader.kt | 159 ++++++++---------- .../ui/screens/ConjugationSelectionScreen.kt | 7 +- .../ConjugateDataDownloadViewModel.kt | 101 ++++------- .../screens/download/DataDownloadViewModel.kt | 101 ++++------- .../helpers/data/ContractDataLoaderTest.kt | 50 +----- 8 files changed, 149 insertions(+), 280 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cba62c724..0fa78ec43 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -333,7 +333,6 @@ dependencies { ksp("com.github.bumptech.glide:ksp:4.16.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") implementation("com.charleskorn.kaml:kaml:0.57.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") } diff --git a/app/src/main/java/be/scri/helpers/DatabaseManagers.kt b/app/src/main/java/be/scri/helpers/DatabaseManagers.kt index d9325f0eb..854655ce3 100644 --- a/app/src/main/java/be/scri/helpers/DatabaseManagers.kt +++ b/app/src/main/java/be/scri/helpers/DatabaseManagers.kt @@ -39,11 +39,16 @@ class DatabaseManagers( /** * A facade method to load the data contract for a given language. * It delegates the loading and parsing logic to the [ContractDataLoader]. + * If the contract is missing, it is downloaded in the background for the next load. * * @param language The language code (e.g., "DE", "FR") for which to load the contract. * * @return A [DataContract] object containing the language's structural metadata, or `null` * if not found or on error. */ - fun getLanguageContract(language: String): DataContract? = contractLoader.loadContract(language) + fun getLanguageContract(language: String): DataContract? { + val contract = contractLoader.loadContract(language) + if (contract == null) contractLoader.downloadMissingContractInBackground(language) + return contract + } } diff --git a/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt b/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt index f7a5c221a..ff64615c5 100644 --- a/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt +++ b/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt @@ -6,6 +6,7 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteException import android.util.Log +import be.scri.data.remote.SqlIdentifier import be.scri.helpers.DatabaseFileManager /** @@ -188,6 +189,8 @@ class ConjugateDataManager( val targetForm = words.first() + require(SqlIdentifier.isValid(targetForm)) { "Invalid column name: $targetForm" } + val db = fileManager.getConjugateDatabase(language = language) var auxResult = "" diff --git a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt index e51996225..455cac35a 100644 --- a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt +++ b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt @@ -8,15 +8,18 @@ import be.scri.data.remote.RetrofitClient import be.scri.data.remote.SqlIdentifier import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration -import com.charleskorn.kaml.YamlException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import retrofit2.HttpException import java.io.File import java.io.IOException +import java.util.concurrent.ConcurrentHashMap /** - * Loads, deserializes, downloads, and saves contract data from local storage, assets, or network. + * Loads, deserializes, downloads, and saves contract data from local storage or network. + * Contracts are stored as `.yaml`; JSON responses are stored as-is since JSON is valid YAML. * @param context The application context. */ class ContractDataLoader( @@ -25,72 +28,38 @@ class ContractDataLoader( companion object { private const val TAG = "ContractDataLoader" private const val CONTRACTS_DIR = "data-contracts" - private val json = - Json { - ignoreUnknownKeys = true - isLenient = true - } private val yaml = Yaml( configuration = YamlConfiguration(strictMode = false), ) - } - /** - * Checks if a contract file exists in local app storage for the specified language. - * - * @param language The language code (e.g., "DE", "EN"). - * @return True if a local .yaml or .json contract file exists, false otherwise. - */ - fun hasContract(language: String): Boolean { - val lang = language.lowercase() - val dir = File(context.filesDir, CONTRACTS_DIR) - return File(dir, "$lang.yaml").exists() || File(dir, "$lang.json").exists() + // Languages with a background download in flight, so repeated keyboard loads don't stack requests. + private val pendingDownloads = ConcurrentHashMap.newKeySet() } + private fun contractFile(language: String) = File(File(context.filesDir, CONTRACTS_DIR), "${language.lowercase()}.yaml") + /** - * Loads and deserializes a data contract for the specified language. - * Checks internal app storage first, falling back to assets. + * Loads and deserializes the locally saved data contract for the specified language. * Gracefully handles file-not-found and parsing errors by returning null. * - * @param language The language code (e.g., "DE", "EN") used to determine the filename (e.g., "de.yaml" or "de.json"). + * @param language The language code (e.g., "DE", "EN") used to determine the filename (e.g., "de.yaml"). * * @return The decoded [DataContract] object if successful, or `null` * if the file does not exist or cannot be parsed. */ fun loadContract(language: String): DataContract? { - val lang = language.lowercase() - Log.d(TAG, "Attempting to load contract for language: $lang") - - val dir = File(context.filesDir, CONTRACTS_DIR) - val localFiles = listOf(File(dir, "$lang.yaml"), File(dir, "$lang.json")) - for (localFile in localFiles) { - if (localFile.exists() && localFile.isFile) { - try { - val parsed = parseContract(localFile.readText()) - if (parsed != null) return parsed - } catch (e: IOException) { - Log.e(TAG, "Error reading local contract file: ${localFile.path}", e) - } - } + val file = contractFile(language) + if (!file.isFile) { + Log.e(TAG, "No local contract file for: ${language.lowercase()}") + return null } - - val assetExtensions = listOf("yaml", "json") - for (ext in assetExtensions) { - val assetPath = "$CONTRACTS_DIR/$lang.$ext" - try { - context.assets.open(assetPath).use { contractFile -> - val content = contractFile.bufferedReader().readText() - val parsed = parseContract(content) - if (parsed != null) return parsed - } - } catch (_: IOException) { - // Asset not found, try next extension. - } + return try { + parseContract(file.readText()) + } catch (e: IOException) { + Log.e(TAG, "Error reading local contract file: ${file.path}", e) + null } - - Log.e(TAG, "Error loading contract file for: $lang. It may not exist.") - return null } /** @@ -102,22 +71,11 @@ class ContractDataLoader( fun parseContract(content: String): DataContract? = try { yaml.decodeFromString(DataContract.serializer(), content) - } catch (_: YamlException) { - parseContractAsJson(content) - } catch (_: SerializationException) { - parseContractAsJson(content) - } catch (_: IllegalArgumentException) { - parseContractAsJson(content) - } - - private fun parseContractAsJson(content: String): DataContract? = - try { - json.decodeFromString(DataContract.serializer(), content) } catch (e: SerializationException) { - Log.e(TAG, "Error parsing contract content as JSON", e) + Log.e(TAG, "Error parsing contract content", e) null } catch (e: IllegalArgumentException) { - Log.e(TAG, "Error parsing contract content as JSON", e) + Log.e(TAG, "Error parsing contract content", e) null } @@ -150,6 +108,7 @@ class ContractDataLoader( for ((_, tenseGroup) in contract.verbs.conjugations) { for ((_, category) in tenseGroup.tenses) { for ((_, form) in category.tenseForms) { + // Bracketed words are checked where they are used in ConjugateDataManager.parseComplexForm. val rawCol = form.value.replace(Regex("""\[.*?\]"""), "").trim() if (rawCol.isNotEmpty() && !SqlIdentifier.isValid(rawCol)) { Log.e(TAG, "Invalid column name in contract verbs: '$rawCol'") @@ -164,18 +123,18 @@ class ContractDataLoader( /** * Validates and saves contract content (YAML or JSON) to local app storage. * Content is parse-checked and SQL-identifier-validated before writing. - * Writing uses a temporary file and atomic rename so a failed or invalid response - * cannot corrupt or overwrite an existing working contract. + * Writing uses a uniquely named temporary file and atomic rename so a failed, invalid + * or concurrent write cannot corrupt or overwrite an existing working contract. * * @param language The language code (e.g., "DE", "EN"). - * @param yamlContent The contract string content to persist. + * @param content The contract string content to persist. * @return True if validated and saved successfully, false otherwise. */ fun saveContract( language: String, - yamlContent: String, + content: String, ): Boolean { - val parsedContract = parseContract(yamlContent) + val parsedContract = parseContract(content) if (parsedContract == null) { Log.e(TAG, "Failed to parse contract content for $language before saving") return false @@ -186,38 +145,24 @@ class ContractDataLoader( } val dir = File(context.filesDir, CONTRACTS_DIR) - if (!dir.exists() && !dir.mkdirs()) { + if (!dir.mkdirs() && !dir.isDirectory) { Log.e(TAG, "Failed to create directory: ${dir.path}") return false } - val lang = language.lowercase() - val isJson = yamlContent.trimStart().startsWith("{") - val extension = if (isJson) "json" else "yaml" - val targetFile = File(dir, "$lang.$extension") - val tempFile = File(dir, "$lang.$extension.tmp") + val targetFile = contractFile(language) return try { - tempFile.writeText(yamlContent) - if (tempFile.renameTo(targetFile)) { - true - } else { - try { - java.nio.file.Files.move( - tempFile.toPath(), - targetFile.toPath(), - java.nio.file.StandardCopyOption.REPLACE_EXISTING, - java.nio.file.StandardCopyOption.ATOMIC_MOVE, - ) - true - } catch (e: IOException) { - Log.e(TAG, "Failed to move temp contract file to target file for $language", e) - tempFile.delete() - false + val tempFile = File.createTempFile(targetFile.name, ".tmp", dir) + try { + tempFile.writeText(content) + tempFile.renameTo(targetFile).also { renamed -> + if (!renamed) Log.e(TAG, "Failed to move temp contract file to target file for $language") } + } finally { + tempFile.delete() } } catch (e: IOException) { Log.e(TAG, "Failed to write contract to temp file for $language", e) - tempFile.delete() false } } @@ -242,4 +187,32 @@ class ContractDataLoader( false } } + + /** + * Loads the local contract for [language], downloading it first if it is missing or unreadable. + * + * @param language The language code (e.g., "DE", "EN"). + * @return The decoded [DataContract], or `null` if it could not be loaded or downloaded. + */ + suspend fun loadOrDownloadContract(language: String): DataContract? = + loadContract(language) ?: if (downloadAndSaveContract(language)) loadContract(language) else null + + /** + * Downloads the contract for [language] in the background if its language database is already + * downloaded, so users who downloaded data before contracts existed get one on their next load. + * + * @param language The language code (e.g., "DE", "EN"). + */ + fun downloadMissingContractInBackground(language: String) { + val lang = language.lowercase() + val hasDatabase = context.getDatabasePath("${lang.uppercase()}LanguageData.sqlite").exists() + if (!hasDatabase || !pendingDownloads.add(lang)) return + CoroutineScope(Dispatchers.IO).launch { + try { + loadOrDownloadContract(lang) + } finally { + pendingDownloads.remove(lang) + } + } + } } diff --git a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt index c1bf2249e..edc46c197 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt @@ -83,12 +83,7 @@ fun ConjugationSelectionScreen( LaunchedEffect(verb, languageAlias) { withContext(Dispatchers.IO) { - val loader = ContractDataLoader(context) - var contract = loader.loadContract(languageAlias) - if (contract == null) { - loader.downloadAndSaveContract(languageAlias) - contract = loader.loadContract(languageAlias) - } + val contract = ContractDataLoader(context).loadOrDownloadContract(languageAlias) if (contract != null) { val fileManager = DatabaseFileManager(context) val manager = ConjugateDataManager(fileManager) diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt index c9c2a89d9..c1e1999b8 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDataDownloadViewModel.kt @@ -73,9 +73,8 @@ class ConjugateDataDownloadViewModel( } } - // After initializing, check for updates on all Completed languages and backfill missing contracts. + // After initializing, check for updates on all Completed languages. checkAllForUpdates() - backfillMissingContracts() _checkUpdateState.value = CheckUpdateState.Idle } @@ -157,60 +156,50 @@ class ConjugateDataDownloadViewModel( val serverLastUpdate = response.contract.updatedAt // Always download when forcing, or when update is available. - if (forceDownload || isUpdateAvailable(localLastUpdate, serverLastUpdate)) { + val updateNeeded = forceDownload || isUpdateAvailable(localLastUpdate, serverLastUpdate) + if (updateNeeded) { val dbHelper = ConjugateDynamicDbHelper(getApplication(), langCode) dbHelper.syncConjugateDatabase(response) + } - val contractLoader = ContractDataLoader(getApplication()) - val contractSaved = contractLoader.downloadAndSaveContract(langCode) - if (!contractSaved) { - val template = - getApplication().getString( - R.string.i18n_app_download_error_server, - ) - val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") - updateErrorState(key, errorMsg) - return@launch + // Refresh the contract along with new data; otherwise download it only if missing or unreadable. + val contractLoader = ContractDataLoader(getApplication()) + val contractReady = + if (updateNeeded) { + contractLoader.downloadAndSaveContract(langCode) + } else { + contractLoader.loadOrDownloadContract(langCode) != null } + if (!contractReady) { + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) + val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") + updateErrorState(key, errorMsg) + return@launch + } - // Save timestamp only if both database and contract saved successfully. - if (isValidTimestamp(serverLastUpdate)) { - prefs.edit().putString("last_conjugate_update_$langCode", serverLastUpdate).apply() - } + // Save timestamp only if both database and contract saved successfully. + if (updateNeeded && isValidTimestamp(serverLastUpdate)) { + prefs.edit().putString("last_conjugate_update_$langCode", serverLastUpdate).apply() + } - withContext(Dispatchers.Main) { - downloadStates[key] = DownloadState.Completed - val template = - getApplication().getString( - R.string.i18n_app_download_menu_ui_conjugate_data_download_success, - ) - val msg = StringUtils.formatStringWithParams(template, displayLang) - showToast(msg) - } - } else { - // Already up to date: Skip the DB work, but ensure contract is downloaded if missing. - val contractLoader = ContractDataLoader(getApplication()) - if (!contractLoader.hasContract(langCode)) { - val contractSaved = contractLoader.downloadAndSaveContract(langCode) - if (!contractSaved) { + withContext(Dispatchers.Main) { + downloadStates[key] = DownloadState.Completed + val msg = + if (updateNeeded) { val template = getApplication().getString( - R.string.i18n_app_download_error_server, + R.string.i18n_app_download_menu_ui_conjugate_data_download_success, ) - val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") - updateErrorState(key, errorMsg) - return@launch - } - } - - withContext(Dispatchers.Main) { - downloadStates[key] = DownloadState.Completed - val msg = + StringUtils.formatStringWithParams(template, displayLang) + } else { getApplication().getString( R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date, ) - showToast(msg) - } + } + showToast(msg) } } catch (e: IOException) { val template = @@ -324,30 +313,6 @@ class ConjugateDataDownloadViewModel( } } - /** - * Backfills missing data contracts for existing users who already downloaded - * databases but lack contract files in local storage. - */ - private fun backfillMissingContracts() { - if (!NetworkMonitor.isOnline(getApplication())) return - - val contractLoader = ContractDataLoader(getApplication()) - val completedKeys = downloadStates.keys.filter { downloadStates[it] == DownloadState.Completed } - - viewModelScope.launch(Dispatchers.IO) { - completedKeys.forEach { key -> - val langCode = - LanguageMappingConstants - .getLanguageAlias(key.replaceFirstChar { it.uppercase() }) - .lowercase() - - if (!contractLoader.hasContract(langCode)) { - contractLoader.downloadAndSaveContract(langCode) - } - } - } - } - /** * Checks for new data updates for all completed languages. */ diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt index 91e0d8581..14b70e5e2 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadViewModel.kt @@ -73,9 +73,8 @@ class DataDownloadViewModel( } } - // After initializing, check for updates on all Completed languages and backfill missing contracts. + // After initializing, check for updates on all Completed languages. checkAllForUpdates() - backfillMissingContracts() _checkUpdateState.value = CheckUpdateState.Idle } @@ -157,60 +156,50 @@ class DataDownloadViewModel( val serverLastUpdate = response.contract.updatedAt // Always download when forcing, or when update is available. - if (forceDownload || isUpdateAvailable(localLastUpdate, serverLastUpdate)) { + val updateNeeded = forceDownload || isUpdateAvailable(localLastUpdate, serverLastUpdate) + if (updateNeeded) { val dbHelper = DynamicDbHelper(getApplication(), langCode) dbHelper.syncDatabase(response) + } - val contractLoader = ContractDataLoader(getApplication()) - val contractSaved = contractLoader.downloadAndSaveContract(langCode) - if (!contractSaved) { - val template = - getApplication().getString( - R.string.i18n_app_download_error_server, - ) - val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") - updateErrorState(key, errorMsg) - return@launch + // Refresh the contract along with new data; otherwise download it only if missing or unreadable. + val contractLoader = ContractDataLoader(getApplication()) + val contractReady = + if (updateNeeded) { + contractLoader.downloadAndSaveContract(langCode) + } else { + contractLoader.loadOrDownloadContract(langCode) != null } + if (!contractReady) { + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) + val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") + updateErrorState(key, errorMsg) + return@launch + } - // Save timestamp only if both database and contract saved successfully. - if (isValidTimestamp(serverLastUpdate)) { - prefs.edit().putString("last_update_$langCode", serverLastUpdate).apply() - } + // Save timestamp only if both database and contract saved successfully. + if (updateNeeded && isValidTimestamp(serverLastUpdate)) { + prefs.edit().putString("last_update_$langCode", serverLastUpdate).apply() + } - withContext(Dispatchers.Main) { - downloadStates[key] = DownloadState.Completed - val template = - getApplication().getString( - R.string.i18n_app_download_menu_ui_download_data_download_success, - ) - val msg = StringUtils.formatStringWithParams(template, displayLang) - showToast(msg) - } - } else { - // Already up to date: Skip the DB work, but ensure contract is downloaded if missing. - val contractLoader = ContractDataLoader(getApplication()) - if (!contractLoader.hasContract(langCode)) { - val contractSaved = contractLoader.downloadAndSaveContract(langCode) - if (!contractSaved) { + withContext(Dispatchers.Main) { + downloadStates[key] = DownloadState.Completed + val msg = + if (updateNeeded) { val template = getApplication().getString( - R.string.i18n_app_download_error_server, + R.string.i18n_app_download_menu_ui_download_data_download_success, ) - val errorMsg = StringUtils.formatStringWithParams(template, "Contract download failed") - updateErrorState(key, errorMsg) - return@launch - } - } - - withContext(Dispatchers.Main) { - downloadStates[key] = DownloadState.Completed - val msg = + StringUtils.formatStringWithParams(template, displayLang) + } else { getApplication().getString( R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date, ) - showToast(msg) - } + } + showToast(msg) } } catch (e: IOException) { val template = @@ -324,30 +313,6 @@ class DataDownloadViewModel( } } - /** - * Backfills missing data contracts for existing users who already downloaded - * databases but lack contract files in local storage. - */ - private fun backfillMissingContracts() { - if (!NetworkMonitor.isOnline(getApplication())) return - - val contractLoader = ContractDataLoader(getApplication()) - val completedKeys = downloadStates.keys.filter { downloadStates[it] == DownloadState.Completed } - - viewModelScope.launch(Dispatchers.IO) { - completedKeys.forEach { key -> - val langCode = - LanguageMappingConstants - .getLanguageAlias(key.replaceFirstChar { it.uppercase() }) - .lowercase() - - if (!contractLoader.hasContract(langCode)) { - contractLoader.downloadAndSaveContract(langCode) - } - } - } - } - /** * Checks for new data updates for all completed languages. */ diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt index cb7a88a9c..d95390e57 100644 --- a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -2,7 +2,6 @@ package be.scri.helpers.data import android.content.Context -import android.content.res.AssetManager import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.AfterEach @@ -13,13 +12,10 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.io.ByteArrayInputStream import java.io.File -import java.io.IOException class ContractDataLoaderTest { private lateinit var context: Context - private lateinit var assetManager: AssetManager private lateinit var tempFilesDir: File private lateinit var loader: ContractDataLoader @@ -139,13 +135,11 @@ class ContractDataLoaderTest { @BeforeEach fun setup() { context = mockk(relaxed = true) - assetManager = mockk(relaxed = true) tempFilesDir = java.nio.file.Files .createTempDirectory("scribe_test_contracts") .toFile() every { context.filesDir } returns tempFilesDir - every { context.assets } returns assetManager loader = ContractDataLoader(context) } @@ -191,24 +185,6 @@ class ContractDataLoaderTest { assertEquals("Word Types", contract?.translations?.wordType?.sectionTitle) } - @Test - fun loadContract_fallbackToAssets_parsesSuccessfully() { - every { assetManager.open("data-contracts/en.yaml") } returns ByteArrayInputStream(sampleYaml.toByteArray()) - - val contract = loader.loadContract("EN") - - assertNotNull(contract) - assertEquals(listOf("gender"), contract?.nouns?.genders?.canonical) - assertEquals( - "nominativeSingular", - contract - ?.nouns - ?.numbers - ?.get(1) - ?.singular, - ) - } - @Test fun saveContract_writesFileAndCanBeLoaded() { val saved = loader.saveContract("DE", sampleYaml) @@ -221,8 +197,6 @@ class ContractDataLoaderTest { @Test fun loadContract_fileNotFound_returnsNull() { - every { assetManager.open(any()) } throws IOException("File not found") - val contract = loader.loadContract("NON_EXISTENT") assertNull(contract) @@ -230,8 +204,9 @@ class ContractDataLoaderTest { @Test fun loadContract_malformedYaml_returnsNull() { - val invalidYaml = "nouns: [invalid: yaml: :" - every { assetManager.open(any()) } returns ByteArrayInputStream(invalidYaml.toByteArray()) + val contractsDir = File(tempFilesDir, "data-contracts") + contractsDir.mkdirs() + File(contractsDir, "en.yaml").writeText("nouns: [invalid: yaml: :") val contract = loader.loadContract("EN") @@ -259,21 +234,10 @@ class ContractDataLoaderTest { } @Test - fun hasContract_returnsTrueWhenFileExists() { - assertFalse(loader.hasContract("EN")) - - val contractsDir = File(tempFilesDir, "data-contracts") - contractsDir.mkdirs() - File(contractsDir, "en.json").writeText(sampleJson) - - assertTrue(loader.hasContract("EN")) - } - - @Test - fun loadContract_fromJsonFile_parsesSuccessfully() { + fun loadContract_jsonContent_parsesSuccessfully() { val contractsDir = File(tempFilesDir, "data-contracts") contractsDir.mkdirs() - File(contractsDir, "en.json").writeText(sampleJson) + File(contractsDir, "en.yaml").writeText(sampleJson) val contract = loader.loadContract("EN") assertNotNull(contract) @@ -289,10 +253,10 @@ class ContractDataLoaderTest { } @Test - fun saveContract_withJsonContent_savesAsJson() { + fun saveContract_withJsonContent_savesAsSingleContractFile() { val saved = loader.saveContract("ES", sampleJson) assertTrue(saved) - assertTrue(File(tempFilesDir, "data-contracts/es.json").exists()) + assertEquals(listOf("es.yaml"), File(tempFilesDir, "data-contracts").list()?.toList()) val contract = loader.loadContract("ES") assertNotNull(contract) From b021dd88d08a9feaf6e477c639b8f34b01f8b6ce Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Tue, 27 Oct 2026 23:30:12 +0530 Subject: [PATCH 7/7] feat: enhance parsing of complex forms in ConjugateDataManager and update validation in ContractDataLoader --- .../be/scri/helpers/data/ConjugateDataManager.kt | 9 +++++---- .../java/be/scri/helpers/data/ContractDataLoader.kt | 2 +- .../be/scri/helpers/data/ContractDataLoaderTest.kt | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt b/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt index ff64615c5..114a72680 100644 --- a/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt +++ b/app/src/main/java/be/scri/helpers/data/ConjugateDataManager.kt @@ -140,7 +140,7 @@ class ConjugateDataManager( form: String, language: String, ): String = - if (form.contains("[")) { + if (form.contains("[") || form.contains("{")) { parseComplexForm(cursor, form, language) } else { try { @@ -153,8 +153,9 @@ class ConjugateDataManager( /** * Parses a complex conjugation form that includes an auxiliary part in brackets, - * such as "[have] past_participle". It combines the auxiliary word with the value - * from the specified database column. + * such as "[have] past_participle" (literal words) or + * "{indicativePresentFirstPersonSingular auxiliaryVerb} pastParticiple" (column lookup). + * It combines the auxiliary word with the value from the specified database column. * * @param cursor The database cursor positioned at the correct row. * @param form The complex form string to parse. @@ -166,7 +167,7 @@ class ConjugateDataManager( form: String, language: String, ): String { - val bracketRegex = Regex("""\[(.*?)]""") + val bracketRegex = Regex("""[\[{](.*?)[]}]""") val match = bracketRegex.find(form) ?: return "" val auxiliaryWords = match.groupValues[1] diff --git a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt index 455cac35a..07a219ba4 100644 --- a/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt +++ b/app/src/main/java/be/scri/helpers/data/ContractDataLoader.kt @@ -109,7 +109,7 @@ class ContractDataLoader( for ((_, category) in tenseGroup.tenses) { for ((_, form) in category.tenseForms) { // Bracketed words are checked where they are used in ConjugateDataManager.parseComplexForm. - val rawCol = form.value.replace(Regex("""\[.*?\]"""), "").trim() + val rawCol = form.value.replace(Regex("""[\[{].*?[]}]"""), "").trim() if (rawCol.isNotEmpty() && !SqlIdentifier.isValid(rawCol)) { Log.e(TAG, "Invalid column name in contract verbs: '$rawCol'") return false diff --git a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt index d95390e57..d8796a448 100644 --- a/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/data/ContractDataLoaderTest.kt @@ -310,4 +310,17 @@ class ContractDataLoaderTest { assertNotNull(contract) assertTrue(loader.validateContract(contract!!)) } + + @Test + fun validateContract_bracketedAuxiliaryForms_returnsTrue() { + val germanStyle = + sampleJson.replace( + "\"value\": \"simplePresent\"", + "\"value\": \"{indicativePresentFirstPersonSingular auxiliaryVerb} pastParticiple\"", + ) + val englishStyle = sampleJson.replace("\"value\": \"simplePresent\"", "\"value\": \"[had been] presentParticiple\"") + + assertTrue(loader.validateContract(loader.parseContract(germanStyle)!!)) + assertTrue(loader.validateContract(loader.parseContract(englishStyle)!!)) + } }