Localized time zone names

This commit is contained in:
Sad Ellie 2023-10-07 15:42:20 +03:00
parent 6dc1e93c1f
commit ce8bc5c738
8 changed files with 103 additions and 29 deletions

View File

@ -18,6 +18,8 @@
package com.sadellie.unitto.data.common package com.sadellie.unitto.data.common
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -31,7 +33,18 @@ fun TimeZone.offset(currentTime: ZonedDateTime): ZonedDateTime {
return currentTimeWithoutOffset.plusSeconds(this.rawOffset / 1000L) return currentTimeWithoutOffset.plusSeconds(this.rawOffset / 1000L)
} }
val TimeZone.region: String @RequiresApi(Build.VERSION_CODES.N)
fun TimeZone.regionName(
timeZoneNames: TimeZoneNames,
localeDisplayNames: LocaleDisplayNames
): String {
val location = timeZoneNames.getExemplarLocationName(this.id) ?: return fallbackRegion
val region = localeDisplayNames.regionDisplayName(TimeZone.getRegion(id)) ?: return fallbackRegion
return "$location, $region"
}
private val TimeZone.fallbackRegion: String
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
get() = id get() = id
.replace("_", " ") .replace("_", " ")

View File

@ -18,11 +18,14 @@
package com.sadellie.unitto.data.timezone package com.sadellie.unitto.data.timezone
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.sadellie.unitto.data.common.lev import com.sadellie.unitto.data.common.lev
import com.sadellie.unitto.data.common.region import com.sadellie.unitto.data.common.regionName
import com.sadellie.unitto.data.database.TimeZoneDao import com.sadellie.unitto.data.database.TimeZoneDao
import com.sadellie.unitto.data.model.timezone.FavoriteZone import com.sadellie.unitto.data.model.timezone.FavoriteZone
import com.sadellie.unitto.data.model.timezone.SearchResultZone import com.sadellie.unitto.data.model.timezone.SearchResultZone
@ -90,10 +93,13 @@ class TimeZonesRepository @Inject constructor(
} }
suspend fun filterAllTimeZones( suspend fun filterAllTimeZones(
searchQuery: String searchQuery: String,
locale: ULocale,
): List<SearchResultZone> = ): List<SearchResultZone> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val favorites = dao.getFavorites().first().map { it.id } val favorites = dao.getFavorites().first().map { it.id }
val timeZoneNames = TimeZoneNames.getInstance(locale)
val localeDisplayNames = LocaleDisplayNames.getInstance(locale)
val query = searchQuery.trim().lowercase() val query = searchQuery.trim().lowercase()
val threshold: Int = query.length / 2 val threshold: Int = query.length / 2
@ -103,8 +109,8 @@ class TimeZonesRepository @Inject constructor(
if (timeZoneId in favorites) return@forEach if (timeZoneId in favorites) return@forEach
val timeZone = TimeZone.getTimeZone(timeZoneId) val timeZone = TimeZone.getTimeZone(timeZoneId)
val name = timeZone.displayName val displayName = timeZone.displayName
val id = timeZone.region val regionName = timeZone.regionName(timeZoneNames, localeDisplayNames)
// // CODE Match // // CODE Match
// if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) { // if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) {
@ -115,42 +121,42 @@ class TimeZonesRepository @Inject constructor(
// Display name match // Display name match
when { when {
// not zero, so that lev can have that // not zero, so that lev can have that
name.startsWith(query) -> { displayName.startsWith(query) -> {
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1)
return@forEach return@forEach
} }
name.contains(query) -> { displayName.contains(query) -> {
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2) timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2)
return@forEach return@forEach
} }
} }
val nameLevDist = name val displayNameLevDist = displayName
.substring(0, minOf(query.length, name.length)) .substring(0, minOf(query.length, displayName.length))
.lev(query) .lev(query)
if (nameLevDist < threshold) { if (displayNameLevDist < threshold) {
timeZonesWithDist.add(SearchResultZone(timeZone, id) to nameLevDist) timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to displayNameLevDist)
return@forEach return@forEach
} }
// ID Match // ID Match
when { when {
// not zero, so that lev can have that // not zero, so that lev can have that
id.startsWith(query) -> { regionName.startsWith(query) -> {
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1)
return@forEach return@forEach
} }
id.contains(query) -> { regionName.contains(query) -> {
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2) timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2)
return@forEach return@forEach
} }
} }
val idLevDist = id val regionNameLevDist = regionName
.substring(0, minOf(query.length, id.length)) .substring(0, minOf(query.length, regionName.length))
.lev(query) .lev(query)
if (idLevDist < threshold) { if (regionNameLevDist < threshold) {
timeZonesWithDist.add(SearchResultZone(timeZone, id) to idLevDist) timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to regionNameLevDist)
return@forEach return@forEach
} }
} }

View File

@ -35,6 +35,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation(libs.com.github.sadellie.themmo) implementation(libs.com.github.sadellie.themmo)
implementation(libs.org.burnoutcrew.composereorderable.reorderable) implementation(libs.org.burnoutcrew.composereorderable.reorderable)
implementation(libs.androidx.appcompat.appcompat)
implementation(project(":data:common")) implementation(project(":data:common"))
implementation(project(":data:userprefs")) implementation(project(":data:userprefs"))

View File

@ -18,10 +18,14 @@
package com.sadellie.unitto.feature.timezone.components package com.sadellie.unitto.feature.timezone.components
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
@ -38,6 +42,10 @@ class FavoriteTimeZonesTest {
@Test @Test
fun convertTime(): Unit = with(composeTestRule) { fun convertTime(): Unit = with(composeTestRule) {
setContent { setContent {
val locale = ULocale.getDefault()
val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) }
val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) }
FavoriteTimeZoneItem( FavoriteTimeZoneItem(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer), .background(MaterialTheme.colorScheme.secondaryContainer),
@ -55,7 +63,9 @@ class FavoriteTimeZonesTest {
onDelete = {}, onDelete = {},
onPrimaryClick = {}, onPrimaryClick = {},
onLabelClick = {}, onLabelClick = {},
isDragging = false isDragging = false,
timeZoneNames = timeZoneNames,
localeDisplayNames = localeDisplayNames
) )
} }

View File

@ -18,9 +18,13 @@
package com.sadellie.unitto.feature.timezone package com.sadellie.unitto.feature.timezone
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -45,7 +49,7 @@ import com.sadellie.unitto.core.ui.common.UnittoSearchBar
import com.sadellie.unitto.core.ui.datetime.formatLocal import com.sadellie.unitto.core.ui.datetime.formatLocal
import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall
import com.sadellie.unitto.data.common.offset import com.sadellie.unitto.data.common.offset
import com.sadellie.unitto.data.common.region import com.sadellie.unitto.data.common.regionName
import com.sadellie.unitto.data.model.timezone.SearchResultZone import com.sadellie.unitto.data.model.timezone.SearchResultZone
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -78,6 +82,7 @@ fun AddTimeZoneScreen(
userTime: ZonedDateTime, userTime: ZonedDateTime,
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags())
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@ -104,7 +109,7 @@ fun AddTimeZoneScreen(
addToFavorites(it.timeZone) addToFavorites(it.timeZone)
navigateUp() navigateUp()
}, },
headlineContent = { Text(it.timeZone.displayName) }, headlineContent = { Text(it.timeZone.getDisplayName(locale)) },
supportingContent = { Text(it.formattedLabel) }, supportingContent = { Text(it.formattedLabel) },
trailingContent = { trailingContent = {
Text( Text(
@ -124,6 +129,7 @@ fun AddTimeZoneScreen(
@Preview @Preview
@Composable @Composable
fun PreviewAddTimeZoneScreen() { fun PreviewAddTimeZoneScreen() {
val locale = ULocale.getDefault()
AddTimeZoneScreen( AddTimeZoneScreen(
uiState = AddTimeZoneUIState.Ready( uiState = AddTimeZoneUIState.Ready(
query = TextFieldValue(), query = TextFieldValue(),
@ -135,7 +141,10 @@ fun PreviewAddTimeZoneScreen() {
val zone = TimeZone.getTimeZone(it) val zone = TimeZone.getTimeZone(it)
SearchResultZone( SearchResultZone(
timeZone = zone, timeZone = zone,
formattedLabel = zone.region formattedLabel = zone.regionName(
timeZoneNames = TimeZoneNames.getInstance(locale),
localeDisplayNames = LocaleDisplayNames.getInstance(locale)
)
) )
} }
), ),

View File

@ -19,8 +19,10 @@
package com.sadellie.unitto.feature.timezone package com.sadellie.unitto.feature.timezone
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -55,7 +57,14 @@ class AddTimeZoneViewModel @Inject constructor(
} }
.mapLatest { ui -> .mapLatest { ui ->
viewModelScope.launch { viewModelScope.launch {
_result.update { timezonesRepository.filterAllTimeZones(ui.query.text) } _result.update {
timezonesRepository.filterAllTimeZones(
searchQuery = ui.query.text,
locale = ULocale.forLanguageTag(
AppCompatDelegate.getApplicationLocales().toLanguageTags()
)
)
}
} }
ui ui
} }

View File

@ -18,9 +18,13 @@
package com.sadellie.unitto.feature.timezone package com.sadellie.unitto.feature.timezone
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateInt import androidx.compose.animation.core.animateInt
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
@ -137,6 +141,11 @@ private fun TimeZoneScreen(
} }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags())
val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) }
val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) }
LaunchedEffect(uiState.customUserTime) { LaunchedEffect(uiState.customUserTime) {
while ((uiState.customUserTime == null) and isActive) { while ((uiState.customUserTime == null) and isActive) {
currentUserTime = uiState.userTimeZone.timeNow() currentUserTime = uiState.userTimeZone.timeNow()
@ -247,7 +256,9 @@ private fun TimeZoneScreen(
onPrimaryClick = { offsetTime -> onPrimaryClick = { offsetTime ->
setDialogState(TimeZoneDialogState.FavoriteTimePicker(item, offsetTime)) setDialogState(TimeZoneDialogState.FavoriteTimePicker(item, offsetTime))
}, },
isDragging = isDragging isDragging = isDragging,
timeZoneNames = timeZoneNames,
localeDisplayNames = localeDisplayNames,
) )
} }
} }

View File

@ -18,7 +18,10 @@
package com.sadellie.unitto.feature.timezone.components package com.sadellie.unitto.feature.timezone.components
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
@ -65,6 +68,7 @@ import com.sadellie.unitto.core.ui.datetime.formatLocal
import com.sadellie.unitto.core.ui.datetime.formatOffset import com.sadellie.unitto.core.ui.datetime.formatOffset
import com.sadellie.unitto.core.ui.theme.numberHeadlineMedium import com.sadellie.unitto.core.ui.theme.numberHeadlineMedium
import com.sadellie.unitto.data.common.offset import com.sadellie.unitto.data.common.offset
import com.sadellie.unitto.data.common.regionName
import com.sadellie.unitto.data.model.timezone.FavoriteZone import com.sadellie.unitto.data.model.timezone.FavoriteZone
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -81,6 +85,8 @@ internal fun FavoriteTimeZoneItem(
onDelete: () -> Unit, onDelete: () -> Unit,
onLabelClick: () -> Unit, onLabelClick: () -> Unit,
onPrimaryClick: (ZonedDateTime) -> Unit, onPrimaryClick: (ZonedDateTime) -> Unit,
timeZoneNames: TimeZoneNames,
localeDisplayNames: LocaleDisplayNames,
) { ) {
var deleteAnimationRunning by remember { mutableStateOf(false) } var deleteAnimationRunning by remember { mutableStateOf(false) }
val animatedAlpha by animateFloatAsState( val animatedAlpha by animateFloatAsState(
@ -89,6 +95,10 @@ internal fun FavoriteTimeZoneItem(
finishedListener = { if (it == 0f) onDelete() } finishedListener = { if (it == 0f) onDelete() }
) )
val regionName = remember(timeZoneNames, localeDisplayNames) {
item.timeZone.regionName(timeZoneNames, localeDisplayNames)
}
val offsetTime by remember(fromTime) { mutableStateOf(item.timeZone.offset(fromTime)) } val offsetTime by remember(fromTime) { mutableStateOf(item.timeZone.offset(fromTime)) }
val offsetTimeFormatted = offsetTime.formatOffset(fromTime) val offsetTimeFormatted = offsetTime.formatOffset(fromTime)
@ -118,7 +128,7 @@ internal fun FavoriteTimeZoneItem(
verticalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
Text( Text(
text = item.timeZone.displayName, text = regionName,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -298,6 +308,9 @@ private fun PreviewFavoriteTimeZones(
@PreviewParameter(FavoriteTimeZoneItemParameterProvider::class) tz: FavoriteTimeZoneItemParameter, @PreviewParameter(FavoriteTimeZoneItemParameterProvider::class) tz: FavoriteTimeZoneItemParameter,
) { ) {
var expanded by remember { mutableStateOf(tz.expanded) } var expanded by remember { mutableStateOf(tz.expanded) }
val locale = ULocale.getDefault()
val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) }
val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) }
FavoriteTimeZoneItem( FavoriteTimeZoneItem(
modifier = Modifier modifier = Modifier
@ -312,6 +325,8 @@ private fun PreviewFavoriteTimeZones(
onDelete = {}, onDelete = {},
onPrimaryClick = {}, onPrimaryClick = {},
onLabelClick = {}, onLabelClick = {},
isDragging = false isDragging = false,
timeZoneNames = timeZoneNames,
localeDisplayNames = localeDisplayNames
) )
} }