From 9c9013d85b54ca2db74218bd4401d0f885723538 Mon Sep 17 00:00:00 2001 From: Lorenzo Vainigli Date: Sun, 15 Dec 2024 22:24:02 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20language=20selection=20in=20d?= =?UTF-8?q?ebug=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added the ability to select the language of the app. - Added a new dialog to select the language. - Added a new preference to store the selected language. - Added a new helper class to change the locale of the app. - Updated the worker to use the selected language. - Updated the settings screen to add a new option to open the language picker dialog. --- .../foodexpirationdates/model/LocaleHelper.kt | 60 +++++++++ .../model/repository/PreferencesRepository.kt | 25 ++++ .../model/worker/CheckExpirationsWorker.kt | 13 +- .../foodexpirationdates/view/MainActivity.kt | 7 + .../view/composable/LanguagePickerDialog.kt | 121 ++++++++++++++++++ .../view/composable/screen/SettingsScreen.kt | 41 ++++++ app/src/main/res/values-it/strings.xml | 4 + app/src/main/res/values-ja/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 9 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt create mode 100644 app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt new file mode 100644 index 00000000..b9f3d2e9 --- /dev/null +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/LocaleHelper.kt @@ -0,0 +1,60 @@ +package com.lorenzovainigli.foodexpirationdates.model + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import com.lorenzovainigli.foodexpirationdates.view.MainActivity +import java.util.Locale +import kotlin.jvm.java + +enum class Language(val code: String, val label: String) { + SYSTEM("system", "System"), + ARABIC("ar", "العربية"), + CHINESE_TRADITIONAL("zh-TW", "繁體中文"), + ENGLISH("en", "English"), + FRENCH("fr", "Français"), + GERMAN("de", "Deutsch"), + HINDI("hi", "हिन्दी"), + INDONESIAN("id", "Bahasa Indonesia"), + ITALIAN("it", "Italiano"), + JAPANESE("ja", "日本語"), + POLISH("pl", "Polski"), + RUSSIAN("ru", "Русский"), + SPANISH("es", "Español"), + TAMIL("ta", "தமிழ்"), + TURKISH("tr", "Türkçe"), + VIETNAMESE("vi", "Tiếng Việt"); + + companion object { + fun fromCode(code: String): Language { + return Language.entries.find { it.code == code } ?: SYSTEM + } + } +} + +object LocaleHelper { + + fun setLocale(context: Context, language: String): Context { + val locale = if (language == Language.SYSTEM.name) { + Locale.getDefault() + } else { + Locale(language) + } + Locale.setDefault(locale) + + val config = Configuration(context.resources.configuration) + config.setLocale(locale) + config.setLayoutDirection(locale) + + return context.createConfigurationContext(config) + } + + fun changeLanguage(context: Context, newLanguage: String) { + setLocale(context, newLanguage) + val intent = Intent(context, MainActivity::class.java) // or current activity + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + (context as Activity).finish() + } +} diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt index e82043bb..ad593088 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/repository/PreferencesRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.Window import android.view.WindowManager import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.Language import java.lang.Exception import java.text.DateFormat import java.text.SimpleDateFormat @@ -22,6 +23,7 @@ class PreferencesRepository { const val keyTopBarFont = "top_bar_font" const val keyDynamicColors = "dynamic_colors" const val keyMonochromeIcons = "monochrome_icons" + const val keyLanguage = "language" private val availLocaleDateFormats = arrayOf(DateFormat.SHORT, DateFormat.MEDIUM) private val availOtherDateFormats = arrayOf( @@ -213,5 +215,28 @@ class PreferencesRepository { return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) .edit().putBoolean(keyMonochromeIcons, monochromeIconsEnabled).apply() } + + fun getLanguage( + context: Context, + sharedPrefs: String = sharedPrefsName, + ): String { + try { + return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .getString(keyLanguage, Language.SYSTEM.code) + ?: Language.SYSTEM.code + } catch (e: Exception){ + e.printStackTrace() + } + return Language.SYSTEM.code + } + + fun setLanguage( + context: Context, + sharedPrefs: String = sharedPrefsName, + language: String + ) { + return context.getSharedPreferences(sharedPrefs, Context.MODE_PRIVATE) + .edit().putString(keyLanguage, language).apply() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt index 386f5070..e2409308 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/model/worker/CheckExpirationsWorker.kt @@ -4,10 +4,13 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.lorenzovainigli.foodexpirationdates.BuildConfig import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.LocaleHelper import com.lorenzovainigli.foodexpirationdates.model.NotificationManager.Companion.CHANNEL_REMINDERS_ID import com.lorenzovainigli.foodexpirationdates.model.entity.computeExpirationDate import com.lorenzovainigli.foodexpirationdates.model.repository.ExpirationDateRepository +import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository import com.lorenzovainigli.foodexpirationdates.showNotification import kotlinx.coroutines.flow.first import java.util.Calendar @@ -57,10 +60,16 @@ class CheckExpirationsWorker @Inject constructor( var message = "" if (sb.toString().length > 2) message = sb.toString().substring(0, sb.toString().length - 2) + "." + val context = if (BuildConfig.DEBUG) { + LocaleHelper.setLocale( + context = applicationContext, + language = PreferencesRepository.getLanguage(applicationContext) + ) + } else applicationContext showNotification( - context = applicationContext, + context = context, channelId = CHANNEL_REMINDERS_ID, - title = applicationContext.getString(R.string.your_food_is_expiring), + title = context.getString(R.string.your_food_is_expiring), message = message ) return Result.success() diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt index cc688a3b..e28133ba 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/MainActivity.kt @@ -1,5 +1,6 @@ package com.lorenzovainigli.foodexpirationdates.view +import android.content.Context import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -19,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.rememberNavController +import com.lorenzovainigli.foodexpirationdates.model.LocaleHelper import com.lorenzovainigli.foodexpirationdates.model.NotificationManager import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository.Companion.checkAndSetSecureFlags @@ -100,4 +102,9 @@ class MainActivity : ComponentActivity() { } } + override fun attachBaseContext(newBase: Context) { + val locale = PreferencesRepository.getLanguage(newBase) + super.attachBaseContext(LocaleHelper.setLocale(newBase, locale)) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt new file mode 100644 index 00000000..43bc5786 --- /dev/null +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/LanguagePickerDialog.kt @@ -0,0 +1,121 @@ +package com.lorenzovainigli.foodexpirationdates.view.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.Language +import com.lorenzovainigli.foodexpirationdates.model.LocaleHelper +import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository +import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme + +@Composable +fun LanguagePickerDialog( + isDialogOpen: Boolean = true, + onDismiss: () -> Unit = {} +) { + if (isDialogOpen) { + val context = LocalContext.current + val storedLanguage = PreferencesRepository.getLanguage(context) + var selectedLanguage = remember { + mutableStateOf(storedLanguage) + } + Dialog( + onDismissRequest = onDismiss + ) { + Card( + shape = RoundedCornerShape(10.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) + ) { + Text( + modifier = Modifier.padding(4.dp), + text = stringResource(R.string.select_language), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Column( + modifier = Modifier + .height(480.dp) + .verticalScroll(rememberScrollState()) + ) { + Language.entries.forEach { language -> + Row( + modifier = Modifier.fillMaxWidth(1f).clickable( + onClick = { + selectedLanguage.value = language.code + }), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedLanguage.value == language.code, + onClick = { + selectedLanguage.value = language.code + } + ) + Text( + text = language.label, + color = if (selectedLanguage.value == language.code) + MaterialTheme.colorScheme.primary + else Color.Unspecified + ) + } + } + } + Button( + modifier = Modifier.align(Alignment.End), + onClick = { + LocaleHelper.changeLanguage(context, selectedLanguage.value) + PreferencesRepository.setLanguage( + context, + language = selectedLanguage.value + ) + } + ) { + Text(stringResource(R.string.apply)) + } + } + } + } + } +} + +@PreviewLightDark +@Composable +fun LanguagePickerDialogPreview() { + FoodExpirationDatesTheme { + Surface { + LanguagePickerDialog() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt index d3196d60..21fb698b 100644 --- a/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt +++ b/app/src/main/java/com/lorenzovainigli/foodexpirationdates/view/composable/screen/SettingsScreen.kt @@ -5,6 +5,7 @@ import android.os.Build import android.util.Log import android.view.WindowManager import androidx.annotation.RequiresApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -37,7 +39,9 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp +import com.lorenzovainigli.foodexpirationdates.BuildConfig import com.lorenzovainigli.foodexpirationdates.R +import com.lorenzovainigli.foodexpirationdates.model.Language import com.lorenzovainigli.foodexpirationdates.model.NotificationManager import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository import com.lorenzovainigli.foodexpirationdates.model.repository.PreferencesRepository.Companion.getScreenProtectionEnabled @@ -46,6 +50,7 @@ import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme import com.lorenzovainigli.foodexpirationdates.view.MainActivity import com.lorenzovainigli.foodexpirationdates.view.composable.AutoResizedText import com.lorenzovainigli.foodexpirationdates.view.composable.DateFormatDialog +import com.lorenzovainigli.foodexpirationdates.view.composable.LanguagePickerDialog import com.lorenzovainigli.foodexpirationdates.view.composable.NotificationTimeBottomSheet import com.lorenzovainigli.foodexpirationdates.view.composable.SettingsItem import com.lorenzovainigli.foodexpirationdates.view.preview.LanguagePreviews @@ -75,6 +80,9 @@ fun SettingsScreen( var isDateFormatDialogOpened by remember { mutableStateOf(false) } + var isLanguagePickerDialogOpened by remember { + mutableStateOf(false) + } val notificationTimeHour = prefsViewModel?.getNotificationTimeHour(context)?.collectAsState()?.value @@ -117,6 +125,14 @@ fun SettingsScreen( } ) } + prefsViewModel?.let { + LanguagePickerDialog( + isDialogOpen = isLanguagePickerDialogOpened, + onDismiss = { + isLanguagePickerDialogOpened = false + } + ) + } Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -301,6 +317,31 @@ fun SettingsScreen( } ) } + + if (BuildConfig.DEBUG) { + Text( + text = stringResource(R.string.debug_options), + style = MaterialTheme.typography.labelLarge + ) + SettingsItem( + label = stringResource(R.string.language) + ) { + Spacer( + Modifier + .weight(1f) + .fillMaxHeight() + ) + BasicText( + modifier = Modifier.clickable { + isLanguagePickerDialogOpened = true + }, + text = Language.fromCode(PreferencesRepository.getLanguage(context)).label, + style = MaterialTheme.typography.headlineSmall.copy( + color = MaterialTheme.colorScheme.onSurface + ) + ) + } + } } } diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 06b6cd71..a4cfe6f0 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -110,6 +110,10 @@ Colori dinamici Scanner di codici a barre + Applica + Seleziona lingua + Opzioni di debug + Lingua Giorni Mesi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1e742422..db2ee4b8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -109,4 +109,8 @@ スキャンエラー バーコードスキャナー ♥で作られました + 適用 + 言語を選択 + デバッグオプション + 言語 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d25dc5c0..a93ccf68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,6 +112,10 @@ Barcode scanner Made with ❤️ + Apply + Select language + Debug Options + Language Days Months