Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GT-2166 GT-2167 app language fixes #3181

Merged
merged 16 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [email protected]
}
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