Localized formats

This commit is contained in:
Sad Ellie 2023-10-08 21:44:36 +03:00
parent cfbfd6041d
commit 08d78427d8
9 changed files with 190 additions and 66 deletions

View File

@ -25,10 +25,16 @@ import android.util.AttributeSet
import android.view.View
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.ui.LocalLocale
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.AndroidEntryPoint
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@ -43,8 +49,16 @@ internal class MainActivity : AppCompatActivity() {
setContent {
val prefs = userPrefsRepository.appPrefs
.collectAsStateWithLifecycle(null).value
val locale = remember(LocalConfiguration.current) {
val tag: String = AppCompatDelegate
.getApplicationLocales()
.toLanguageTags()
if (tag.isEmpty()) Locale.getDefault() else Locale.forLanguageTag(tag)
}
UnittoApp(prefs)
CompositionLocalProvider(LocalLocale provides locale) {
UnittoApp(prefs)
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui
import androidx.compose.runtime.compositionLocalOf
import java.util.Locale
val LocalLocale = compositionLocalOf {
Locale.getDefault()
}

View File

@ -19,60 +19,56 @@
package com.sadellie.unitto.core.ui.datetime
import java.time.format.DateTimeFormatter
import java.util.Locale
data object UnittoDateTimeFormatter {
internal data object UnittoDateTimeFormatter {
/**
* 23:59
*/
val time24Formatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("HH:mm") }
fun time24(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm", locale)
/**
* 11:59 AM
*/
val time12FormatterFull: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh:mm a") }
fun time12Full(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a", locale)
/**
* 11:59
* 11:59 (no AM/PM)
*/
val time12Formatter1: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh:mm") }
fun time12Short(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm", locale)
/**
* 23
*/
val time24OnlyHoursFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("HH") }
fun time24Hours(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("HH", locale)
/**
* 23
*/
val time12OnlyHoursFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh") }
fun time12Hours(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("hh", locale)
/**
* 59
*/
val timeOnlyMinutesFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("mm") }
/**
* 59
*/
val timeOnlySecondsFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("ss") }
fun timeMinutes(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("mm", locale)
/**
* AM
*/
val time12Formatter2: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("a") }
fun time12AmPm(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("a", locale)
/**
* 31 Dec 2077
*/
val dayMonthYear: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("d MMM y") }
fun dateDayMonthYear(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("d MMM y", locale)
/**
* Mon, 31 Dec, 2077
*/
val weekDayMonthYear: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("EEE, MMM d, y") }
fun dateWeekDayMonthYear(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, MMM d, y", locale)
/**
* GMT+3
*/
val zoneFormatPattern: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("O") }
fun zone(locale: Locale): DateTimeFormatter = DateTimeFormatter.ofPattern("O", locale)
}

View File

@ -18,13 +18,13 @@
package com.sadellie.unitto.core.ui.datetime
import android.text.format.DateFormat
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.core.base.R
import java.time.LocalDate
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.Locale
import kotlin.math.absoluteValue
/**
@ -35,44 +35,105 @@ import kotlin.math.absoluteValue
* Depends on system preferences
*
* @return Formatted string
* @see UnittoDateTimeFormatter.time24
* @see UnittoDateTimeFormatter.time12Full
*/
@Composable
fun ZonedDateTime.formatLocal(): String {
return if (DateFormat.is24HourFormat(LocalContext.current)) format(UnittoDateTimeFormatter.time24Formatter)
else format(UnittoDateTimeFormatter.time12FormatterFull)
}
fun ZonedDateTime.formatTime(
locale: Locale,
is24Hour: Boolean,
): String =
if (is24Hour) {
format(UnittoDateTimeFormatter.time24(locale))
} else {
format(UnittoDateTimeFormatter.time12Full(locale))
}
@Composable
fun ZonedDateTime.formatOnlyHours(): String {
return if (DateFormat.is24HourFormat(LocalContext.current)) format(UnittoDateTimeFormatter.time24OnlyHoursFormatter)
else format(UnittoDateTimeFormatter.time12OnlyHoursFormatter)
}
/**
* @see UnittoDateTimeFormatter.time12Short
*/
fun ZonedDateTime.formatTime12Short(
locale: Locale,
): String =
format(UnittoDateTimeFormatter.time12Short(locale))
@Composable
fun ZonedDateTime.formatOnlyMinutes(): String {
return format(UnittoDateTimeFormatter.timeOnlyMinutesFormatter)
}
/**
* Formats date time into something like:
*
* 23 or 11
*
* Depends on system preferences
*
* @return Formatted string
* @see UnittoDateTimeFormatter.time24Hours
* @see UnittoDateTimeFormatter.time12Hours
*/
fun ZonedDateTime.formatTimeHours(
locale: Locale,
is24Hour: Boolean,
): String =
if (is24Hour)
format(UnittoDateTimeFormatter.time24Hours(locale))
else
format(UnittoDateTimeFormatter.time12Hours(locale))
@Composable
fun ZonedDateTime.formatOnlySeconds(): String {
return format(UnittoDateTimeFormatter.timeOnlySecondsFormatter)
}
/**
* @see UnittoDateTimeFormatter.timeMinutes
*/
fun ZonedDateTime.formatTimeMinutes(
locale: Locale,
): String =
format(UnittoDateTimeFormatter.timeMinutes(locale))
@Composable
fun ZonedDateTime.formatOnlyAmPm(): String {
return format(UnittoDateTimeFormatter.time12Formatter2)
}
/**
* @see UnittoDateTimeFormatter.time12AmPm
*/
fun ZonedDateTime.formatTimeAmPm(
locale: Locale,
): String =
format(UnittoDateTimeFormatter.time12AmPm(locale))
/**
* @see UnittoDateTimeFormatter.dateWeekDayMonthYear
*/
fun ZonedDateTime.formatDateWeekDayMonthYear(
locale: Locale,
): String =
format(UnittoDateTimeFormatter.dateWeekDayMonthYear(locale))
/**
* @see UnittoDateTimeFormatter.zone
*/
fun ZonedDateTime.formatZone(
locale: Locale,
): String =
format(UnittoDateTimeFormatter.zone(locale))
/**
* @see UnittoDateTimeFormatter.dateDayMonthYear
*/
fun ZonedDateTime.formatDateDayMonthYear(
locale: Locale,
): String =
format(UnittoDateTimeFormatter.dateDayMonthYear(locale))
/**
* @see UnittoDateTimeFormatter.dateWeekDayMonthYear
*/
fun LocalDate.formatDateWeekDayMonthYear(
locale: Locale
): String =
format(UnittoDateTimeFormatter.dateWeekDayMonthYear(locale))
/**
* Format offset string. Examples:
*
* 0
* 0h
*
* +8
* +8h
*
* +8, tomorrow
* +8h, tomorrow
*
* -8, yesterday
* -8h 30m, yesterday
*
* @receiver [ZonedDateTime] Time with offset.
* @param currentTime Time without offset.
@ -80,7 +141,7 @@ fun ZonedDateTime.formatOnlyAmPm(): String {
*/
@Composable
fun ZonedDateTime.formatOffset(
currentTime: ZonedDateTime
currentTime: ZonedDateTime,
): String? {
val offsetFixed = ChronoUnit.SECONDS.between(currentTime, this)

View File

@ -69,6 +69,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.Token
import com.sadellie.unitto.core.ui.LocalLocale
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
import com.sadellie.unitto.core.ui.common.MenuButton
import com.sadellie.unitto.core.ui.common.PortraitLandscape
@ -78,13 +79,14 @@ import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField
import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter
import com.sadellie.unitto.core.ui.datetime.formatDateWeekDayMonthYear
import com.sadellie.unitto.data.common.format
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.unit.AbstractUnit
import com.sadellie.unitto.feature.converter.components.DefaultKeyboard
import com.sadellie.unitto.feature.converter.components.NumberBaseKeyboard
import com.sadellie.unitto.feature.converter.components.UnitSelectionButton
import java.util.Locale
@Composable
internal fun ConverterRoute(
@ -257,6 +259,7 @@ private fun Default(
clearInput: () -> Unit,
refreshCurrencyRates: (AbstractUnit) -> Unit,
) {
val locale: Locale = LocalLocale.current
var calculation by remember(uiState.calculation) {
mutableStateOf(
TextFieldValue(uiState.calculation?.format(uiState.scale, uiState.outputFormat) ?: "")
@ -266,7 +269,7 @@ private fun Default(
val lastUpdate by remember(uiState) {
derivedStateOf {
if (uiState.currencyRateUpdateState !is CurrencyRateUpdateState.Ready) return@derivedStateOf null
uiState.currencyRateUpdateState.date.format(UnittoDateTimeFormatter.weekDayMonthYear)
uiState.currencyRateUpdateState.date.formatDateWeekDayMonthYear(locale)
}
}

View File

@ -42,8 +42,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.LocalLocale
import com.sadellie.unitto.core.ui.common.squashable
import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter
import com.sadellie.unitto.core.ui.datetime.formatDateWeekDayMonthYear
import com.sadellie.unitto.core.ui.datetime.formatTime
import com.sadellie.unitto.core.ui.datetime.formatTime12Short
import com.sadellie.unitto.core.ui.datetime.formatTimeAmPm
import java.time.ZonedDateTime
@Composable
@ -56,6 +60,8 @@ internal fun DateTimeSelectorBlock(
onDateClick: () -> Unit = {},
onLongClick: () -> Unit = {},
) {
val locale = LocalLocale.current
Column(
modifier = modifier
.squashable(
@ -72,7 +78,7 @@ internal fun DateTimeSelectorBlock(
if (DateFormat.is24HourFormat(LocalContext.current)) {
AnimatedContent(
targetState = dateTime.format(UnittoDateTimeFormatter.time24Formatter),
targetState = dateTime,
transitionSpec = {
slideInVertically { height -> height } + fadeIn() togetherWith
slideOutVertically { height -> -height } + fadeOut() using
@ -86,7 +92,7 @@ internal fun DateTimeSelectorBlock(
interactionSource = remember { MutableInteractionSource() },
onClick = onTimeClick
),
text = time,
text = time.formatTime(locale, true),
style = MaterialTheme.typography.displaySmall,
maxLines = 1
)
@ -109,7 +115,7 @@ internal fun DateTimeSelectorBlock(
label = "Animated 12 hour",
) { time ->
Text(
text = time.format(UnittoDateTimeFormatter.time12Formatter1),
text = time.formatTime12Short(locale),
style = MaterialTheme.typography.displaySmall,
maxLines = 1
)
@ -125,7 +131,7 @@ internal fun DateTimeSelectorBlock(
label = "Animated am/pm",
) { time ->
Text(
text = time.format(UnittoDateTimeFormatter.time12Formatter2),
text = time.formatTimeAmPm(locale),
style = MaterialTheme.typography.bodyLarge,
maxLines = 1
)
@ -148,7 +154,7 @@ internal fun DateTimeSelectorBlock(
interactionSource = remember { MutableInteractionSource() },
onClick = onDateClick
),
text = date.format(UnittoDateTimeFormatter.weekDayMonthYear),
text = date.formatDateWeekDayMonthYear(locale),
style = MaterialTheme.typography.bodySmall
)
}

View File

@ -23,6 +23,7 @@ import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build
import android.text.format.DateFormat.is24HourFormat
import androidx.annotation.RequiresApi
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
@ -37,16 +38,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.LocalLocale
import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen
import com.sadellie.unitto.core.ui.common.UnittoListItem
import com.sadellie.unitto.core.ui.common.UnittoSearchBar
import com.sadellie.unitto.core.ui.datetime.formatLocal
import com.sadellie.unitto.core.ui.datetime.formatTime
import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall
import com.sadellie.unitto.data.common.displayName
import com.sadellie.unitto.data.common.offset
@ -83,6 +86,8 @@ fun AddTimeZoneScreen(
userTime: ZonedDateTime,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val locale = LocalLocale.current
val is24Hour = is24HourFormat(LocalContext.current)
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@ -113,7 +118,9 @@ fun AddTimeZoneScreen(
supportingContent = { Text(it.region) },
trailingContent = {
Text(
text = it.timeZone.offset(userTime).formatLocal(),
text = it.timeZone
.offset(userTime)
.formatTime(locale, is24Hour),
style = MaterialTheme.typography.numberHeadlineSmall
)
}

View File

@ -23,6 +23,7 @@ import android.icu.text.TimeZoneNames
import android.icu.util.TimeZone
import android.icu.util.ULocale
import android.os.Build
import android.text.format.DateFormat
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
@ -57,6 +58,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -64,8 +66,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.datetime.formatLocal
import com.sadellie.unitto.core.ui.LocalLocale
import com.sadellie.unitto.core.ui.datetime.formatOffset
import com.sadellie.unitto.core.ui.datetime.formatTime
import com.sadellie.unitto.core.ui.theme.numberHeadlineMedium
import com.sadellie.unitto.data.common.offset
import com.sadellie.unitto.data.common.regionName
@ -88,6 +91,8 @@ internal fun FavoriteTimeZoneItem(
timeZoneNames: TimeZoneNames,
localeDisplayNames: LocaleDisplayNames,
) {
val locale = LocalLocale.current
val is24Hour = DateFormat.is24HourFormat(LocalContext.current)
var deleteAnimationRunning by remember { mutableStateOf(false) }
val animatedAlpha by animateFloatAsState(
label = "delete animation",
@ -146,7 +151,7 @@ internal fun FavoriteTimeZoneItem(
}
}
AnimatedContent(
targetState = offsetTime.formatLocal(),
targetState = offsetTime.formatTime(locale, is24Hour),
label = "Time change",
transitionSpec = {
fadeIn() togetherWith fadeOut() using (SizeTransform(clip = false))

View File

@ -18,6 +18,7 @@
package com.sadellie.unitto.feature.timezone.components
import android.text.format.DateFormat
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
@ -43,14 +44,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.LocalLocale
import com.sadellie.unitto.core.ui.common.squashable
import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter
import com.sadellie.unitto.core.ui.datetime.formatOnlyHours
import com.sadellie.unitto.core.ui.datetime.formatOnlyMinutes
import com.sadellie.unitto.core.ui.datetime.formatDateDayMonthYear
import com.sadellie.unitto.core.ui.datetime.formatTimeHours
import com.sadellie.unitto.core.ui.datetime.formatTimeMinutes
import com.sadellie.unitto.core.ui.datetime.formatZone
import com.sadellie.unitto.core.ui.theme.numberBodyLarge
import com.sadellie.unitto.core.ui.theme.numberDisplayLarge
import java.time.ZonedDateTime
@ -63,6 +67,8 @@ internal fun UserTimeZone(
onResetClick: () -> Unit,
showReset: Boolean,
) {
val locale = LocalLocale.current
val is24Hour = DateFormat.is24HourFormat(LocalContext.current)
Row(
modifier = modifier
@ -77,7 +83,7 @@ internal fun UserTimeZone(
) {
Column(Modifier.weight(1f)) {
Text(
text = userTime.format(UnittoDateTimeFormatter.zoneFormatPattern),
text = userTime.formatZone(locale),
style = MaterialTheme.typography.numberBodyLarge,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
@ -85,13 +91,13 @@ internal fun UserTimeZone(
Row(
verticalAlignment = Alignment.Bottom
) {
SlidingText(text = userTime.formatOnlyHours())
SlidingText(text = userTime.formatTimeHours(locale, is24Hour))
TimeSeparator()
SlidingText(text = userTime.formatOnlyMinutes())
SlidingText(text = userTime.formatTimeMinutes(locale))
}
Text(
text = userTime.format(UnittoDateTimeFormatter.dayMonthYear),
text = userTime.formatDateDayMonthYear(locale),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)