Localize sidebar dates and calendar months

This commit is contained in:
2026-07-22 20:28:40 +02:00
parent 7b8e539340
commit 0d44c21b0e
3 changed files with 57 additions and 28 deletions

View File

@@ -6,7 +6,7 @@ The project is under active development. Core blogging workflows are broadly ava
## Available Features ## Available Features
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, relative-dated entity lists with row deletion, dialogs, tasks, embedded Wry previews, a live Pico CSS theme editor, and per-project restart restoration of the active activity, shell visibility, and open editor tabs. - Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars with locale-aware post dates, calendar months, and relative-dated entity lists, row deletion, dialogs, tasks, embedded Wry previews, a live Pico CSS theme editor, and per-project restart restoration of the active activity, shell visibility, and open editor tabs.
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import. - Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import.
- Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. - Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.

View File

@@ -3,7 +3,7 @@ use chrono::{DateTime, Datelike, Local, NaiveDate};
/// chrono format locale per sidebar_views.allium LocaleMapping /// chrono format locale per sidebar_views.allium LocaleMapping
/// (ui_locale → format_locale, e.g. "de" → "de-DE"). /// (ui_locale → format_locale, e.g. "de" → "de-DE").
fn format_locale(locale: UiLocale) -> chrono::Locale { pub(crate) fn format_locale(locale: UiLocale) -> chrono::Locale {
match locale { match locale {
UiLocale::En => chrono::Locale::en_US, UiLocale::En => chrono::Locale::en_US,
UiLocale::De => chrono::Locale::de_DE, UiLocale::De => chrono::Locale::de_DE,

View File

@@ -9,7 +9,7 @@ use bds_core::model::{ChatConversation, ImportDefinition, Media, Post, Script, T
use crate::app::Message; use crate::app::Message;
use crate::components::inputs; use crate::components::inputs;
use crate::i18n::relative_date::format_relative_date; use crate::i18n::relative_date::{format_locale, format_relative_date};
use crate::i18n::t; use crate::i18n::t;
use crate::state::navigation::SidebarView; use crate::state::navigation::SidebarView;
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter}; use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
@@ -145,12 +145,21 @@ fn post_type_icon(categories: &[String]) -> &'static str {
"\u{1F4C4}" // 📄 document (default) "\u{1F4C4}" // 📄 document (default)
} }
/// Per sidebar_views.allium PostDateFormat: "Feb 10, 2026". /// Per sidebar_views.allium PostDateFormat: locale-formatted short month,
fn format_post_date(unix_ms: i64) -> String { /// numeric day, and numeric year (for example, "Feb 10, 2026" in en-US).
let secs = unix_ms / 1000; fn format_post_date(locale: UiLocale, unix_ms: i64) -> String {
let dt = chrono::DateTime::from_timestamp(secs, 0) let timestamp = chrono::DateTime::from_timestamp_millis(unix_ms)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap()); .unwrap_or_default()
dt.format("%b %d, %Y").to_string() .with_timezone(&chrono::Local);
let pattern = match locale {
UiLocale::En => "%b %-d, %Y",
UiLocale::De => "%-d. %b. %Y",
UiLocale::Fr | UiLocale::It | UiLocale::Es => "%-d %b %Y",
};
timestamp
.format_localized(pattern, format_locale(locale))
.to_string()
} }
/// Search input style. /// Search input style.
@@ -314,23 +323,14 @@ fn with_row_delete(
) )
} }
/// Month name abbreviation for calendar display. /// Locale-formatted short month name for calendar display.
fn month_abbr(month: u32) -> &'static str { fn month_abbr(locale: UiLocale, month: u32) -> String {
match month { chrono::NaiveDate::from_ymd_opt(2000, month, 1)
1 => "Jan", .map(|date| {
2 => "Feb", date.format_localized("%b", format_locale(locale))
3 => "Mar", .to_string()
4 => "Apr", })
5 => "May", .unwrap_or_default()
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
_ => "???",
}
} }
/// Build the calendar archive tree widget. /// Build the calendar archive tree widget.
@@ -376,7 +376,7 @@ fn calendar_widget(
if year_selected { if year_selected {
for cm in &cy.months { for cm in &cy.months {
let month_selected = selected_month == Some(cm.month); let month_selected = selected_month == Some(cm.month);
let label = format!(" {} ({})", month_abbr(cm.month), cm.count); let label = format!(" {} ({})", month_abbr(locale, cm.month), cm.count);
let month_val = cm.month; let month_val = cm.month;
let on_month_clone = on_month.clone(); let on_month_clone = on_month.clone();
let style_fn = if month_selected { let style_fn = if month_selected {
@@ -797,7 +797,7 @@ pub fn view(
bds_core::model::PostStatus::Published => "\u{25CF}", bds_core::model::PostStatus::Published => "\u{25CF}",
bds_core::model::PostStatus::Archived => "\u{25A1}", bds_core::model::PostStatus::Archived => "\u{25A1}",
}; };
let date = format_post_date(p.created_at); let date = format_post_date(locale, p.created_at);
let prefix_chars: usize = 5; let prefix_chars: usize = 5;
let text_px = width let text_px = width
- SIDEBAR_TEXT_OVERHEAD_PX - SIDEBAR_TEXT_OVERHEAD_PX
@@ -1320,6 +1320,35 @@ pub fn view(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use chrono::{Local, TimeZone};
fn local_ms(year: i32, month: u32, day: u32) -> i64 {
Local
.with_ymd_and_hms(year, month, day, 12, 0, 0)
.single()
.expect("unambiguous local time")
.timestamp_millis()
}
#[test]
fn post_date_uses_locale_order_month_name_and_punctuation() {
let timestamp = local_ms(2026, 2, 10);
assert_eq!(format_post_date(UiLocale::En, timestamp), "Feb 10, 2026");
assert_eq!(format_post_date(UiLocale::De, timestamp), "10. Feb. 2026");
assert_eq!(format_post_date(UiLocale::Fr, timestamp), "10 févr. 2026");
assert_eq!(format_post_date(UiLocale::It, timestamp), "10 feb 2026");
assert_eq!(format_post_date(UiLocale::Es, timestamp), "10 feb 2026");
}
#[test]
fn calendar_month_abbreviations_use_ui_locale() {
assert_eq!(month_abbr(UiLocale::En, 3), "Mar");
assert_eq!(month_abbr(UiLocale::De, 3), "Mär");
assert_eq!(month_abbr(UiLocale::Fr, 2), "févr.");
assert_eq!(month_abbr(UiLocale::It, 1), "gen");
assert_eq!(month_abbr(UiLocale::Es, 1), "ene");
}
#[test] #[test]
fn truncate_media_title_short() { fn truncate_media_title_short() {