Skip to content

Commit

Permalink
Merge pull request #3181 from CruGlobal/appLanguageFixes
Browse files Browse the repository at this point in the history
GT-2166 GT-2167 app language fixes
  • Loading branch information
frett authored Oct 23, 2023
2 parents d107ef2 + d7ebb7e commit de1dc63
Show file tree
Hide file tree
Showing 19 changed files with 151 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class ToolsViewModel @Inject constructor(
langs.sortedWith(
compareByDescending<Language> { it.code == appLang }
.then(compareByDescending { it.isAdded })
.then(Language.displayNameComparator(context))
.then(Language.displayNameComparator(context, appLang))
)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand All @@ -35,25 +37,22 @@ class DownloadableLanguagesViewModel @Inject constructor(
private var floatedLanguages: Set<Locale>?
get() = savedStateHandle.get<List<Locale>>(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<Language> { 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() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,13 +35,17 @@ import org.robolectric.annotation.Config
@Config(application = Application::class)
@OptIn(ExperimentalCoroutinesApi::class)
class DownloadableLanguagesViewModelTest {
private val appLanguageFlow = MutableStateFlow<Locale>(Locale.ENGLISH)
private val languages = MutableSharedFlow<List<Language>>()

private val context: Context get() = ApplicationProvider.getApplicationContext()
private val languagesRepository: LanguagesRepository = mockk {
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
}
Expand All @@ -53,6 +59,7 @@ class DownloadableLanguagesViewModelTest {
viewModel = DownloadableLanguagesViewModel(
context = context,
languagesRepository = languagesRepository,
settings = settings,
syncService = syncService,
savedStateHandle = savedStateHandle,
)
Expand All @@ -70,6 +77,7 @@ class DownloadableLanguagesViewModelTest {
val viewModel2 = DownloadableLanguagesViewModel(
context = context,
languagesRepository = languagesRepository,
settings = settings,
syncService = syncService,
savedStateHandle = savedStateHandle
)
Expand Down
7 changes: 6 additions & 1 deletion library/base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt
Original file line number Diff line number Diff line change
@@ -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<Locale> = flow {
// TODO: is there a way to actively listen for changes?
while (true) {
emit(appLanguage)
delay(1_000 / 60)
}
}.distinctUntilChanged()
Original file line number Diff line number Diff line change
@@ -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<Locale?> { 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)
}
19 changes: 5 additions & 14 deletions library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Locale> = 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<Locale> = context.getAppLanguageFlow()
.shareIn(coroutineScope, SharingStarted.WhileSubscribed(5_000))
.onStart { emit(context.appLanguage) }
.distinctUntilChanged()
// endregion Language Settings

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit de1dc63

Please sign in to comment.