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..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 @@ -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 @@ -115,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) @@ -127,16 +130,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( 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/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() 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]" 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)) } } 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/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), 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 ) diff --git a/library/base/build.gradle.kts b/library/base/build.gradle.kts index 1c2e017126..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( @@ -16,6 +20,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/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) +} 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 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) 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..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 @@ -10,6 +9,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" @@ -24,21 +24,33 @@ 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() - @VisibleForTesting - internal val Locale?.primaryCollator: Collator - get() = Collator.getInstance(this ?: Locale.getDefault()).also { it.strength = Collator.PRIMARY } + 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) } + } + } + + private val Locale.primaryCollator: Collator + get() = Collator.getInstance(this).also { it.strength = Collator.PRIMARY } } @JsonApiAttribute(JSON_CODE) @@ -55,8 +67,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..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 @@ -1,12 +1,16 @@ 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.model.Language.Companion.primaryCollator +import org.cru.godtools.base.util.getDisplayName import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Test class LanguageTest { // region jsonapi parsing @@ -32,18 +36,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() - - @Test - fun `primaryCollator - Doesn't crash on null Locale`() { - assertNotNull((null as Locale?).primaryCollator) - } } 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() } 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() 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 }