From 249606a358fc0a766cc4c34e48010f54b3e0cb2e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 17 Jul 2026 09:47:58 +0200 Subject: [PATCH] fix(i18n): unify the calendar titles on one locale-aware formatter (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Month, Week and Day each hand-rolled their title from a German-style template — "$weekday, ${date.day}. $monthName ${date.year}" — so every language got a trailing ordinal dot and day-before-month order, including ones that write the month first. English read "Fri, 17. Jul 2026" where it should read "Fri, Jul 17, 2026". 396a561 already fixed exactly this, but only for Agenda: it added localizedDateFormatter and migrated the four agenda files, leaving Month, Week, Day and MonthWidget on their originals. Day and Agenda therefore disagreed about the same date. All four now route through formatCalendarTitle, so there is one place left that decides how a title reads. Two behaviour changes fall out of the reporter's point that the bar wastes space: The year is dropped while you are in the current year. The title sits above a grid that already says which year it is; the year's absence is itself the signal that you are in the current one, and it reappears when you page out — the moment it starts carrying information. Since a skeleton is a field list, this is just appending "y", and the locale still places it. The Week title names a month instead of a day range. "24. Jun – 31. Jun" restated the day numbers printed in the column headers directly below, in the widest string in the bar. Naming weekStart's month keeps a straddling week on the outgoing month until it is fully gone — a week is seven contiguous days, so the earlier month has a day in it exactly while weekStart is inside it. No straddle conditional, and the title depends on nothing but weekStart, so it cannot drift with the direction you paged in from. currentLocale/localizedDateFormatter move to floret-kit's core-locale (neither is calendar-specific); LocaleSupport.kt goes away and its 11 callers repoint. MonthWidget keeps Locale.getDefault() — Glance has no LocalConfiguration and the widget re-renders on a configuration change, matching AgendaWidget. Does not touch the FAB stack the issue opens with: the buttons overlaying content is intentional and matches Google Calendar. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 ++++++++ .../calendula/ui/agenda/AgendaRange.kt | 2 +- .../calendula/ui/agenda/AgendaScreen.kt | 4 +- .../calendula/ui/common/CalendarTitle.kt | 28 +++++++++++ .../calendula/ui/common/LocaleSupport.kt | 33 ------------- .../calendula/ui/day/DayScreen.kt | 33 +++++++++---- .../calendula/ui/detail/EventDetailScreen.kt | 2 +- .../calendula/ui/edit/EventEditScreen.kt | 2 +- .../calendula/ui/month/MonthScreen.kt | 28 +++++++---- .../calendula/ui/search/SearchScreen.kt | 2 +- .../calendula/ui/settings/SettingsScreen.kt | 2 +- .../calendula/ui/week/WeekScreen.kt | 48 ++++++++++++------- .../calendula/widget/agenda/AgendaWidget.kt | 2 +- .../calendula/widget/month/MonthWidget.kt | 19 +++++--- floret-kit | 2 +- 15 files changed, 142 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTitle.kt delete mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4de5d..367dc6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- Dates in the Month, Week and Day title bars now follow your language's own + conventions. They were laid out in a fixed German-style order with a trailing + dot on the day number — "Fri, 17. Jul 2026" — in every language, including + ones that write the month first; the Agenda view already read correctly, so the + two disagreed about the same date. All four views now share one formatter and + render "Fri, Jul 17, 2026" in English, "17. Juli 2026" in German, and so on + ([#60]). +- The title bar drops the year while you're in the current one — "July" rather + than "July 2026". The year reappears the moment you page out of the current + year, which is when it tells you something you didn't already know. +- The Week view's title now names the month instead of spelling out the day range. + "24. Jun – 31. Jun" restated the day numbers already printed in the column + headers right below it, in the widest string in the bar. A week that straddles + two months keeps the outgoing month until it is fully gone ([#60]). + ## [2.15.0] — 2026-07-15 ### Added @@ -997,3 +1015,4 @@ automatically, with zero telemetry and no internet permission. [#48]: https://codeberg.org/jlmakiola/calendula/issues/48 [#49]: https://codeberg.org/jlmakiola/calendula/issues/49 [#52]: https://codeberg.org/jlmakiola/calendula/issues/52 +[#60]: https://codeberg.org/jlmakiola/calendula/issues/60 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt index b4cda3f..474f48d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt @@ -1,6 +1,6 @@ package de.jeanlucmakiola.calendula.ui.agenda -import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter +import de.jeanlucmakiola.floret.locale.localizedDateFormatter import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import java.time.YearMonth diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt index 51aad58..ebea45d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt @@ -68,7 +68,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS -import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter +import de.jeanlucmakiola.floret.locale.localizedDateFormatter import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.floret.components.GroupedRow import de.jeanlucmakiola.floret.components.Position @@ -76,7 +76,7 @@ import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill import de.jeanlucmakiola.calendula.ui.common.next import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay import kotlinx.coroutines.launch diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTitle.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTitle.kt new file mode 100644 index 0000000..3cb3422 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTitle.kt @@ -0,0 +1,28 @@ +package de.jeanlucmakiola.calendula.ui.common + +import de.jeanlucmakiola.floret.locale.localizedDateFormatter +import java.util.Locale + +/** + * Formats a calendar title (top bar or widget header): [skeleton]'s fields laid + * out in [locale]'s own order, with the year shown only when [date] falls outside + * [currentYear]. + * + * Pass [skeleton] *without* a year field — "LLLL" for a month, "EEEdMMM" for a + * day. A skeleton is a field list, so wanting the year is just asking for one + * more field; the locale still decides where it lands ("July 2026" vs "2026年7月"). + * + * Dropping the year in the current year is the one bit of policy here: the title + * sits directly above a grid that already says which year it is, and the year's + * *absence* is itself the signal that you're in the current one — it appears the + * moment you page out of it, which is when it starts carrying information. + */ +fun formatCalendarTitle( + date: java.time.LocalDate, + locale: Locale, + currentYear: Int, + skeleton: String, +): String { + val fields = if (date.year == currentYear) skeleton else skeleton + "y" + return localizedDateFormatter(locale, fields).format(date) +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt deleted file mode 100644 index 8fc28ef..0000000 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt +++ /dev/null @@ -1,33 +0,0 @@ -package de.jeanlucmakiola.calendula.ui.common - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalConfiguration -import androidx.core.os.ConfigurationCompat -import java.time.format.DateTimeFormatter -import java.util.Locale - -/** - * Current display [Locale], read observably from [LocalConfiguration] so the UI - * recomposes after a locale change (lint: NonObservableLocale). Used for - * weekday/month name formatting. - */ -@Composable -fun currentLocale(): Locale { - val configuration = LocalConfiguration.current - return remember(configuration) { - ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() - } -} - -/** - * A [DateTimeFormatter] for [skeleton]'s fields laid out in [locale]'s own order - * (via Android's best-pattern matching), so dates read naturally per locale - * instead of a hardcoded day-month-year order. [skeleton] lists the wanted - * fields, e.g. "dMMMy" (day, abbreviated month, year) or "EEEdMMM" (weekday too). - */ -fun localizedDateFormatter(locale: Locale, skeleton: String): DateTimeFormatter = - DateTimeFormatter.ofPattern( - android.text.format.DateFormat.getBestDateTimePattern(locale, skeleton), - locale, - ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt index c1185d0..8507102 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt @@ -68,6 +68,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.EventInstance +import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure @@ -83,7 +84,7 @@ import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors import de.jeanlucmakiola.calendula.ui.common.eventFill import de.jeanlucmakiola.calendula.ui.common.eventInk import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines import de.jeanlucmakiola.calendula.ui.common.formatHourLabel @@ -93,7 +94,9 @@ import de.jeanlucmakiola.calendula.ui.week.TimedBlock import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate -import java.time.format.TextStyle as JavaTextStyle +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock import java.util.Locale import kotlin.math.roundToInt @@ -157,6 +160,13 @@ fun DayScreen( else -> true } + // Drives whether the title carries the year. Falls back to the clock only + // while the first load is in flight, when there is no state to read today from. + val currentYear = when (val s = state) { + is DayUiState.Success -> s.today.year + else -> Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date.year + } + // Slide direction for the day transition: +1 = next, -1 = prev, 0 = jump. var slideDir by remember { mutableIntStateOf(0) } val goNext = { slideDir = 1; viewModel.goToNext() } @@ -205,6 +215,7 @@ fun DayScreen( topBar = { DayTopBar( date = date, + currentYear = currentYear, selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, @@ -360,16 +371,18 @@ private fun DaySuccess( @Composable private fun DayTopBar( date: LocalDate, + currentYear: Int, selectedView: CalendarView, onCycleView: () -> Unit, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior, ) { + val locale = currentLocale() TopAppBar( title = { Text( - text = formatDayTitle(date), + text = formatDayTitle(date, locale, currentYear), style = MaterialTheme.typography.titleLarge, ) }, @@ -669,10 +682,10 @@ private fun DayLoading() { private fun minToHm(min: Int, is24Hour: Boolean, locale: Locale): String = formatMinuteOfDay(min, is24Hour, locale) -private fun formatDayTitle(date: LocalDate): String { - val locale = Locale.getDefault() - val java = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day) - val weekday = java.dayOfWeek.getDisplayName(JavaTextStyle.SHORT, locale) - val monthName = java.month.getDisplayName(JavaTextStyle.SHORT, locale) - return "$weekday, ${date.day}. $monthName ${date.year}" -} +private fun formatDayTitle(date: LocalDate, locale: Locale, currentYear: Int): String = + formatCalendarTitle( + date = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day), + locale = locale, + currentYear = currentYear, + skeleton = "EEEdMMM", + ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt index d4073a6..3d148fe 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt @@ -98,7 +98,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors import de.jeanlucmakiola.calendula.ui.common.eventFill import de.jeanlucmakiola.floret.components.OptionCard -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter import de.jeanlucmakiola.calendula.ui.common.recurrenceText diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 301cea9..11ec532 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -143,7 +143,7 @@ import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS import de.jeanlucmakiola.calendula.ui.common.TimePickerAlert import de.jeanlucmakiola.floret.reminders.ReminderUnit -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 100c71f..fbf5e26 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -64,6 +64,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.hasEnded +import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure @@ -79,7 +80,7 @@ import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec import de.jeanlucmakiola.floret.identity.rememberReduceMotion -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec import de.jeanlucmakiola.calendula.ui.common.next import de.jeanlucmakiola.floret.time.isoWeekNumber @@ -130,6 +131,13 @@ fun MonthScreen( else -> true } + // Drives whether the title carries the year. Falls back to the clock only + // while the first load is in flight, when there is no state to read today from. + val currentYear = when (val s = state) { + is MonthUiState.Success -> s.today.year + else -> Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date.year + } + // Slide direction for the grid transition: +1 = next, -1 = prev, 0 = jump (no slide). var slideDir by remember { mutableIntStateOf(0) } @@ -186,6 +194,7 @@ fun MonthScreen( topBar = { MonthTopBar( month = month, + currentYear = currentYear, selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, @@ -294,16 +303,18 @@ private fun MonthContent( @Composable private fun MonthTopBar( month: YearMonth, + currentYear: Int, selectedView: CalendarView, onCycleView: () -> Unit, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior, ) { + val locale = currentLocale() TopAppBar( title = { Text( - text = formatMonthYear(month), + text = formatMonthTitle(month, locale, currentYear), style = MaterialTheme.typography.titleLarge, ) }, @@ -732,9 +743,10 @@ private fun MonthGridLoading() { } } -private fun formatMonthYear(ym: YearMonth): String { - val locale = Locale.getDefault() - val name = java.time.Month.of(ym.month.ordinal + 1) - .getDisplayName(JavaTextStyle.FULL, locale) - return "$name ${ym.year}" -} +private fun formatMonthTitle(ym: YearMonth, locale: Locale, currentYear: Int): String = + formatCalendarTitle( + date = java.time.LocalDate.of(ym.year, ym.month.ordinal + 1, 1), + locale = locale, + currentYear = currentYear, + skeleton = "LLLL", + ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt index 9eec23e..505e753 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt @@ -54,7 +54,7 @@ import de.jeanlucmakiola.floret.identity.predictiveBack import de.jeanlucmakiola.floret.components.GroupedRow import de.jeanlucmakiola.floret.components.InlineTextField import de.jeanlucmakiola.floret.components.Position -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors import de.jeanlucmakiola.calendula.ui.common.eventFill import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 19f798b..dd5c76e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -135,7 +135,7 @@ import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel import de.jeanlucmakiola.calendula.ui.common.SnoozeDurationPicker import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel import de.jeanlucmakiola.calendula.ui.theme.BundledFont diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index 251e3b4..8cba3e2 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -77,6 +77,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.hasEnded +import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure @@ -93,7 +94,7 @@ import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec import de.jeanlucmakiola.floret.identity.rememberReduceMotion -import de.jeanlucmakiola.calendula.ui.common.currentLocale +import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines import de.jeanlucmakiola.calendula.ui.common.formatHourLabel @@ -180,6 +181,13 @@ fun WeekScreen( else -> true } + // Drives whether the title carries the year. Falls back to the clock only + // while the first load is in flight, when there is no state to read today from. + val currentYear = when (val s = state) { + is WeekUiState.Success -> s.today.year + else -> Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date.year + } + // Slide direction for the week transition: +1 = next, -1 = prev, 0 = jump. var slideDir by remember { mutableIntStateOf(0) } val goNext = { slideDir = 1; viewModel.goToNext() } @@ -228,6 +236,7 @@ fun WeekScreen( topBar = { WeekTopBar( weekStart = weekStart, + currentYear = currentYear, selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, @@ -395,16 +404,18 @@ private fun WeekSuccess( @Composable private fun WeekTopBar( weekStart: LocalDate, + currentYear: Int, selectedView: CalendarView, onCycleView: () -> Unit, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior, ) { + val locale = currentLocale() TopAppBar( title = { Text( - text = formatWeekRange(weekStart), + text = formatWeekTitle(weekStart, locale, currentYear), style = MaterialTheme.typography.titleLarge, ) }, @@ -854,18 +865,21 @@ private fun WeekLoading() { private fun minToHm(min: Int, is24Hour: Boolean, locale: java.util.Locale): String = formatMinuteOfDay(min, is24Hour, locale) -private fun formatWeekRange(weekStart: LocalDate): String { - val locale = Locale.getDefault() - val end = weekStart.plus(6, kotlinx.datetime.DateTimeUnit.DAY) - val monthName = { d: LocalDate -> - java.time.Month.of(d.month.ordinal + 1).getDisplayName(JavaTextStyle.SHORT, locale) - } - return if (weekStart.month == end.month && weekStart.year == end.year) { - "${weekStart.day}.–${end.day}. ${monthName(weekStart)} ${weekStart.year}" - } else if (weekStart.year == end.year) { - "${weekStart.day}. ${monthName(weekStart)} – ${end.day}. ${monthName(end)} ${end.year}" - } else { - "${weekStart.day}. ${monthName(weekStart)} ${weekStart.year} – " + - "${end.day}. ${monthName(end)} ${end.year}" - } -} +/** + * The week's title: just the month [weekStart] falls in. + * + * The day numbers are already in the column headers directly below, so spelling + * out "13.–19. Jul" restates them in the widest string in the bar. Naming the + * month of [weekStart] means a week straddling a boundary keeps the outgoing + * month until it is fully gone — a week is seven contiguous days, so the earlier + * month has a day in it exactly while [weekStart] is still inside it. That also + * makes the title depend on nothing but [weekStart], so it cannot drift with the + * direction you paged in from. + */ +private fun formatWeekTitle(weekStart: LocalDate, locale: Locale, currentYear: Int): String = + formatCalendarTitle( + date = java.time.LocalDate.of(weekStart.year, weekStart.month.ordinal + 1, 1), + locale = locale, + currentYear = currentYear, + skeleton = "LLLL", + ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index 039b4dc..2b048fc 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -55,7 +55,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay -import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter +import de.jeanlucmakiola.floret.locale.localizedDateFormatter import de.jeanlucmakiola.calendula.ui.common.eventFill import de.jeanlucmakiola.calendula.widget.AgendaWidgetData import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index 68b788a..24f19a3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -49,6 +49,7 @@ import de.jeanlucmakiola.calendula.MainActivity import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle import de.jeanlucmakiola.calendula.ui.month.MonthWeek import de.jeanlucmakiola.calendula.ui.month.layoutMonthWeeks import de.jeanlucmakiola.calendula.ui.common.eventFill @@ -162,7 +163,7 @@ private fun MonthWidgetBody(source: MonthWidgetSource, dark: Boolean, soften: Bo val colW = (LocalSize.current.width - GRID_HPADDING * 2) / 7 val weeks = layoutMonthWeeks(ym, source.weekStart, source.instances, zone) - MonthHeader(label = monthLabel(ym)) + MonthHeader(label = monthLabel(ym, source.today.year)) Spacer(GlanceModifier.height(2.dp)) WeekdayHeader(weekStart = source.weekStart, colW = colW) weeks.forEach { week -> @@ -480,9 +481,13 @@ private fun PermissionMessage() { } } -private fun monthLabel(month: YearMonth): String { - val locale = Locale.getDefault() - val name = java.time.Month.of(month.month.ordinal + 1) - .getDisplayName(JavaTextStyle.FULL, locale) - return "$name ${month.year}" -} +// Locale.getDefault() rather than currentLocale(): Glance has no +// LocalConfiguration, and the widget is re-rendered on a configuration change +// anyway (same convention as AgendaWidget). +private fun monthLabel(month: YearMonth, currentYear: Int): String = + formatCalendarTitle( + date = java.time.LocalDate.of(month.year, month.month.ordinal + 1, 1), + locale = Locale.getDefault(), + currentYear = currentYear, + skeleton = "LLLL", + ) diff --git a/floret-kit b/floret-kit index 2124227..45ca324 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit 2124227a7f462b46b5833250bc26d2ae6c7e2cd0 +Subproject commit 45ca3243feddc7dc7aad70b8e8337e9329bff51e