From 5f6818a1abe756b27f30dda4da3aa9255436e882 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:51:24 +0530 Subject: [PATCH 1/2] feat: localize Scribe-Server API messages for Android (#112) Implements localized strings for all Scribe-Server API-related user-facing messages in the Android app, using the new keys added to Scribe-i18n in scribe-org/Scribe-i18n#115. Changes: - DataDownloadViewModel.kt: localized 7 Toast messages (network error, database error, server error, timeout, already up to date, generic up to date, download success) - ConjugateDataDownloadViewModel.kt: localized 7 Toast messages with conjugate-specific keys where applicable - DataDownloadScreen.kt: 'Update all' button text localized - ConjugateDownloadDataScreen.kt: 'Update all' button text localized - SelectLanguageScreen.kt: confirmation dialog strings localized (download warning and keep source language button) - DynamicDbHelper.kt: re-throws SQLiteException so ViewModels can catch and display localized database error messages - StringUtils.kt: added formatStringWithParams() helper for template string interpolation outside Composable scope - string.xml: added 14 new resource keys synced from i18n submodule Note: i18n submodule pointer will be updated to the merged scribe-org/Scribe-i18n#115 commit in a follow-up. --- .../be/scri/data/remote/DynamicDbHelper.kt | 1 + .../main/java/be/scri/helpers/StringUtils.kt | 24 ++++++++++++++++ .../scri/ui/screens/SelectLanguageScreen.kt | 17 +++++++---- .../ConjugateDataDownloadViewModel.kt | 28 ++++++++++++++----- .../download/ConjugateDownloadDataScreen.kt | 2 +- .../ui/screens/download/DataDownloadScreen.kt | 2 +- .../screens/download/DataDownloadViewModel.kt | 28 ++++++++++++++----- 7 files changed, 81 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/be/scri/data/remote/DynamicDbHelper.kt b/app/src/main/java/be/scri/data/remote/DynamicDbHelper.kt index c0bb1205c..3fc46aef6 100644 --- a/app/src/main/java/be/scri/data/remote/DynamicDbHelper.kt +++ b/app/src/main/java/be/scri/data/remote/DynamicDbHelper.kt @@ -61,6 +61,7 @@ class DynamicDbHelper( db.setTransactionSuccessful() } catch (e: SQLiteException) { Log.e("SCRIBE_DB", "Error during insert: ${e.message}") + throw e } finally { db.endTransaction() db.close() diff --git a/app/src/main/java/be/scri/helpers/StringUtils.kt b/app/src/main/java/be/scri/helpers/StringUtils.kt index 920ba33a1..ed85c8fc5 100644 --- a/app/src/main/java/be/scri/helpers/StringUtils.kt +++ b/app/src/main/java/be/scri/helpers/StringUtils.kt @@ -43,4 +43,28 @@ object StringUtils { } return result } + + /** + * Replaces placeholder variables in a template string with provided parameters. + * + * This helper function works with template strings that use {variable_name} placeholder syntax, + * replacing them in order with the provided parameters. + * + * @param template The template string (e.g., "Network error: {error}") + * @param params Variable number of string parameters to replace placeholders with. + * Placeholders are replaced in the order they appear in the string. + * + * @return The formatted string with all placeholders replaced by the provided parameters. + */ + fun formatStringWithParams( + template: String, + vararg params: String, + ): String { + var result = template + params.forEach { param -> + result = result.replaceFirst(Regex("""\{[^}]+\}"""), param) + } + return result + } } + diff --git a/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt b/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt index 9578e2d4b..9429ed90f 100644 --- a/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt @@ -129,13 +129,20 @@ fun SelectTranslationSourceLanguageScreen( } if (showDialog.value) { + val localizedSelectedLang = getDisplayLanguageName(selectedLanguage.value) + val localizedSavedLang = getDisplayLanguageName(savedLanguage.value) ConfirmationDialog( text = - "You've changed your source translation language. " + - "Would you like to download new data so that you can translate " + - "from ${selectedLanguage.value}?", - textConfirm = "Download data", - textChange = "Keep ${savedLanguage.value}", + be.scri.helpers.StringUtils.stringResourceWithParams( + R.string.i18n_app_settings_keyboard_translation_change_source_tooltip_download_warning, + localizedSelectedLang, + ), + textConfirm = stringResource(R.string.i18n_app__global_download_data), + textChange = + be.scri.helpers.StringUtils.stringResourceWithParams( + R.string.i18n_app_settings_keyboard_translation_change_source_tooltip_keep_source_language, + localizedSavedLang, + ), onConfirm = { // User confirmed - save the new selection permanently. savedLanguage.value = selectedLanguage.value 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 15ab0ee23..5701d1fc5 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 @@ -10,9 +10,11 @@ import android.widget.Toast import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import be.scri.R import be.scri.data.remote.ConjugateDynamicDbHelper import be.scri.data.remote.RetrofitClient import be.scri.helpers.LanguageMappingConstants +import be.scri.helpers.StringUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException @@ -106,7 +108,9 @@ class ConjugateDataDownloadViewModel( } if (currentState == DownloadState.Completed) { - Toast.makeText(getApplication(), "$displayLang conjugate data is already up to date", Toast.LENGTH_SHORT).show() + val template = getApplication().getString(R.string.i18n_app_download_menu_ui_conjugate_data_already_up_to_date) + val msg = StringUtils.formatStringWithParams(template, displayLang) + Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() return } } @@ -144,23 +148,33 @@ class ConjugateDataDownloadViewModel( withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - Toast.makeText(getApplication(), "Download $displayLang conjugate data finished!", Toast.LENGTH_SHORT).show() + val template = getApplication().getString(R.string.i18n_app_download_menu_ui_conjugate_data_download_success) + val msg = StringUtils.formatStringWithParams(template, displayLang) + Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } } else { // Already up to date: Skip the DB work. withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - Toast.makeText(getApplication(), "Already up to date!", Toast.LENGTH_SHORT).show() + val msg = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date) + Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } } } catch (e: IOException) { - updateErrorState(key, "Network Error: ${e.message}") + val template = getApplication().getString(R.string.i18n_app_download_error_network) + val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") + updateErrorState(key, errorMsg) } catch (e: SQLiteException) { - updateErrorState(key, "Database Error: ${e.message}") + val template = getApplication().getString(R.string.i18n_app_download_error_database) + val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") + updateErrorState(key, errorMsg) } catch (e: HttpException) { - updateErrorState(key, "Server Error: ${e.code()}") + val template = getApplication().getString(R.string.i18n_app_download_error_server) + val errorMsg = StringUtils.formatStringWithParams(template, e.code().toString()) + updateErrorState(key, errorMsg) } catch (e: TimeoutCancellationException) { - updateErrorState(key, "Download timed out") + val errorMsg = getApplication().getString(R.string.i18n_app_download_error_timeout) + updateErrorState(key, errorMsg) throw e } finally { // Clean up the job reference when done. diff --git a/app/src/main/java/be/scri/ui/screens/download/ConjugateDownloadDataScreen.kt b/app/src/main/java/be/scri/ui/screens/download/ConjugateDownloadDataScreen.kt index e32e19cd1..46875a66b 100644 --- a/app/src/main/java/be/scri/ui/screens/download/ConjugateDownloadDataScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/download/ConjugateDownloadDataScreen.kt @@ -143,7 +143,7 @@ fun ConjugateDownloadDataScreen( ) { Column(Modifier.padding(vertical = 10.dp, horizontal = 4.dp)) { Text( - text = "Update all", + text = stringResource(R.string.i18n_app_download_menu_ui_download_data_update_all), color = colorResource(R.color.dark_scribe_blue), fontSize = 20.sp, fontWeight = FontWeight.Medium, diff --git a/app/src/main/java/be/scri/ui/screens/download/DataDownloadScreen.kt b/app/src/main/java/be/scri/ui/screens/download/DataDownloadScreen.kt index 33ea5acd1..733b66d35 100644 --- a/app/src/main/java/be/scri/ui/screens/download/DataDownloadScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/download/DataDownloadScreen.kt @@ -295,7 +295,7 @@ private fun LanguagesListSection( ) { Column(Modifier.padding(vertical = 10.dp, horizontal = 4.dp)) { Text( - text = "Update all", + text = stringResource(R.string.i18n_app_download_menu_ui_download_data_update_all), color = colorResource(R.color.dark_scribe_blue), fontSize = 20.sp, fontWeight = FontWeight.Medium, 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 93045dca9..92ff79116 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 @@ -10,9 +10,11 @@ import android.widget.Toast import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import be.scri.R import be.scri.data.remote.DynamicDbHelper import be.scri.data.remote.RetrofitClient import be.scri.helpers.LanguageMappingConstants +import be.scri.helpers.StringUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException @@ -106,7 +108,9 @@ class DataDownloadViewModel( } if (currentState == DownloadState.Completed) { - Toast.makeText(getApplication(), "$displayLang data is already up to date", Toast.LENGTH_SHORT).show() + val template = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_already_up_to_date) + val msg = StringUtils.formatStringWithParams(template, displayLang) + Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() return } } @@ -144,23 +148,33 @@ class DataDownloadViewModel( withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - Toast.makeText(getApplication(), "Download $displayLang data finished!", Toast.LENGTH_SHORT).show() + val template = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_download_success) + val msg = StringUtils.formatStringWithParams(template, displayLang) + Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } } else { // Already up to date: Skip the DB work. withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - Toast.makeText(getApplication(), "Already up to date!", Toast.LENGTH_SHORT).show() + val msg = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date) + Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } } } catch (e: IOException) { - updateErrorState(key, "Network Error: ${e.message}") + val template = getApplication().getString(R.string.i18n_app_download_error_network) + val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") + updateErrorState(key, errorMsg) } catch (e: SQLiteException) { - updateErrorState(key, "Database Error: ${e.message}") + val template = getApplication().getString(R.string.i18n_app_download_error_database) + val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") + updateErrorState(key, errorMsg) } catch (e: HttpException) { - updateErrorState(key, "Server Error: ${e.code()}") + val template = getApplication().getString(R.string.i18n_app_download_error_server) + val errorMsg = StringUtils.formatStringWithParams(template, e.code().toString()) + updateErrorState(key, errorMsg) } catch (e: TimeoutCancellationException) { - updateErrorState(key, "Download timed out") + val errorMsg = getApplication().getString(R.string.i18n_app_download_error_timeout) + updateErrorState(key, errorMsg) throw e } finally { // Clean up the job reference when done. From f840a2f8c059906bc8fcbfca14187a44b801ced2 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:51:11 +0530 Subject: [PATCH 2/2] fix: resolve ktlint and detekt formatting errors --- .../main/java/be/scri/helpers/StringUtils.kt | 1 - .../scri/ui/screens/SelectLanguageScreen.kt | 5 +-- .../ConjugateDataDownloadViewModel.kt | 35 +++++++++++++++---- .../screens/download/DataDownloadViewModel.kt | 35 +++++++++++++++---- .../kotlin/be/scri/helpers/StringUtilsTest.kt | 15 ++++++++ 5 files changed, 74 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/be/scri/helpers/StringUtils.kt b/app/src/main/java/be/scri/helpers/StringUtils.kt index ed85c8fc5..a5475b389 100644 --- a/app/src/main/java/be/scri/helpers/StringUtils.kt +++ b/app/src/main/java/be/scri/helpers/StringUtils.kt @@ -67,4 +67,3 @@ object StringUtils { return result } } - diff --git a/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt b/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt index 9429ed90f..9cf62761f 100644 --- a/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/SelectLanguageScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.edit import be.scri.R +import be.scri.helpers.StringUtils import be.scri.ui.common.ScribeBaseScreen import be.scri.ui.common.appcomponents.ConfirmationDialog @@ -133,13 +134,13 @@ fun SelectTranslationSourceLanguageScreen( val localizedSavedLang = getDisplayLanguageName(savedLanguage.value) ConfirmationDialog( text = - be.scri.helpers.StringUtils.stringResourceWithParams( + StringUtils.stringResourceWithParams( R.string.i18n_app_settings_keyboard_translation_change_source_tooltip_download_warning, localizedSelectedLang, ), textConfirm = stringResource(R.string.i18n_app__global_download_data), textChange = - be.scri.helpers.StringUtils.stringResourceWithParams( + StringUtils.stringResourceWithParams( R.string.i18n_app_settings_keyboard_translation_change_source_tooltip_keep_source_language, localizedSavedLang, ), 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 5701d1fc5..637e3d43a 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 @@ -108,7 +108,10 @@ class ConjugateDataDownloadViewModel( } if (currentState == DownloadState.Completed) { - val template = getApplication().getString(R.string.i18n_app_download_menu_ui_conjugate_data_already_up_to_date) + val template = + getApplication().getString( + R.string.i18n_app_download_menu_ui_conjugate_data_already_up_to_date, + ) val msg = StringUtils.formatStringWithParams(template, displayLang) Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() return @@ -148,7 +151,10 @@ class ConjugateDataDownloadViewModel( withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - val template = getApplication().getString(R.string.i18n_app_download_menu_ui_conjugate_data_download_success) + val template = + getApplication().getString( + R.string.i18n_app_download_menu_ui_conjugate_data_download_success, + ) val msg = StringUtils.formatStringWithParams(template, displayLang) Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } @@ -156,24 +162,39 @@ class ConjugateDataDownloadViewModel( // Already up to date: Skip the DB work. withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - val msg = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date) + val msg = + getApplication().getString( + R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date, + ) Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } } } catch (e: IOException) { - val template = getApplication().getString(R.string.i18n_app_download_error_network) + val template = + getApplication().getString( + R.string.i18n_app_download_error_network, + ) val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") updateErrorState(key, errorMsg) } catch (e: SQLiteException) { - val template = getApplication().getString(R.string.i18n_app_download_error_database) + val template = + getApplication().getString( + R.string.i18n_app_download_error_database, + ) val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") updateErrorState(key, errorMsg) } catch (e: HttpException) { - val template = getApplication().getString(R.string.i18n_app_download_error_server) + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) val errorMsg = StringUtils.formatStringWithParams(template, e.code().toString()) updateErrorState(key, errorMsg) } catch (e: TimeoutCancellationException) { - val errorMsg = getApplication().getString(R.string.i18n_app_download_error_timeout) + val errorMsg = + getApplication().getString( + R.string.i18n_app_download_error_timeout, + ) updateErrorState(key, errorMsg) throw e } finally { 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 92ff79116..36a057e83 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 @@ -108,7 +108,10 @@ class DataDownloadViewModel( } if (currentState == DownloadState.Completed) { - val template = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_already_up_to_date) + val template = + getApplication().getString( + R.string.i18n_app_download_menu_ui_download_data_already_up_to_date, + ) val msg = StringUtils.formatStringWithParams(template, displayLang) Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() return @@ -148,7 +151,10 @@ class DataDownloadViewModel( withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - val template = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_download_success) + val template = + getApplication().getString( + R.string.i18n_app_download_menu_ui_download_data_download_success, + ) val msg = StringUtils.formatStringWithParams(template, displayLang) Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } @@ -156,24 +162,39 @@ class DataDownloadViewModel( // Already up to date: Skip the DB work. withContext(Dispatchers.Main) { downloadStates[key] = DownloadState.Completed - val msg = getApplication().getString(R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date) + val msg = + getApplication().getString( + R.string.i18n_app_download_menu_ui_download_data_generic_already_up_to_date, + ) Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show() } } } catch (e: IOException) { - val template = getApplication().getString(R.string.i18n_app_download_error_network) + val template = + getApplication().getString( + R.string.i18n_app_download_error_network, + ) val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") updateErrorState(key, errorMsg) } catch (e: SQLiteException) { - val template = getApplication().getString(R.string.i18n_app_download_error_database) + val template = + getApplication().getString( + R.string.i18n_app_download_error_database, + ) val errorMsg = StringUtils.formatStringWithParams(template, e.message ?: "") updateErrorState(key, errorMsg) } catch (e: HttpException) { - val template = getApplication().getString(R.string.i18n_app_download_error_server) + val template = + getApplication().getString( + R.string.i18n_app_download_error_server, + ) val errorMsg = StringUtils.formatStringWithParams(template, e.code().toString()) updateErrorState(key, errorMsg) } catch (e: TimeoutCancellationException) { - val errorMsg = getApplication().getString(R.string.i18n_app_download_error_timeout) + val errorMsg = + getApplication().getString( + R.string.i18n_app_download_error_timeout, + ) updateErrorState(key, errorMsg) throw e } finally { diff --git a/app/src/test/kotlin/be/scri/helpers/StringUtilsTest.kt b/app/src/test/kotlin/be/scri/helpers/StringUtilsTest.kt index 9701d1143..275bc8585 100644 --- a/app/src/test/kotlin/be/scri/helpers/StringUtilsTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/StringUtilsTest.kt @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later package be.scri.helpers +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -35,4 +36,18 @@ class StringUtilsTest { fun isWordCapitalized_returnsFalse_forNumberStartingString() { assertFalse(StringUtils.isWordCapitalized("1hello")) } + + @Test + fun formatStringWithParams_replacesPlaceholdersInOrder() { + val template = "Hello {name}, welcome to {place}." + val result = StringUtils.formatStringWithParams(template, "Alice", "Wonderland") + assertEquals("Hello Alice, welcome to Wonderland.", result) + } + + @Test + fun formatStringWithParams_doesNotChangeTemplate_ifNoPlaceholders() { + val template = "Hello World" + val result = StringUtils.formatStringWithParams(template, "Alice") + assertEquals("Hello World", result) + } }