From 6274a66ae958a2cf3c5436cb465001d77721f013 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 24 Sep 2025 06:29:59 +0200 Subject: [PATCH] fix(time): icu locale requires hyphenated lang codes The recent icu 2.0.0 update broke `icu::locale::Locale` parsing. Lang codes containing underscores are no longer valid, so we need to hyphenate the lang code before parsing it. I've added `replacen('_', "-", 1)` everywhere that an `icu::locale::Locale` is parsed from a string. To prevent the need to re-parse the locale on every view update, I've also added `icu::locale::Locale`s for `LC_TIME` and `LC_NUMERIC` directly to `region::Page` the moment the page is refreshed. Closes #1384 --- cosmic-settings/src/pages/time/date.rs | 15 ++- cosmic-settings/src/pages/time/region.rs | 113 ++++++++--------------- 2 files changed, 47 insertions(+), 81 deletions(-) diff --git a/cosmic-settings/src/pages/time/date.rs b/cosmic-settings/src/pages/time/date.rs index c44f640..b37c43b 100644 --- a/cosmic-settings/src/pages/time/date.rs +++ b/cosmic-settings/src/pages/time/date.rs @@ -1,8 +1,6 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use std::str::FromStr; - use chrono::{Datelike, Timelike}; use cosmic::{ Apply, Element, Task, @@ -507,14 +505,15 @@ fn timezone() -> Section { } fn locale() -> Result> { - let locale = std::env::var("LC_TIME").or_else(|_| std::env::var("LANG"))?; - let locale = locale + let locale_env = std::env::var("LC_TIME").or_else(|_| std::env::var("LANG"))?; + + locale_env .split('.') .next() - .ok_or(format!("Can't split the locale {locale}"))?; - - let locale = Locale::from_str(locale).map_err(|e| format!("{e:?}"))?; - Ok(locale) + .ok_or(format!("Can't split the locale {locale_env}"))? + .replacen("_", "-", 1) + .parse::() + .map_err(|e| format!("{e:?}").into()) } fn format_date(date: &DateTime, military: bool, show_seconds: bool) -> String { diff --git a/cosmic-settings/src/pages/time/region.rs b/cosmic-settings/src/pages/time/region.rs index f423c02..4964de8 100644 --- a/cosmic-settings/src/pages/time/region.rs +++ b/cosmic-settings/src/pages/time/region.rs @@ -3,7 +3,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::rc::Rc; -use std::str::FromStr; use std::sync::Arc; use cosmic::app::{ContextDrawer, context_drawer}; @@ -32,7 +31,6 @@ pub enum Message { AddLanguage(DefaultKey), AddLanguageContext, AddLanguageSearch(String), - SystemLocales(SlotMap), ExpandLanguagePopover(Option), InstallAdditionalLanguages, SelectRegion(DefaultKey), @@ -115,6 +113,10 @@ pub struct Page { registry: Option, expanded_source_popover: Option, add_language_search: String, + /// Cached LC_NUMERIC locale in icu locale format. + numeric_locale: Option, + /// Cached LC_TIME locale in icu locale format. + time_locale: Option, } impl page::Page for Page { @@ -251,10 +253,6 @@ impl Page { self.add_language_search = search; } - Message::SystemLocales(languages) => { - self.available_languages = languages; - } - Message::ExpandLanguagePopover(id) => { self.expanded_source_popover = id; } @@ -277,6 +275,8 @@ impl Page { self.language = page_refresh.language; self.region = page_refresh.region; self.registry = Some(page_refresh.registry.0); + self.numeric_locale = self.icu_locale_from_env("LC_NUMERIC"); + self.time_locale = self.icu_locale_from_env("LC_TIME"); } Err(why) => { @@ -397,17 +397,21 @@ impl Page { list.apply(Element::from).map(crate::pages::Message::Region) } - fn formatted_date(&self) -> String { - let time_locale = self - .system_locales - .get("LC_TIME") + fn icu_locale_from_env(&self, key: &'static str) -> Option { + self.system_locales + .get(key) .or_else(|| self.system_locales.get("LANG")) - .map_or("en_US", |locale| &locale.lang_code) + .map_or("en-US", |locale| &locale.lang_code) .split('.') .next() - .unwrap_or("en_US"); + .unwrap_or("en-US") + .replacen('_', "-", 1) + .parse::() + .ok() + } - let Ok(locale) = Locale::from_str(time_locale) else { + fn formatted_date(&self) -> String { + let Some(locale) = self.time_locale.as_ref() else { return String::new(); }; @@ -423,16 +427,7 @@ impl Page { } fn formatted_dates_and_times(&self) -> String { - let time_locale = self - .system_locales - .get("LC_TIME") - .or_else(|| self.system_locales.get("LANG")) - .map_or("en_US", |locale| &locale.lang_code) - .split('.') - .next() - .unwrap_or("en_US"); - - let Ok(locale) = Locale::from_str(time_locale) else { + let Some(locale) = self.time_locale.as_ref() else { return String::new(); }; @@ -448,16 +443,7 @@ impl Page { } fn formatted_time(&self) -> String { - let time_locale = self - .system_locales - .get("LC_TIME") - .or_else(|| self.system_locales.get("LANG")) - .map_or("en_US", |locale| &locale.lang_code) - .split('.') - .next() - .unwrap_or("en_US"); - - let Ok(locale) = Locale::from_str(time_locale) else { + let Some(locale) = self.time_locale.as_ref() else { return String::new(); }; @@ -473,16 +459,7 @@ impl Page { } fn formatted_numbers(&self) -> String { - let numerical_locale = self - .system_locales - .get("LC_NUMERIC") - .or_else(|| self.system_locales.get("LANG")) - .map_or("en_US", |locale| &locale.lang_code) - .split('.') - .next() - .unwrap_or("en_US"); - - let Ok(locale) = Locale::from_str(numerical_locale) else { + let Some(locale) = self.numeric_locale.as_ref() else { return String::new(); }; @@ -621,22 +598,13 @@ mod formatting { pub fn section() -> Section { crate::slab!(descriptions { formatting_txt = fl!("formatting"); - dates_txt = fl!("formatting", "dates"); - time_txt = fl!("formatting", "time"); - date_and_time_txt = fl!("formatting", "date-and-time"); - numbers_txt = fl!("formatting", "numbers"); - // measurement_txt = fl!("formatting", "measurement"); - // paper_txt = fl!("formatting", "paper"); + dates_txt = [&fl!("formatting", "dates"), ":"].concat(); + time_txt = [&fl!("formatting", "time"), ":"].concat(); + date_and_time_txt = [&fl!("formatting", "date-and-time"), ":"].concat(); + numbers_txt = [&fl!("formatting", "numbers"), ":"].concat(); region_txt = fl!("region"); }); - let dates_label = [&descriptions[dates_txt], ":"].concat(); - let time_label = [&descriptions[time_txt], ":"].concat(); - let date_and_time_label = [&descriptions[date_and_time_txt], ":"].concat(); - let numbers_label = [&descriptions[numbers_txt], ":"].concat(); - // let measurement_label = [&descriptions[measurement_txt], ":"].concat(); - // let paper_label = [&descriptions[paper_txt], ":"].concat(); - Section::default() .title(fl!("formatting")) .descriptions(descriptions) @@ -644,17 +612,17 @@ mod formatting { let desc = §ion.descriptions; let dates = widget::row::with_capacity(2) - .push(widget::text::body(dates_label.clone())) + .push(widget::text::body(&desc[dates_txt])) .push(widget::text::body(page.formatted_date()).font(cosmic::font::bold())) .spacing(4); let time = widget::row::with_capacity(2) - .push(widget::text::body(time_label.clone())) + .push(widget::text::body(&desc[time_txt])) .push(widget::text::body(page.formatted_time()).font(cosmic::font::bold())) .spacing(4); let dates_and_times = widget::row::with_capacity(2) - .push(widget::text::body(date_and_time_label.clone())) + .push(widget::text::body(&desc[date_and_time_txt])) .push( widget::text::body(page.formatted_dates_and_times()) .font(cosmic::font::bold()), @@ -662,7 +630,7 @@ mod formatting { .spacing(4); let numbers = widget::row::with_capacity(2) - .push(widget::text::body(numbers_label.clone())) + .push(widget::text::body(&desc[numbers_txt])) .push(widget::text::body(page.formatted_numbers()).font(cosmic::font::bold())) .spacing(4); @@ -934,18 +902,17 @@ pub async fn set_locale(lang: String, region: String) { .await; } -fn parse_locale(locale: String) -> Result> { - let locale = locale +fn parse_locale(locale: &str) -> Option { + locale .split('.') - .next() - .ok_or(format!("Can't split the locale {locale}"))?; - - let locale = Locale::from_str(locale).map_err(|e| format!("{e:?}"))?; - Ok(locale) + .next()? + .replacen('_', "-", 1) + .parse::() + .ok() } -fn get_default_24h(locale: String) -> bool { - let Ok(locale) = parse_locale(locale) else { +fn get_default_24h(locale: &str) -> bool { + let Some(locale) = parse_locale(locale) else { return false; }; @@ -966,8 +933,8 @@ fn get_default_24h(locale: String) -> bool { formatted.contains("13") } -fn get_default_first_day(locale: String) -> usize { - let Ok(locale) = parse_locale(locale) else { +fn get_default_first_day(locale: &str) -> usize { + let Some(locale) = parse_locale(locale) else { return 6; }; let Ok(week_info) = week::WeekInformation::try_new(week::WeekPreferences::from(&locale)) else { @@ -1000,13 +967,13 @@ fn update_time_settings_after_region_change(region: String) { }; // Update military_time based on new locale - let new_military_time = get_default_24h(region.clone()); + let new_military_time = get_default_24h(®ion); if let Err(why) = cosmic_applet_config.set("military_time", new_military_time) { tracing::error!(?why, "Failed to update military_time after region change"); } // Update first_day_of_week based on new locale - let new_first_day = get_default_first_day(region); + let new_first_day = get_default_first_day(®ion); if let Err(why) = cosmic_applet_config.set("first_day_of_week", new_first_day) { tracing::error!( ?why,