From bdc2961b199f4b16eeb6b70aa6e70ddd7d75ad5b Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Tue, 17 Oct 2023 13:21:49 -0600 Subject: [PATCH 01/16] make appLanguage and appLanguageFlow accessible from a Context object also localize the context object for older versions of Android, without this context.getString() doesn't reflect the currently configured app language --- library/base/build.gradle.kts | 1 + .../org/cru/godtools/base/AppLanguage.kt | 23 +++++++++++++++++++ .../kotlin/org/cru/godtools/base/Settings.kt | 19 ++++----------- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt diff --git a/library/base/build.gradle.kts b/library/base/build.gradle.kts index 1c2e017126..a4256fff1c 100644 --- a/library/base/build.gradle.kts +++ b/library/base/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.gtoSupport.androidx.core) implementation(libs.gtoSupport.androidx.lifecycle) implementation(libs.gtoSupport.kotlin.coroutines) implementation(libs.gtoSupport.util) diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt b/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt new file mode 100644 index 0000000000..749022d539 --- /dev/null +++ b/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt @@ -0,0 +1,23 @@ +package org.cru.godtools.base + +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import java.util.Locale +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import org.ccci.gto.android.common.androidx.core.content.localizeIfPossible + +val Context.appLanguage: Locale + get() = localizeIfPossible(AppCompatDelegate.getApplicationLocales()) + .getString(R.string.normalized_app_language) + .let { Locale.forLanguageTag(it) } + +fun Context.getAppLanguageFlow(): Flow = flow { + // TODO: is there a way to actively listen for changes? + while (true) { + emit(appLanguage) + delay(1_000 / 60) + } +}.distinctUntilChanged() diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt b/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt index f12dd99143..d5c019bebd 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt @@ -15,11 +15,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import org.ccci.gto.android.common.androidx.lifecycle.getBooleanLiveData @@ -62,19 +60,12 @@ class Settings internal constructor(private val context: Context, coroutineScope // region Language Settings var appLanguage: Locale - get() = Locale.forLanguageTag(context.getString(R.string.normalized_app_language)) - set(value) { - AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(value)) - } + get() = context.appLanguage + set(value) = AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(value)) - val appLanguageFlow: Flow = flow { - // TODO: is there a way to actively listen for changes? - while (true) { - delay(1_000 / 60) - emit(appLanguage) - } - }.shareIn(coroutineScope, SharingStarted.WhileSubscribed()) - .onStart { emit(appLanguage) } + val appLanguageFlow: Flow = context.getAppLanguageFlow() + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(5_000)) + .onStart { emit(context.appLanguage) } .distinctUntilChanged() // endregion Language Settings From 35aa254284ec6aefbc1e90cee820fb63af3895f7 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Tue, 17 Oct 2023 13:23:13 -0600 Subject: [PATCH 02/16] localize the app language name in the app language this works around older versions of Android not correctly updating Locale.getDefault() --- .../org/cru/godtools/ui/languages/LanguageSettingsLayout.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageSettingsLayout.kt b/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageSettingsLayout.kt index 9b1fa8a0d8..32f6dd9055 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageSettingsLayout.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageSettingsLayout.kt @@ -128,7 +128,7 @@ internal fun LanguageSettingsLayout( .padding(end = 6.dp) .size(12.dp) ) - Text(appLanguage.displayName) + Text(appLanguage.getDisplayName(appLanguage)) Icon(Icons.Default.ArrowDropDown, null, modifier = Modifier.size(24.dp)) } } From 9f05a6c17220268f04431ec3556c316ce80d328b Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 14:55:06 -0400 Subject: [PATCH 03/16] provide a LocalAppLanguage that provides the current AppLanguage to composables --- library/base/build.gradle.kts | 6 ++++- .../org/cru/godtools/base/LocalAppLanguage.kt | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt diff --git a/library/base/build.gradle.kts b/library/base/build.gradle.kts index a4256fff1c..cf17c47753 100644 --- a/library/base/build.gradle.kts +++ b/library/base/build.gradle.kts @@ -3,7 +3,11 @@ plugins { alias(libs.plugins.ksp) } -android.namespace = "org.cru.godtools.base" +android { + namespace = "org.cru.godtools.base" + + configureCompose(project) +} onesky { sourceStringFiles = listOf( diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt b/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt new file mode 100644 index 0000000000..8f01365fc9 --- /dev/null +++ b/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt @@ -0,0 +1,24 @@ +package org.cru.godtools.base + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalContext +import java.util.Locale + +object LocalAppLanguage { + private val LocalComposition = staticCompositionLocalOf { null } + + /** + * Returns current App Language value + */ + val current: Locale + @Composable + get() = LocalComposition.current + ?: LocalContext.current.let { it.getAppLanguageFlow().collectAsState(it.appLanguage).value } + + /** + * Associates a [LocalAppLanguage] key to a value in a call to [CompositionLocalProvider]. + */ + infix fun provides(locale: Locale) = LocalComposition.provides(locale) +} From 353b08d6af4472e77f64f12c8bd684c0b5868855 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 16:48:31 -0400 Subject: [PATCH 04/16] add an extension method for filtering languages based on the display & native name --- .../main/kotlin/org/cru/godtools/model/Language.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt b/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt index b696910896..c189a35cb4 100644 --- a/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt +++ b/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt @@ -36,6 +36,19 @@ class Language : Base() { fun Collection.getSortedDisplayNames(context: Context?, displayLocale: Locale? = null) = toDisplayNameSortedMap(context, displayLocale).keys.toList() + fun Collection.filterByDisplayAndNativeName( + query: String, + context: Context, + appLanguage: Locale, + ): List { + val terms = query.split(Regex("\\s+")).filter { it.isNotBlank() } + return filter { + val displayName by lazy { it.getDisplayName(context, appLanguage) } + val nativeName by lazy { it.getDisplayName(context, it.code) } + terms.all { displayName.contains(it, true) || nativeName.contains(it, true) } + } + } + @VisibleForTesting internal val Locale?.primaryCollator: Collator get() = Collator.getInstance(this ?: Locale.getDefault()).also { it.strength = Collator.PRIMARY } From 1ccdbc31349236b548c53b2ac15944acc0acdc76 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 14:55:39 -0400 Subject: [PATCH 05/16] update ToolFilters to utilize filterByDisplayAndNativeName() --- .../cru/godtools/ui/dashboard/tools/ToolFilters.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt index f07b1dcf3c..39213c75ab 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt @@ -32,8 +32,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import org.cru.godtools.R +import org.cru.godtools.base.LocalAppLanguage import org.cru.godtools.base.ui.theme.GodToolsTheme import org.cru.godtools.base.ui.util.getToolCategoryName +import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName import org.cru.godtools.ui.languages.LanguageName private val POPUP_MAX_HEIGHT = 600.dp @@ -127,16 +129,10 @@ private fun LanguageFilter(viewModel: ToolsViewModel, modifier: Modifier = Modif onDismissRequest = { expanded = false }, modifier = Modifier.heightIn(max = POPUP_MAX_HEIGHT), ) { + val appLanguage = LocalAppLanguage.current var filter by rememberSaveable { mutableStateOf("") } val languages by remember { - derivedStateOf { - val terms = filter.split(Regex("\\s+")).filter { it.isNotBlank() } - rawLanguages.filter { - val displayName by lazy { it.getDisplayName(context) } - val nativeName by lazy { it.getDisplayName(context, it.code) } - terms.all { displayName.contains(it, true) || nativeName.contains(it, true) } - } - } + derivedStateOf { rawLanguages.filterByDisplayAndNativeName(filter, context, appLanguage) } } SearchBar( From a3aef988238809de01e141bc2ec58b046840873a Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 16:11:18 -0400 Subject: [PATCH 06/16] update DownloadableLanguagesViewModel to leverage filterByDisplayAndNativeName() --- .../DownloadableLanguagesViewModel.kt | 25 +++++++++---------- .../DownloadableLanguagesViewModelTest.kt | 8 ++++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModel.kt b/app/src/main/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModel.kt index fb2411bac7..753755cd5c 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModel.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModel.kt @@ -12,11 +12,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.cru.godtools.base.Settings import org.cru.godtools.db.repository.LanguagesRepository import org.cru.godtools.model.Language +import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName import org.cru.godtools.sync.GodToolsSyncService private const val KEY_FLOATED_LANGUAGES = "floatedLanguages" @@ -26,6 +27,7 @@ private const val KEY_SEARCH_QUERY = "searchQuery" class DownloadableLanguagesViewModel @Inject constructor( @ApplicationContext context: Context, languagesRepository: LanguagesRepository, + settings: Settings, syncService: GodToolsSyncService, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -35,25 +37,22 @@ class DownloadableLanguagesViewModel @Inject constructor( private var floatedLanguages: Set? get() = savedStateHandle.get>(KEY_FLOATED_LANGUAGES)?.toSet() set(value) = savedStateHandle.set(KEY_FLOATED_LANGUAGES, value?.let { ArrayList(it) }) - val languages = languagesRepository.getLanguagesFlow() - .map { + private val sortedLanguages = languagesRepository.getLanguagesFlow() + .combine(settings.appLanguageFlow) { langs, appLanguage -> val floated = floatedLanguages - ?: it.filter { it.isAdded }.map { it.code }.toSet().also { floatedLanguages = it } - it.sortedWith( + ?: langs.filter { it.isAdded }.mapTo(mutableSetOf()) { it.code }.also { floatedLanguages = it } + langs.sortedWith( compareByDescending { it.code in floated } - .then(Language.displayNameComparator(context)) + .then(Language.displayNameComparator(context, appLanguage)) ) } .flowOn(Dispatchers.Default) - .combine(searchQuery.map { it.split(Regex("\\s+")).filter { it.isNotBlank() } }) { langs, terms -> - langs.filter { - val displayName by lazy { it.getDisplayName(context) } - val nativeName by lazy { it.getDisplayName(context, it.code) } - terms.all { displayName.contains(it, true) || nativeName.contains(it, true) } - } - } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + val languages = combine(sortedLanguages, settings.appLanguageFlow, searchQuery) { it, appLanguage, query -> + it.filterByDisplayAndNativeName(query, context, appLanguage) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + // region Sync logic init { viewModelScope.launch { syncService.syncLanguages() } diff --git a/app/src/test/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModelTest.kt b/app/src/test/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModelTest.kt index b5ccfb9264..4230f34728 100644 --- a/app/src/test/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModelTest.kt +++ b/app/src/test/kotlin/org/cru/godtools/ui/languages/downloadable/DownloadableLanguagesViewModelTest.kt @@ -17,12 +17,14 @@ import kotlin.test.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import org.cru.godtools.base.Settings import org.cru.godtools.db.repository.LanguagesRepository import org.cru.godtools.model.Language import org.cru.godtools.sync.GodToolsSyncService @@ -33,6 +35,7 @@ import org.robolectric.annotation.Config @Config(application = Application::class) @OptIn(ExperimentalCoroutinesApi::class) class DownloadableLanguagesViewModelTest { + private val appLanguageFlow = MutableStateFlow(Locale.ENGLISH) private val languages = MutableSharedFlow>() private val context: Context get() = ApplicationProvider.getApplicationContext() @@ -40,6 +43,9 @@ class DownloadableLanguagesViewModelTest { every { getLanguagesFlow() } returns languages } private val savedStateHandle = SavedStateHandle() + private val settings: Settings = mockk { + every { appLanguageFlow } returns this@DownloadableLanguagesViewModelTest.appLanguageFlow + } private val syncService: GodToolsSyncService = mockk { coEvery { syncLanguages() } returns true } @@ -53,6 +59,7 @@ class DownloadableLanguagesViewModelTest { viewModel = DownloadableLanguagesViewModel( context = context, languagesRepository = languagesRepository, + settings = settings, syncService = syncService, savedStateHandle = savedStateHandle, ) @@ -70,6 +77,7 @@ class DownloadableLanguagesViewModelTest { val viewModel2 = DownloadableLanguagesViewModel( context = context, languagesRepository = languagesRepository, + settings = settings, syncService = syncService, savedStateHandle = savedStateHandle ) From 8be3dfcb93b4014b2c76da21a2861574f4991694 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 14:56:25 -0400 Subject: [PATCH 07/16] utilize the AppLanguage when displaying LanguageName labels --- .../org/cru/godtools/ui/languages/LanguageLayouts.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageLayouts.kt b/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageLayouts.kt index 36dae41b82..ccd980ee81 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageLayouts.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/languages/LanguageLayouts.kt @@ -18,15 +18,21 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.sp import java.util.Locale +import org.cru.godtools.base.LocalAppLanguage import org.cru.godtools.model.Language @Composable -internal fun LanguageName(locale: Locale) = LanguageName(locale.displayName, locale.getDisplayName(locale)) +internal fun LanguageName(locale: Locale) = + LanguageName(locale.getDisplayName(LocalAppLanguage.current), locale.getDisplayName(locale)) @Composable internal fun LanguageName(language: Language, modifier: Modifier = Modifier) { val context = LocalContext.current - LanguageName(language.getDisplayName(context), language.getDisplayName(context, language.code), modifier) + LanguageName( + language.getDisplayName(context, LocalAppLanguage.current), + language.getDisplayName(context, language.code), + modifier + ) } private const val LANGUAGE_NAME_GAP = "[gap]" From 5a64632e8c033bb7b5c70858f8c4268b0267092e Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 15:12:10 -0400 Subject: [PATCH 08/16] render the selected filter language using the current app language --- .../kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt index 39213c75ab..15e6bb0718 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt @@ -117,7 +117,8 @@ private fun LanguageFilter(viewModel: ToolsViewModel, modifier: Modifier = Modif ) { val language by viewModel.selectedLanguage.collectAsState() Text( - language?.getDisplayName(context) ?: stringResource(R.string.dashboard_tools_section_filter_language_any), + text = language?.getDisplayName(context, LocalAppLanguage.current) + ?: stringResource(R.string.dashboard_tools_section_filter_language_any), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) From 95008948ab152fe055f067ee4e033813c67f14e3 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 19 Oct 2023 16:29:57 -0400 Subject: [PATCH 09/16] make sure the available in language label is translated to the app language --- .../kotlin/org/cru/godtools/ui/tools/AvailableInLanguage.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/tools/AvailableInLanguage.kt b/app/src/main/kotlin/org/cru/godtools/ui/tools/AvailableInLanguage.kt index acd8150013..dee2af7aaa 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/tools/AvailableInLanguage.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/tools/AvailableInLanguage.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import org.cru.godtools.R +import org.cru.godtools.base.LocalAppLanguage import org.cru.godtools.model.Language import org.cru.godtools.model.Translation @@ -58,8 +59,9 @@ internal fun AvailableInLanguage( horizontalArrangement = horizontalArrangement, modifier = modifier.widthIn(min = 50.dp) ) { + val appLanguage = LocalAppLanguage.current val context = LocalContext.current - val name = remember(language, context) { language?.getDisplayName(context).orEmpty() } + val name = remember(language, context, appLanguage) { language?.getDisplayName(context, appLanguage).orEmpty() } Text( if (available) name else stringResource(R.string.tool_card_label_language_unavailable, name), From efcca894668563c19dc9e97ab6e8c0bebeb7bb89 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 13:02:51 -0400 Subject: [PATCH 10/16] update SettingsBottomSheetDialogFragment to utilize the appLanguageFlow --- .../ui/settings/SettingsBottomSheetDialogFragment.kt | 2 -- .../SettingsBottomSheetDialogFragmentDataModel.kt | 11 +++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragment.kt b/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragment.kt index 951c161d13..4941a3544c 100644 --- a/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragment.kt +++ b/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragment.kt @@ -18,7 +18,6 @@ import org.cru.godtools.base.tool.activity.BaseToolActivity import org.cru.godtools.base.tool.activity.MultiLanguageToolActivityDataModel import org.cru.godtools.base.tool.ui.shareable.ShareableImageBottomSheetDialogFragment import org.cru.godtools.base.ui.languages.LanguagesDropdownAdapter -import org.cru.godtools.base.util.deviceLocale import org.cru.godtools.model.Language import org.cru.godtools.shared.tool.parser.model.shareable.ShareableImage import org.cru.godtools.tool.tract.R @@ -71,7 +70,6 @@ class SettingsBottomSheetDialogFragment : activityDataModel.toolCode.filterNotNull().collect(dataModel.toolCode) } } - context?.deviceLocale?.let { dataModel.deviceLocale.value = it } } // endregion Data Model diff --git a/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragmentDataModel.kt b/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragmentDataModel.kt index a48d2e41be..37f5b99f15 100644 --- a/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragmentDataModel.kt +++ b/ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/settings/SettingsBottomSheetDialogFragmentDataModel.kt @@ -1,7 +1,6 @@ package org.cru.godtools.tract.ui.settings import android.content.Context -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.distinctUntilChanged @@ -10,12 +9,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import org.ccci.gto.android.common.androidx.lifecycle.combineWith -import org.cru.godtools.base.util.deviceLocale +import org.cru.godtools.base.Settings import org.cru.godtools.db.repository.LanguagesRepository import org.cru.godtools.db.repository.TranslationsRepository import org.cru.godtools.model.Language.Companion.sortedByDisplayName @@ -25,18 +24,18 @@ import org.cru.godtools.model.Language.Companion.sortedByDisplayName class SettingsBottomSheetDialogFragmentDataModel @Inject constructor( @ApplicationContext context: Context, languagesRepository: LanguagesRepository, + settings: Settings, translationsRepository: TranslationsRepository, ) : ViewModel() { val toolCode = MutableStateFlow(null) - val deviceLocale = MutableLiveData(context.deviceLocale) private val rawLanguages = toolCode .flatMapLatest { it?.let { translationsRepository.getTranslationsFlowForTool(it) } ?: flowOf(emptyList()) } .map { it.map { it.languageCode }.toSet() } .distinctUntilChanged() .flatMapLatest { languagesRepository.getLanguagesFlowForLocales(it) } + val sortedLanguages = settings.appLanguageFlow + .combine(rawLanguages) { locale, languages -> languages.sortedByDisplayName(context, locale) } .asLiveData() - val sortedLanguages = deviceLocale.distinctUntilChanged() - .combineWith(rawLanguages) { locale, languages -> languages.sortedByDisplayName(context, locale) } .distinctUntilChanged() } From 2783aebedc9ad69dd152d1a1eac7c6bb114841fb Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 13:11:57 -0400 Subject: [PATCH 11/16] use the appLanguage for the TutorialLayout --- .../org/cru/godtools/tutorial/layout/TutorialLayout.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/layout/TutorialLayout.kt b/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/layout/TutorialLayout.kt index 159ead2f10..887d75b43a 100644 --- a/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/layout/TutorialLayout.kt +++ b/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/layout/TutorialLayout.kt @@ -24,15 +24,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import com.google.accompanist.pager.HorizontalPagerIndicator -import java.util.Locale import kotlinx.coroutines.launch import org.ccci.gto.android.common.androidx.compose.material3.ui.appbar.AppBarActionButton import org.ccci.gto.android.common.androidx.compose.ui.draw.invisibleIf import org.cru.godtools.analytics.compose.RecordAnalyticsScreen -import org.cru.godtools.base.util.deviceLocale +import org.cru.godtools.base.LocalAppLanguage import org.cru.godtools.tutorial.Action import org.cru.godtools.tutorial.Page import org.cru.godtools.tutorial.PageSet @@ -42,9 +40,8 @@ import org.cru.godtools.tutorial.analytics.model.TutorialAnalyticsScreenEvent @Composable @OptIn(ExperimentalFoundationApi::class) internal fun TutorialLayout(pageSet: PageSet, onTutorialAction: (Action) -> Unit = {}) { - val context = LocalContext.current val coroutineScope = rememberCoroutineScope() - val locale = context.deviceLocale ?: Locale.getDefault() + val locale = LocalAppLanguage.current val pages = remember(pageSet, locale) { pageSet.pagesFor(locale) } val pagerState = rememberPagerState { pages.size } From b7ee1a98af9358637ac0f4ed40c45b6cd1c995df Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 14:16:06 -0400 Subject: [PATCH 12/16] update analytics events in DrawerMenuLayout to utilize the appLanguage instead of deviceLocale --- .../org/cru/godtools/ui/drawer/DrawerMenuLayout.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/drawer/DrawerMenuLayout.kt b/app/src/main/kotlin/org/cru/godtools/ui/drawer/DrawerMenuLayout.kt index 114b192e89..f338149af2 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/drawer/DrawerMenuLayout.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/drawer/DrawerMenuLayout.kt @@ -57,8 +57,8 @@ import org.ccci.gto.android.common.androidx.compose.material3.ui.navigationdrawe import org.cru.godtools.BuildConfig import org.cru.godtools.R import org.cru.godtools.analytics.model.AnalyticsScreenEvent +import org.cru.godtools.base.appLanguage import org.cru.godtools.base.ui.compose.LocalEventBus -import org.cru.godtools.base.util.deviceLocale import org.cru.godtools.shared.analytics.AnalyticsActionNames import org.cru.godtools.shared.analytics.AnalyticsScreenNames import org.cru.godtools.tutorial.PageSet @@ -240,7 +240,7 @@ fun DrawerContentLayout( selected = false, onClick = { eventBus.post( - AnalyticsScreenEvent(AnalyticsActionNames.PLATFORM_SHARE_GODTOOLS, context.deviceLocale) + AnalyticsScreenEvent(AnalyticsActionNames.PLATFORM_SHARE_GODTOOLS, context.appLanguage) ) context.shareGodTools() dismissDrawer() @@ -257,7 +257,7 @@ fun DrawerContentLayout( selected = false, onClick = { eventBus.post( - AnalyticsScreenEvent(AnalyticsScreenNames.PLATFORM_TERMS_OF_USE, context.deviceLocale) + AnalyticsScreenEvent(AnalyticsScreenNames.PLATFORM_TERMS_OF_USE, context.appLanguage) ) uriHandler.openUri("https://godtoolsapp.com/terms-of-use/") dismissDrawer() @@ -269,7 +269,7 @@ fun DrawerContentLayout( selected = false, onClick = { eventBus.post( - AnalyticsScreenEvent(AnalyticsScreenNames.PLATFORM_PRIVACY_POLICY, context.deviceLocale) + AnalyticsScreenEvent(AnalyticsScreenNames.PLATFORM_PRIVACY_POLICY, context.appLanguage) ) uriHandler.openUri("https://www.cru.org/about/privacy.html") dismissDrawer() @@ -281,7 +281,7 @@ fun DrawerContentLayout( selected = false, onClick = { eventBus.post( - AnalyticsScreenEvent(AnalyticsScreenNames.PLATFORM_COPYRIGHT, context.deviceLocale) + AnalyticsScreenEvent(AnalyticsScreenNames.PLATFORM_COPYRIGHT, context.appLanguage) ) uriHandler.openUri("https://godtoolsapp.com/copyright/") dismissDrawer() From d68c3d9f6a217704562a787aa4d096a6b591cfbd Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 14:17:04 -0400 Subject: [PATCH 13/16] use the appLanguage when choosing the language for the articles tutorial deeplink --- .../main/kotlin/org/cru/godtools/tutorial/TutorialActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/TutorialActivity.kt b/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/TutorialActivity.kt index 2e8ee343c3..f4362b229b 100644 --- a/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/TutorialActivity.kt +++ b/ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/TutorialActivity.kt @@ -11,10 +11,10 @@ import javax.inject.Inject import org.ccci.gto.android.common.compat.content.getSerializableExtraCompat import org.ccci.gto.android.common.util.includeFallbacks import org.cru.godtools.base.Settings +import org.cru.godtools.base.appLanguage import org.cru.godtools.base.ui.startAppLanguageActivity import org.cru.godtools.base.ui.startArticlesActivity import org.cru.godtools.base.ui.startDashboardActivity -import org.cru.godtools.base.util.deviceLocale import org.cru.godtools.shared.analytics.TutorialAnalyticsActionNames import org.cru.godtools.tutorial.analytics.model.TutorialAnalyticsActionEvent import org.cru.godtools.tutorial.layout.TutorialLayout @@ -84,7 +84,7 @@ class TutorialActivity : AppCompatActivity() { Action.ONBOARDING_WATCH_VIDEO -> startYoutubePlayerActivity("RvhZ_wuxAgE") Action.ONBOARDING_LAUNCH_ARTICLES -> { eventBus.post(TutorialAnalyticsActionEvent(TutorialAnalyticsActionNames.ONBOARDING_LINK_ARTICLES)) - val locale = sequenceOf(deviceLocale, Locale.ENGLISH).filterNotNull().includeFallbacks() + val locale = sequenceOf(appLanguage, Locale.ENGLISH).filterNotNull().includeFallbacks() .firstOrNull { ARTICLES_SUPPORTED_LANGUAGES.contains(it) } ?: Locale.ENGLISH startArticlesActivity("es", locale) finish() From 68400096024cc0d525ae915285654666d6637b64 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 14:23:42 -0400 Subject: [PATCH 14/16] remove unused deviceLocale property --- .../src/main/kotlin/org/cru/godtools/base/util/LocaleUtils.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/util/LocaleUtils.kt b/library/base/src/main/kotlin/org/cru/godtools/base/util/LocaleUtils.kt index c36cbab201..01d427fbaf 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/util/LocaleUtils.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/util/LocaleUtils.kt @@ -3,15 +3,12 @@ package org.cru.godtools.base.util import android.content.Context -import androidx.core.os.ConfigurationCompat import java.util.Locale import org.ccci.gto.android.common.util.content.localizeIfPossible import org.ccci.gto.android.common.util.getOptionalDisplayName import org.cru.godtools.base.R import timber.log.Timber -val Context.deviceLocale get() = ConfigurationCompat.getLocales(resources.configuration)[0] - @JvmOverloads fun Locale.getDisplayName(context: Context? = null, defaultName: String? = null, inLocale: Locale? = null): String { return context?.localizeIfPossible(inLocale)?.getLanguageNameStringRes(this) From b14ed67266c0f7159322fbeac847beeb9e0d7792 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 15:01:34 -0400 Subject: [PATCH 15/16] default to utilizing the appLanguage for Language.getDisplayName() --- .../kotlin/org/cru/godtools/model/Language.kt | 5 ++-- .../org/cru/godtools/model/LanguageTest.kt | 26 +++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt b/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt index c189a35cb4..71ee3b6622 100644 --- a/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt +++ b/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt @@ -10,6 +10,7 @@ import kotlin.random.Random import org.ccci.gto.android.common.jsonapi.annotation.JsonApiAttribute import org.ccci.gto.android.common.jsonapi.annotation.JsonApiIgnore import org.ccci.gto.android.common.jsonapi.annotation.JsonApiType +import org.cru.godtools.base.appLanguage import org.cru.godtools.base.util.getDisplayName private const val JSON_CODE = "code" @@ -68,8 +69,8 @@ class Language : Base() { @JsonApiIgnore var isAdded: Boolean = false - fun getDisplayName(context: Context?) = getDisplayName(context, null) - fun getDisplayName(context: Context?, inLocale: Locale?) = + @JvmOverloads + fun getDisplayName(context: Context?, inLocale: Locale? = context?.appLanguage) = _code?.getDisplayName(context, name, inLocale) ?: name ?: "" // XXX: output the language id and code for debugging purposes diff --git a/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt b/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt index a37aff4e60..5b551c98c6 100644 --- a/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt +++ b/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt @@ -1,12 +1,18 @@ package org.cru.godtools.model +import android.content.Context +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verifyAll import java.util.Locale +import kotlin.test.Test import org.ccci.gto.android.common.jsonapi.JsonApiConverter import org.ccci.gto.android.common.jsonapi.converter.LocaleTypeConverter +import org.cru.godtools.base.util.getDisplayName import org.cru.godtools.model.Language.Companion.primaryCollator import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Test class LanguageTest { // region jsonapi parsing @@ -32,13 +38,17 @@ class LanguageTest { // region getDisplayName() @Test - fun `getDisplayName() - Missing Locale and Default Name`() { - assertEquals("", Language().getDisplayName(null)) - } - - @Test - fun `getDisplayName() - Missing Locale`() { - assertEquals("Default Name", Language().apply { name = "Default Name" }.getDisplayName(null)) + fun `getDisplayName()`() { + mockkStatic("org.cru.godtools.base.util.LocaleUtils") { + every { any().getDisplayName(any(), any(), any()) } returns "DisplayName" + + val context: Context = mockk() + val inLocale: Locale = Locale.CANADA_FRENCH + assertEquals("DisplayName", Language(Locale.ENGLISH) { name = "name" }.getDisplayName(context, inLocale)) + verifyAll { + Locale.ENGLISH.getDisplayName(context, "name", inLocale) + } + } } // endregion getDisplayName() From d7ebb7ee5ae04021c805b9e6516bb7f88d36aa53 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Fri, 20 Oct 2023 15:17:42 -0400 Subject: [PATCH 16/16] Update language sorting logic to default to the appLanguage --- .../godtools/ui/dashboard/tools/ToolsViewModel.kt | 2 +- .../main/kotlin/org/cru/godtools/model/Language.kt | 14 ++++++-------- .../kotlin/org/cru/godtools/model/LanguageTest.kt | 7 ------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt index 700a08aa95..e0702f2661 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt @@ -83,7 +83,7 @@ class ToolsViewModel @Inject constructor( langs.sortedWith( compareByDescending { it.code == appLang } .then(compareByDescending { it.isAdded }) - .then(Language.displayNameComparator(context)) + .then(Language.displayNameComparator(context, appLang)) ) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) diff --git a/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt b/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt index 71ee3b6622..d721765b0c 100644 --- a/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt +++ b/library/model/src/main/kotlin/org/cru/godtools/model/Language.kt @@ -2,7 +2,6 @@ package org.cru.godtools.model import android.content.Context import androidx.annotation.RestrictTo -import androidx.annotation.VisibleForTesting import java.text.Collator import java.util.Locale import java.util.UUID @@ -25,16 +24,16 @@ class Language : Base() { val INVALID_CODE = Locale("x", "inv") - fun displayNameComparator(context: Context? = null, displayLocale: Locale? = null): Comparator = + fun displayNameComparator(context: Context, displayLocale: Locale = context.appLanguage): Comparator = compareBy(displayLocale.primaryCollator) { it.getDisplayName(context, displayLocale) } - private fun Collection.toDisplayNameSortedMap(context: Context?, displayLocale: Locale? = null) = + private fun Collection.toDisplayNameSortedMap(context: Context, displayLocale: Locale) = associateBy { it.getDisplayName(context, displayLocale) }.toSortedMap(displayLocale.primaryCollator) - fun Collection.sortedByDisplayName(context: Context?, displayLocale: Locale? = null): List = + fun Collection.sortedByDisplayName(context: Context, displayLocale: Locale = context.appLanguage) = toDisplayNameSortedMap(context, displayLocale).values.toList() - fun Collection.getSortedDisplayNames(context: Context?, displayLocale: Locale? = null) = + fun Collection.getSortedDisplayNames(context: Context, displayLocale: Locale = context.appLanguage) = toDisplayNameSortedMap(context, displayLocale).keys.toList() fun Collection.filterByDisplayAndNativeName( @@ -50,9 +49,8 @@ class Language : Base() { } } - @VisibleForTesting - internal val Locale?.primaryCollator: Collator - get() = Collator.getInstance(this ?: Locale.getDefault()).also { it.strength = Collator.PRIMARY } + private val Locale.primaryCollator: Collator + get() = Collator.getInstance(this).also { it.strength = Collator.PRIMARY } } @JsonApiAttribute(JSON_CODE) diff --git a/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt b/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt index 5b551c98c6..caf98f2e15 100644 --- a/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt +++ b/library/model/src/test/kotlin/org/cru/godtools/model/LanguageTest.kt @@ -10,9 +10,7 @@ import kotlin.test.Test import org.ccci.gto.android.common.jsonapi.JsonApiConverter import org.ccci.gto.android.common.jsonapi.converter.LocaleTypeConverter import org.cru.godtools.base.util.getDisplayName -import org.cru.godtools.model.Language.Companion.primaryCollator import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull class LanguageTest { // region jsonapi parsing @@ -51,9 +49,4 @@ class LanguageTest { } } // endregion getDisplayName() - - @Test - fun `primaryCollator - Doesn't crash on null Locale`() { - assertNotNull((null as Locale?).primaryCollator) - } }