date: add ICU support for day/months names (#10457)

Fixes: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2130859
This commit is contained in:
Sylvestre Ledru
2026-01-25 17:35:34 +01:00
committed by GitHub
parent 61dd826a19
commit e1259d984f
9 changed files with 626 additions and 17 deletions
Generated
+139
View File
@@ -272,6 +272,16 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "calendrical_calculations"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"
dependencies = [
"core_maths",
"displaydoc",
]
[[package]]
name = "cc"
version = "1.2.52"
@@ -514,6 +524,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "coreutils"
version = "0.6.0"
@@ -1348,6 +1367,29 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_calendar"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar_data",
"icu_locale",
"icu_locale_core",
"icu_provider",
"ixdtf",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_calendar_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d"
[[package]]
name = "icu_collator"
version = "2.1.1"
@@ -1386,6 +1428,35 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_datetime"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5"
dependencies = [
"displaydoc",
"fixed_decimal",
"icu_calendar",
"icu_datetime_data",
"icu_decimal",
"icu_locale",
"icu_locale_core",
"icu_pattern",
"icu_plurals",
"icu_provider",
"icu_time",
"potential_utf",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_datetime_data"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb"
[[package]]
name = "icu_decimal"
version = "2.1.1"
@@ -1465,6 +1536,38 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_pattern"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b"
dependencies = [
"displaydoc",
"either",
"serde",
"writeable",
"zerovec",
]
[[package]]
name = "icu_plurals"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474"
dependencies = [
"fixed_decimal",
"icu_locale",
"icu_plurals_data",
"icu_provider",
"zerovec",
]
[[package]]
name = "icu_plurals_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0"
[[package]]
name = "icu_properties"
version = "2.1.2"
@@ -1502,6 +1605,30 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_time"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar",
"icu_locale_core",
"icu_provider",
"icu_time_data",
"ixdtf",
"serde",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_time_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf"
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -1600,6 +1727,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "ixdtf"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992"
[[package]]
name = "jiff"
version = "0.2.18"
@@ -3249,6 +3382,7 @@ dependencies = [
"clap",
"codspeed-divan-compat",
"fluent",
"icu_calendar",
"jiff",
"nix",
"parse_datetime",
@@ -4247,7 +4381,9 @@ dependencies = [
"fluent-syntax",
"glob",
"hex",
"icu_calendar",
"icu_collator",
"icu_datetime",
"icu_decimal",
"icu_locale",
"icu_provider",
@@ -4719,6 +4855,9 @@ name = "writeable"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
dependencies = [
"either",
]
[[package]]
name = "wyz"
+2
View File
@@ -329,7 +329,9 @@ gcd = "2.3"
glob = "0.3.1"
half = "2.4.1"
hostname = "0.4"
icu_calendar = "2.0.0"
icu_collator = "2.0.0"
icu_datetime = "2.0.0"
icu_decimal = "2.0.0"
icu_locale = "2.0.0"
icu_provider = "2.0.0"
+138
View File
@@ -194,6 +194,16 @@ version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]]
name = "calendrical_calculations"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"
dependencies = [
"core_maths",
"displaydoc",
]
[[package]]
name = "cc"
version = "1.2.51"
@@ -314,6 +324,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -649,6 +668,29 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_calendar"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar_data",
"icu_locale",
"icu_locale_core",
"icu_provider",
"ixdtf",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_calendar_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d"
[[package]]
name = "icu_collator"
version = "2.1.1"
@@ -687,6 +729,35 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_datetime"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5"
dependencies = [
"displaydoc",
"fixed_decimal",
"icu_calendar",
"icu_datetime_data",
"icu_decimal",
"icu_locale",
"icu_locale_core",
"icu_pattern",
"icu_plurals",
"icu_provider",
"icu_time",
"potential_utf",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_datetime_data"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb"
[[package]]
name = "icu_decimal"
version = "2.1.1"
@@ -766,6 +837,38 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_pattern"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b"
dependencies = [
"displaydoc",
"either",
"serde",
"writeable",
"zerovec",
]
[[package]]
name = "icu_plurals"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474"
dependencies = [
"fixed_decimal",
"icu_locale",
"icu_plurals_data",
"icu_provider",
"zerovec",
]
[[package]]
name = "icu_plurals_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0"
[[package]]
name = "icu_properties"
version = "2.1.2"
@@ -803,6 +906,30 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_time"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar",
"icu_locale_core",
"icu_provider",
"icu_time_data",
"ixdtf",
"serde",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_time_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf"
[[package]]
name = "intl-memoizer"
version = "0.5.3"
@@ -837,6 +964,12 @@ dependencies = [
"either",
]
[[package]]
name = "ixdtf"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992"
[[package]]
name = "jiff"
version = "0.2.18"
@@ -1745,7 +1878,9 @@ dependencies = [
"fluent-syntax",
"glob",
"hex",
"icu_calendar",
"icu_collator",
"icu_datetime",
"icu_decimal",
"icu_locale",
"icu_provider",
@@ -2073,6 +2208,9 @@ name = "writeable"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
dependencies = [
"either",
]
[[package]]
name = "yoke"
+5 -1
View File
@@ -18,16 +18,20 @@ workspace = true
[lib]
path = "src/date.rs"
[features]
i18n-datetime = ["uucore/i18n-datetime", "icu_calendar"]
[dependencies]
clap = { workspace = true }
fluent = { workspace = true }
icu_calendar = { workspace = true, optional = true }
jiff = { workspace = true, features = [
"tzdb-bundle-platform",
"tzdb-zoneinfo",
"tzdb-concatenated",
] }
parse_datetime = { workspace = true }
uucore = { workspace = true, features = ["parser"] }
uucore = { workspace = true, features = ["parser", "i18n-datetime"] }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true, features = ["time"] }
+106 -13
View File
@@ -20,6 +20,9 @@ use std::sync::OnceLock;
use uucore::display::Quotable;
use uucore::error::FromIo;
use uucore::error::{UResult, USimpleError};
use uucore::i18n::datetime::{
get_localized_day_name, get_localized_month_name, should_use_icu_locale,
};
use uucore::translate;
use uucore::{format_usage, show};
#[cfg(windows)]
@@ -474,20 +477,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let config = Config::new().custom(PosixCustom::new()).lenient(true);
for date in dates {
match date {
Ok(date) => {
match BrokenDownTime::from(&date).to_string_with_config(&config, format_string) {
Ok(s) => writeln!(stdout, "{s}").map_err(|e| {
USimpleError::new(1, translate!("date-error-write", "error" => e))
})?,
Err(e) => {
let _ = stdout.flush();
return Err(USimpleError::new(
1,
translate!("date-error-invalid-format", "format" => format_string, "error" => e),
));
}
Ok(date) => match format_date_with_locale_aware_months(&date, format_string, &config) {
Ok(s) => writeln!(stdout, "{s}").map_err(|e| {
USimpleError::new(1, translate!("date-error-write", "error" => e))
})?,
Err(e) => {
let _ = stdout.flush();
return Err(USimpleError::new(
1,
translate!("date-error-invalid-format", "format" => format_string, "error" => e),
));
}
}
},
Err((input, _err)) => {
let _ = stdout.flush();
show!(USimpleError::new(
@@ -612,6 +613,98 @@ pub fn uu_app() -> Command {
.arg(Arg::new(OPT_FORMAT).num_args(0..).trailing_var_arg(true))
}
fn format_date_with_locale_aware_months(
date: &Zoned,
format_string: &str,
config: &Config<PosixCustom>,
) -> Result<String, jiff::Error> {
// Only use ICU for non-default locales and when format string contains month or day specifiers
let use_icu = should_use_icu_locale();
if (format_string.contains("%B")
|| format_string.contains("%b")
|| format_string.contains("%A")
|| format_string.contains("%a"))
&& use_icu
{
let broken_down = BrokenDownTime::from(date);
// Get localized month names if needed
let (full_month, abbrev_month) =
if format_string.contains("%B") || format_string.contains("%b") {
if let Some(month_val) = broken_down.month() {
let month_u8 = if (1..=12).contains(&month_val) {
month_val as u8
} else {
1 // fallback to January for invalid values
};
(
get_localized_month_name(month_u8, true),
get_localized_month_name(month_u8, false),
)
} else {
(String::new(), String::new())
}
} else {
(String::new(), String::new())
};
// Get localized day names if needed
let (full_day, abbrev_day) = if format_string.contains("%A") || format_string.contains("%a")
{
if let (Some(year), Some(month), Some(day)) =
(broken_down.year(), broken_down.month(), broken_down.day())
{
(
get_localized_day_name(year.into(), month as u8, day as u8, true),
get_localized_day_name(year.into(), month as u8, day as u8, false),
)
} else {
(String::new(), String::new())
}
} else {
(String::new(), String::new())
};
// Replace format specifiers with placeholders for successful ICU translations only
let mut temp_format = format_string.to_string();
if !full_month.is_empty() {
temp_format = temp_format.replace("%B", "<<<FULL_MONTH>>>");
}
if !abbrev_month.is_empty() {
temp_format = temp_format.replace("%b", "<<<ABBREV_MONTH>>>");
}
if !full_day.is_empty() {
temp_format = temp_format.replace("%A", "<<<FULL_DAY>>>");
}
if !abbrev_day.is_empty() {
temp_format = temp_format.replace("%a", "<<<ABBREV_DAY>>>");
}
// Format with the temporary string
let temp_result = broken_down.to_string_with_config(config, &temp_format)?;
// Replace placeholders with localized names
let mut final_result = temp_result;
if !full_month.is_empty() {
final_result = final_result.replace("<<<FULL_MONTH>>>", &full_month);
}
if !abbrev_month.is_empty() {
final_result = final_result.replace("<<<ABBREV_MONTH>>>", &abbrev_month);
}
if !full_day.is_empty() {
final_result = final_result.replace("<<<FULL_DAY>>>", &full_day);
}
if !abbrev_day.is_empty() {
final_result = final_result.replace("<<<ABBREV_DAY>>>", &abbrev_day);
}
return Ok(final_result);
}
// Fallback to regular formatting
BrokenDownTime::from(date).to_string_with_config(config, format_string)
}
/// Return the appropriate format string for the given settings.
fn make_format_string(settings: &Settings) -> &str {
match settings.format {
+8 -1
View File
@@ -68,9 +68,15 @@ num-traits = { workspace = true, optional = true }
selinux = { workspace = true, optional = true }
# icu stuff
icu_calendar = { workspace = true, optional = true, features = [
"compiled_data",
] }
icu_collator = { workspace = true, optional = true, features = [
"compiled_data",
] }
icu_datetime = { workspace = true, optional = true, features = [
"compiled_data",
] }
icu_decimal = { workspace = true, optional = true, features = [
"compiled_data",
] }
@@ -143,10 +149,11 @@ format = [
"quoting-style",
"unit-prefix",
]
i18n-all = ["i18n-collator", "i18n-decimal"]
i18n-all = ["i18n-collator", "i18n-decimal", "i18n-datetime"]
i18n-common = ["icu_locale"]
i18n-collator = ["i18n-common", "icu_collator"]
i18n-decimal = ["i18n-common", "icu_decimal", "icu_provider"]
i18n-datetime = ["i18n-common", "icu_calendar", "icu_datetime"]
mode = ["libc"]
perms = ["entries", "libc", "walkdir"]
buf-copy = []
@@ -0,0 +1,131 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Locale-aware datetime formatting utilities using ICU
// spell-checker:ignore fieldsets janvier
use icu_calendar::Date;
use icu_datetime::DateTimeFormatter;
use icu_datetime::fieldsets;
use icu_locale::Locale;
use std::sync::OnceLock;
use crate::i18n::get_locale_from_env;
/// Get the locale for time/date formatting from LC_TIME environment variable
pub fn get_time_locale() -> &'static (Locale, super::UEncoding) {
static TIME_LOCALE: OnceLock<(Locale, super::UEncoding)> = OnceLock::new();
TIME_LOCALE.get_or_init(|| get_locale_from_env("LC_TIME"))
}
/// Check if we should use ICU for locale-aware time/date formatting
///
/// Returns true for non-C/POSIX locales, false otherwise
pub fn should_use_icu_locale() -> bool {
use icu_locale::locale;
let (locale, _encoding) = get_time_locale();
// Use ICU for non-default locales (anything other than C/POSIX)
// The default locale is "und" (undefined) representing C/POSIX
*locale != locale!("und")
}
/// Get a localized month name for the given month number (1-12)
///
/// # Arguments
/// * `month` - Month number (1 = January, 2 = February, etc.)
/// * `full` - If true, return full month name (e.g., "January"), otherwise abbreviated (e.g., "Jan")
///
/// # Returns
/// Localized month name, or falls back to English if locale is not supported
pub fn get_localized_month_name(month: u8, full: bool) -> String {
// Get locale from environment
let (locale, _encoding) = get_time_locale();
// Create a date with the specified month (use year 2000, day 1 as arbitrary values)
let Ok(date) = Date::try_new_gregorian(2000, month, 1) else {
// Invalid month, return empty string to signal failure
return String::new();
};
// Configure field set for month formatting
// Use Year-Month-Day format to ensure we get textual month names
let field_set = if full {
fieldsets::YMD::long()
} else {
fieldsets::YMD::medium()
};
// Create formatter with locale
let Ok(formatter) = DateTimeFormatter::try_new(locale.clone().into(), field_set) else {
// Failed to create formatter, return empty string to signal failure
return String::new();
};
// Format the date to get full date, then extract month
let formatted = formatter.format(&date).to_string();
// Extract month name from formatted date like "15 janvier 2000" or "2000-01-15"
// Look for a word that contains letters (the month name)
let words: Vec<&str> = formatted.split_whitespace().collect();
// Return the month name as extracted from ICU (no further processing needed)
// ICU already handles the full vs abbreviated formatting correctly
words
.iter()
.find(|word| word.chars().any(|c| c.is_alphabetic()))
.map_or_else(String::new, |s| (*s).to_string())
}
/// Get a localized day name for the given date components
///
/// # Arguments
/// * `year` - The year
/// * `month` - The month (1-12)
/// * `day` - The day of the month
/// * `full` - If true, return full day name (e.g., "Monday"), otherwise abbreviated (e.g., "Mon")
///
/// # Returns
/// Localized day name, or falls back to empty string if locale is not supported
pub fn get_localized_day_name(year: i32, month: u8, day: u8, full: bool) -> String {
// Create ICU Date from components
let Ok(date) = Date::try_new_gregorian(year, month, day) else {
return String::new();
};
// Get locale from environment
let (locale, _encoding) = get_time_locale();
// Configure field set for day formatting
let field_set = if full {
fieldsets::E::long() // Full day name
} else {
fieldsets::E::short() // Abbreviated day name
};
// Create formatter with locale
let Ok(formatter) = DateTimeFormatter::try_new(locale.clone().into(), field_set) else {
return String::new();
};
// Format the date to get day name
let formatted = formatter.format(&date).to_string();
formatted.trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_localized_month_name_fallback() {
// This should work even if locale is not available
let name = get_localized_month_name(1, true);
// The function may return empty string if ICU fails, which is fine
// The caller (date.rs) will handle this by falling back to jiff
assert!(name.is_empty() || name.len() >= 3);
}
}
+3 -1
View File
@@ -9,6 +9,8 @@ use icu_locale::{Locale, locale};
#[cfg(feature = "i18n-collator")]
pub mod collator;
#[cfg(feature = "i18n-datetime")]
pub mod datetime;
#[cfg(feature = "i18n-decimal")]
pub mod decimal;
@@ -31,7 +33,7 @@ const DEFAULT_LOCALE: Locale = locale!("und");
/// 3. LANG
///
/// Or fallback on Posix locale, with ASCII encoding.
fn get_locale_from_env(locale_name: &str) -> (Locale, UEncoding) {
pub fn get_locale_from_env(locale_name: &str) -> (Locale, UEncoding) {
let locale_var = ["LC_ALL", locale_name, "LANG"]
.iter()
.find_map(|&key| std::env::var(key).ok());
+94 -1
View File
@@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds
// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes
use std::cmp::Ordering;
@@ -1470,6 +1470,99 @@ fn test_date_posix_format_specifiers() {
}
}
#[test]
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn test_date_format_b_french_locale() {
// Test both %B and %b formats with French locale using a loop
// This test expects localized month names when i18n support is available
let test_cases = [
("2025-01-15", "janvier", "janv."), // Wednesday = mercredi, mer.
("2025-02-15", "février", "févr."), // Saturday = samedi, sam.
];
for (date, expected_full, expected_abbrev) in &test_cases {
let result = new_ucmd!()
.env("LC_TIME", "fr_FR.UTF-8")
.env("TZ", "UTC")
.arg("-d")
.arg(date)
.arg("+%B %b")
.succeeds();
let output = result.stdout_str().trim();
let expected = format!("{expected_full} {expected_abbrev}");
if output == expected {
// i18n feature is working - test passed
assert_eq!(output, expected);
} else {
// i18n feature not available, skip test
println!(
"Skipping French locale test for {date} - i18n feature not available, got: {output}"
);
return; // Exit early if i18n not available
}
}
}
#[test]
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn test_date_format_a_french_locale() {
// Test both %A and %a formats with French locale using a loop
// This test expects localized day names when i18n support is available
let test_cases = [
("2025-01-15", "mercredi", "mer."), // Wednesday
("2025-02-15", "samedi", "sam."), // Saturday
];
for (date, expected_full, expected_abbrev) in &test_cases {
let result = new_ucmd!()
.env("LC_TIME", "fr_FR.UTF-8")
.env("TZ", "UTC")
.arg("-d")
.arg(date)
.arg("+%A %a")
.succeeds();
let output = result.stdout_str().trim();
let expected = format!("{expected_full} {expected_abbrev}");
if output == expected {
// i18n feature is working - test passed
assert_eq!(output, expected);
} else {
// i18n feature not available, skip test
println!(
"Skipping French day locale test for {date} - i18n feature not available, got: {output}"
);
return; // Exit early if i18n not available
}
}
}
#[test]
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn test_date_french_full_sentence() {
let result = new_ucmd!()
.env("LANG", "fr_FR.UTF-8")
.env("TZ", "UTC")
.arg("-d")
.arg("2026-01-21")
.arg("+Nous sommes le %A %d %B %Y")
.succeeds();
let output = result.stdout_str().trim();
let expected = "Nous sommes le mercredi 21 janvier 2026";
if output == expected {
// i18n feature is working - test passed
assert_eq!(output, expected);
} else {
// i18n feature not available, skip test
println!("Skipping French full sentence test - i18n feature not available, got: {output}");
}
}
/// Test that %x format specifier respects locale settings
/// This is a regression test for locale-aware date formatting
#[test]