From 3ce3bab7c6c1cd87a23dd6a00c0d53a15a1dec2d Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Thu, 10 Sep 2026 11:57:19 +0300 Subject: [PATCH] Format the "updated" timestamp as a locale-aware date/time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the raw "2026-09-10 10:03:54" with each platform's own locale-formatted, no-seconds rendering (e.g. "Sep 10, 2026, 10:03 AM" in en-US) — mirroring how amounts are already locale-formatted elsewhere (NumberFormat.kt). Android uses the pre-java.time Calendar/DateFormat APIs since minSdk 24 predates java.time (API 26) and the project has no core library desugaring configured. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy --- .../satsprice/domain/DateFormat.android.kt | 17 ++++++++++++ .../tyiu/satsprice/domain/DateFormat.apple.kt | 27 +++++++++++++++++++ .../xyz/tyiu/satsprice/domain/DateFormat.kt | 11 ++++++++ .../satsprice/ui/ConverterUiStateDisplay.kt | 7 +++-- .../tyiu/satsprice/domain/DateFormat.jvm.kt | 13 +++++++++ .../tyiu/satsprice/domain/DateFormat.web.kt | 16 +++++++++++ 6 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 shared/src/androidMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.android.kt create mode 100644 shared/src/appleMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.apple.kt create mode 100644 shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.kt create mode 100644 shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.jvm.kt create mode 100644 shared/src/webMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.web.kt diff --git a/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.android.kt b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.android.kt new file mode 100644 index 0000000..f9bace6 --- /dev/null +++ b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.android.kt @@ -0,0 +1,17 @@ +package xyz.tyiu.satsprice.domain + +import kotlinx.datetime.LocalDateTime +import java.text.DateFormat +import java.util.Calendar +import java.util.Locale + +// java.time requires API 26+ (this app's minSdk is 24) without core library desugaring, so this +// uses the older Calendar/DateFormat APIs instead, same as pre-java.time Android code always did. +actual fun localizedDateTime(dateTime: LocalDateTime): String { + val calendar = Calendar.getInstance().apply { + clear() + set(dateTime.year, dateTime.monthNumber - 1, dateTime.dayOfMonth, dateTime.hour, dateTime.minute) + } + val formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) + return formatter.format(calendar.time) +} diff --git a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.apple.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.apple.kt new file mode 100644 index 0000000..94f2978 --- /dev/null +++ b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.apple.kt @@ -0,0 +1,27 @@ +package xyz.tyiu.satsprice.domain + +import kotlinx.datetime.LocalDateTime +import platform.Foundation.NSCalendar +import platform.Foundation.NSDateComponents +import platform.Foundation.NSDateFormatter +import platform.Foundation.NSDateFormatterMediumStyle +import platform.Foundation.NSDateFormatterShortStyle +import platform.Foundation.NSLocale +import platform.Foundation.currentLocale + +actual fun localizedDateTime(dateTime: LocalDateTime): String { + val components = NSDateComponents().apply { + year = dateTime.year.toLong() + month = dateTime.monthNumber.toLong() + day = dateTime.dayOfMonth.toLong() + hour = dateTime.hour.toLong() + minute = dateTime.minute.toLong() + } + val date = NSCalendar.currentCalendar.dateFromComponents(components) ?: return dateTime.toString() + val formatter = NSDateFormatter().apply { + dateStyle = NSDateFormatterMediumStyle + timeStyle = NSDateFormatterShortStyle + locale = NSLocale.currentLocale + } + return formatter.stringFromDate(date) +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.kt new file mode 100644 index 0000000..7c44359 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.kt @@ -0,0 +1,11 @@ +package xyz.tyiu.satsprice.domain + +import kotlinx.datetime.LocalDateTime + +/** + * A locale-aware, human-readable rendering of [dateTime] (e.g. "Sep 10, 2026, 10:03 AM" in + * en-US), using each platform's own date/time formatter — mirroring how [localizedGroupedInteger] + * and [localizedDecimalSeparator] format numbers. Deliberately drops seconds: a "last updated" + * label doesn't need second-level precision. + */ +expect fun localizedDateTime(dateTime: LocalDateTime): String diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt index fed75be..357bbb5 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt @@ -5,6 +5,7 @@ import kotlinx.datetime.toLocalDateTime import xyz.tyiu.satsprice.CurrencyInfo import xyz.tyiu.satsprice.domain.CurrencyConverter import xyz.tyiu.satsprice.domain.formatAmount +import xyz.tyiu.satsprice.domain.localizedDateTime import xyz.tyiu.satsprice.domain.toBigDecimalOrNull import kotlin.time.Instant @@ -52,7 +53,5 @@ fun ConverterUiState.unselectedCurrencies(): List = /** Whether the active price source quotes a rate for [code] — every currency is listed regardless. */ fun ConverterUiState.isPriced(code: String): Boolean = code in pricedCurrencyCodes -private fun Instant.toDateTimeString(): String { - val local = toLocalDateTime(TimeZone.currentSystemDefault()) - return local.toString().substringBefore('.').replace('T', ' ') -} +private fun Instant.toDateTimeString(): String = + localizedDateTime(toLocalDateTime(TimeZone.currentSystemDefault())) diff --git a/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.jvm.kt b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.jvm.kt new file mode 100644 index 0000000..ea0f46a --- /dev/null +++ b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.jvm.kt @@ -0,0 +1,13 @@ +package xyz.tyiu.satsprice.domain + +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale + +actual fun localizedDateTime(dateTime: LocalDateTime): String { + val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + return dateTime.toJavaLocalDateTime().format(formatter) +} diff --git a/shared/src/webMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.web.kt b/shared/src/webMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.web.kt new file mode 100644 index 0000000..704ae4a --- /dev/null +++ b/shared/src/webMain/kotlin/xyz/tyiu/satsprice/domain/DateFormat.web.kt @@ -0,0 +1,16 @@ +package xyz.tyiu.satsprice.domain + +import kotlinx.datetime.LocalDateTime + +// Individual Int fields (rather than the LocalDateTime itself) cross the js() boundary, matching +// the rest of this file's web interop — see SystemCurrencies.web.kt for why. +private fun jsFormatDateTime(year: Int, month: Int, day: Int, hour: Int, minute: Int): String = js( + """(function() { + var date = new Date(year, month - 1, day, hour, minute); + var fmt = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }); + return fmt.format(date); + })()""", +) + +actual fun localizedDateTime(dateTime: LocalDateTime): String = + jsFormatDateTime(dateTime.year, dateTime.monthNumber, dateTime.dayOfMonth, dateTime.hour, dateTime.minute)