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
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone
import android.os.Build
import androidx.annotation.RequiresApi
@ -31,7 +33,18 @@ fun TimeZone.offset(currentTime: ZonedDateTime): ZonedDateTime {
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)
get() = id
.replace("_", " ")

View File

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

View File

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

View File

@ -18,10 +18,14 @@
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.ULocale
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
@ -38,6 +42,10 @@ class FavoriteTimeZonesTest {
@Test
fun convertTime(): Unit = with(composeTestRule) {
setContent {
val locale = ULocale.getDefault()
val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) }
val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) }
FavoriteTimeZoneItem(
modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer),
@ -55,7 +63,9 @@ class FavoriteTimeZonesTest {
onDelete = {},
onPrimaryClick = {},
onLabelClick = {},
isDragging = false
isDragging = false,
timeZoneNames = timeZoneNames,
localeDisplayNames = localeDisplayNames
)
}

View File

@ -18,9 +18,13 @@
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.ULocale
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
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.theme.numberHeadlineSmall
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 java.time.ZonedDateTime
@ -78,6 +82,7 @@ fun AddTimeZoneScreen(
userTime: ZonedDateTime,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags())
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@ -104,7 +109,7 @@ fun AddTimeZoneScreen(
addToFavorites(it.timeZone)
navigateUp()
},
headlineContent = { Text(it.timeZone.displayName) },
headlineContent = { Text(it.timeZone.getDisplayName(locale)) },
supportingContent = { Text(it.formattedLabel) },
trailingContent = {
Text(
@ -124,6 +129,7 @@ fun AddTimeZoneScreen(
@Preview
@Composable
fun PreviewAddTimeZoneScreen() {
val locale = ULocale.getDefault()
AddTimeZoneScreen(
uiState = AddTimeZoneUIState.Ready(
query = TextFieldValue(),
@ -135,7 +141,10 @@ fun PreviewAddTimeZoneScreen() {
val zone = TimeZone.getTimeZone(it)
SearchResultZone(
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
import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -55,7 +57,14 @@ class AddTimeZoneViewModel @Inject constructor(
}
.mapLatest { ui ->
viewModelScope.launch {
_result.update { timezonesRepository.filterAllTimeZones(ui.query.text) }
_result.update {
timezonesRepository.filterAllTimeZones(
searchQuery = ui.query.text,
locale = ULocale.forLanguageTag(
AppCompatDelegate.getApplicationLocales().toLanguageTags()
)
)
}
}
ui
}

View File

@ -18,9 +18,13 @@
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.ULocale
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateInt
import androidx.compose.animation.core.updateTransition
@ -137,6 +141,11 @@ private fun TimeZoneScreen(
}
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) {
while ((uiState.customUserTime == null) and isActive) {
currentUserTime = uiState.userTimeZone.timeNow()
@ -247,7 +256,9 @@ private fun TimeZoneScreen(
onPrimaryClick = { 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
import android.icu.text.LocaleDisplayNames
import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build
import androidx.annotation.RequiresApi
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.theme.numberHeadlineMedium
import com.sadellie.unitto.data.common.offset
import com.sadellie.unitto.data.common.regionName
import com.sadellie.unitto.data.model.timezone.FavoriteZone
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@ -81,6 +85,8 @@ internal fun FavoriteTimeZoneItem(
onDelete: () -> Unit,
onLabelClick: () -> Unit,
onPrimaryClick: (ZonedDateTime) -> Unit,
timeZoneNames: TimeZoneNames,
localeDisplayNames: LocaleDisplayNames,
) {
var deleteAnimationRunning by remember { mutableStateOf(false) }
val animatedAlpha by animateFloatAsState(
@ -89,6 +95,10 @@ internal fun FavoriteTimeZoneItem(
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 offsetTimeFormatted = offsetTime.formatOffset(fromTime)
@ -118,7 +128,7 @@ internal fun FavoriteTimeZoneItem(
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = item.timeZone.displayName,
text = regionName,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@ -298,6 +308,9 @@ private fun PreviewFavoriteTimeZones(
@PreviewParameter(FavoriteTimeZoneItemParameterProvider::class) tz: FavoriteTimeZoneItemParameter,
) {
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(
modifier = Modifier
@ -312,6 +325,8 @@ private fun PreviewFavoriteTimeZones(
onDelete = {},
onPrimaryClick = {},
onLabelClick = {},
isDragging = false
isDragging = false,
timeZoneNames = timeZoneNames,
localeDisplayNames = localeDisplayNames
)
}