Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/src/main/java/be/scri/helpers/StringUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package be.scri.helpers

/**
* Utility object for handling string-related operations.
*/
object StringUtils {
/**
* Checks if a word is capitalized (i.e., starts with an uppercase letter).
* @param word The word to check.
* @return `true` if the word is capitalized, `false` otherwise.
*/
fun isWordCapitalized(word: String): Boolean {
if (word.isEmpty()) return false
return word[0].isUpperCase()
}
}
21 changes: 18 additions & 3 deletions app/src/main/java/be/scri/helpers/data/PluralFormsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package be.scri.helpers.data
import DataContract
import android.database.sqlite.SQLiteDatabase
import be.scri.helpers.DatabaseFileManager
import be.scri.helpers.StringUtils.isWordCapitalized

/**
* Manages and queries plural forms of words from the database.
Expand Down Expand Up @@ -49,9 +50,23 @@ class PluralFormsManager(
val pluralCol = numbers.values.firstOrNull()

if (singularCol != null && pluralCol != null) {
fileManager.getLanguageDatabase(language)?.use { db ->
querySpecificPlural(db, singularCol, pluralCol, noun)
}
val wasCapitalized = isWordCapitalized(noun)
val lowerNoun = noun.lowercase()

val result =
fileManager.getLanguageDatabase(language)?.use { db ->
querySpecificPlural(db, singularCol, pluralCol, lowerNoun)
} ?: emptyMap()

if (result.isEmpty()) return emptyMap()

val (singular, plural) = result.entries.first()

val singularOut = if (wasCapitalized) singular.replaceFirstChar { it.uppercase() } else singular

val pluralOut = if (wasCapitalized) plural?.replaceFirstChar { it.uppercase() } else plural

return mapOf(singularOut to pluralOut)
} else {
null
}
Expand Down
42 changes: 39 additions & 3 deletions app/src/main/java/be/scri/helpers/data/TranslationDataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import be.scri.helpers.DatabaseFileManager
import be.scri.helpers.PreferencesHelper
import be.scri.helpers.StringUtils.isWordCapitalized

/**
* Manages translations from a local SQLite database.
Expand Down Expand Up @@ -47,9 +48,44 @@ class TranslationDataManager(

val sourceTable = generateLanguageNameForISOCode(sourceCode)

return fileManager.getTranslationDatabase()?.use { db ->
queryForTranslation(db, sourceTable, destCode, word)
} ?: ""
val isGerman = sourceCode == "de"

val db = fileManager.getTranslationDatabase() ?: return ""

val variants = mutableListOf<String>()

// Try exact match of input.
variants.add(word)

// Add lowercase variants.
if (isGerman || isWordCapitalized(word)) {
variants.add(word.lowercase())
}

// Note: In German canonical noun is capitalization ("buch" → "Buch").
if (isGerman) {
val canonical = word.lowercase().replaceFirstChar { it.uppercase() }
variants.add(canonical)
}

db.use { database ->
for (variant in variants) {
val result = queryForTranslation(database, sourceTable, destCode, variant)

if (result.isNotEmpty()) {
// Non-German rule:
// If the user typed a capitalized word, but the match happened using the lowercase version,
// then re-capitalize the translated result.
if (!isGerman && isWordCapitalized(word) && variant == word.lowercase()) {
return result.replaceFirstChar { it.uppercase() }
}

return result
}
}
}

return ""
}

/**
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2291,14 +2291,15 @@ abstract class GeneralKeyboardIME(
* @param rawInput The verb entered in the command bar.
*/
private fun handleConjugateState(rawInput: String) {
val searchInput = rawInput.lowercase()
currentVerbForConjugation = rawInput
val languageAlias = getLanguageAlias(language)

val tempOutput =
dbManagers.conjugateDataManager.getTheConjugateLabels(
languageAlias,
dataContract,
rawInput,
searchInput,
)

conjugateOutput =
Expand All @@ -2311,7 +2312,7 @@ abstract class GeneralKeyboardIME(
conjugateLabels =
dbManagers.conjugateDataManager.extractConjugateHeadings(
dataContract,
rawInput,
searchInput,
)

currentState =
Expand Down
161 changes: 161 additions & 0 deletions app/src/test/kotlin/be/scri/helpers/data/TranslationDataManagerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package be.scri.helpers.data

import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import be.scri.helpers.DatabaseFileManager
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class TranslationDataManagerTest {
private lateinit var context: Context
private lateinit var fileManager: DatabaseFileManager
private lateinit var db: SQLiteDatabase
private lateinit var manager: TranslationDataManager

@BeforeEach
fun setup() {
context = mockk(relaxed = true)
fileManager = mockk(relaxed = true)
db = mockk(relaxed = true)

manager = TranslationDataManager(context, fileManager)
}

private fun createCursorWithResult(result: String): Cursor =
mockk<Cursor>(relaxed = true) {
every { moveToFirst() } returns true
every { getColumnIndexOrThrow(any()) } returns 0
every { getString(0) } returns result
}

private fun createEmptyCursor(): Cursor =
mockk<Cursor>(relaxed = true) {
every { moveToFirst() } returns false
}

@Test
fun `finds translation using lowercase variant when exact capitalized match fails`() {
every { fileManager.getTranslationDatabase() } returns db

// First query (exact "Book") fails, second query (lowercase "book") succeeds.
every { db.rawQuery(any(), any()) } returnsMany
listOf(
createEmptyCursor(),
createCursorWithResult("livre"),
)

val result = manager.getTranslationDataForAWord("en" to "fr", "Book")

// Should recapitalize the result since input was capitalized.
assertEquals("Livre", result)
}

@Test
fun `does not recapitalize when input is lowercase`() {
every { fileManager.getTranslationDatabase() } returns db

// Exact match succeeds immediately.
every { db.rawQuery(any(), any()) } returns createCursorWithResult("livre")

val result = manager.getTranslationDataForAWord("en" to "fr", "book")

// Input was lowercase, so output stays lowercase.
assertEquals("livre", result)
}

@Test
fun `returns empty string when no variant matches`() {
every { fileManager.getTranslationDatabase() } returns db

// All queries fail.
every { db.rawQuery(any(), any()) } returns createEmptyCursor()

val result = manager.getTranslationDataForAWord("en" to "fr", "nonexistent")

assertEquals("", result)
}

@Test
fun `German capitalized input matches exact`() {
every { fileManager.getTranslationDatabase() } returns db

// User types "Buch", database has "Buch" - direct match.
every { db.rawQuery(any(), any()) } returns createCursorWithResult("book")

val result = manager.getTranslationDataForAWord("de" to "en", "Buch")

assertEquals("book", result)
}

@Test
fun `German source tries canonical capitalization as fallback`() {
every { fileManager.getTranslationDatabase() } returns db

// Simulate: exact "buch" fails, lowercase "buch" fails, canonical "Buch" succeeds.
every { db.rawQuery(any(), any()) } returnsMany
listOf(
createEmptyCursor(),
createEmptyCursor(),
createCursorWithResult("book"),
)

val result = manager.getTranslationDataForAWord("de" to "en", "buch")

assertEquals("book", result)
}

@Test
fun `German capitalized verb finds translation via lowercase fallback`() {
every { fileManager.getTranslationDatabase() } returns db

// User types "Laufen" (capitalized verb), but database has "laufen" (lowercase).
every { db.rawQuery(any(), any()) } returnsMany
listOf(
createEmptyCursor(),
createCursorWithResult("run"),
)

val result = manager.getTranslationDataForAWord("de" to "en", "Laufen")

// German doesn't recapitalize, returns result as-is.
assertEquals("run", result)
}

@Test
fun `returns original word when source and destination are the same`() {
// No database call should happen.
val result = manager.getTranslationDataForAWord("en" to "en", "Book")

assertEquals("Book", result)
verify(exactly = 0) { fileManager.getTranslationDatabase() }
}

@Test
fun `returns empty string when database is unavailable`() {
every { fileManager.getTranslationDatabase() } returns null

val result = manager.getTranslationDataForAWord("en" to "fr", "book")

assertEquals("", result)
}

@Test
fun `returns original word when source language is null`() {
val result = manager.getTranslationDataForAWord(null to "fr", "Book")

assertEquals("Book", result)
}

@Test
fun `returns original word when destination language is null`() {
val result = manager.getTranslationDataForAWord("en" to null, "Book")

assertEquals("Book", result)
}
}
Loading