diff --git a/Cargo.lock b/Cargo.lock index 17c05da50f0..acd8dc124f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,18 +337,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.55" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.55" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -1008,7 +1008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1355,6 +1355,7 @@ dependencies = [ "icu_locale_core", "icu_provider", "ixdtf", + "serde", "tinystr", "zerovec", ] @@ -1720,7 +1721,18 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-icu" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646" +dependencies = [ + "icu_calendar", + "icu_time", + "jiff", ] [[package]] @@ -2003,7 +2015,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2593,7 +2605,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2903,7 +2915,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3378,6 +3390,7 @@ dependencies = [ "icu_calendar", "icu_locale", "jiff", + "jiff-icu", "nix", "parse_datetime", "tempfile", @@ -4456,6 +4469,7 @@ dependencies = [ "icu_provider", "itertools 0.14.0", "jiff", + "jiff-icu", "libc", "md-5", "memchr", @@ -4649,7 +4663,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 12e062f8f12..228272604a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -346,6 +346,7 @@ indicatif = "0.18.0" itertools = "0.14.0" itoa = "1.0.15" jiff = "0.2.18" +jiff-icu = "0.2.2" libc = "0.2.172" lscolors = { version = "0.21.0", default-features = false, features = [ "gnu_legacy", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 62bd4b15749..c4d05638def 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -131,9 +131,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake2b_simd" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" dependencies = [ "arrayref", "arrayvec", @@ -142,15 +142,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", ] [[package]] @@ -206,9 +207,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "jobserver", @@ -230,9 +231,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "num-traits", @@ -263,9 +264,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" @@ -307,16 +308,16 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation-sys" @@ -415,15 +416,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-encoding-macro" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -431,9 +432,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", "syn", @@ -517,9 +518,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixed_decimal" @@ -534,9 +535,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -605,9 +606,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -681,6 +682,7 @@ dependencies = [ "icu_locale_core", "icu_provider", "ixdtf", + "serde", "tinystr", "zerovec", ] @@ -810,9 +812,9 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" +checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" [[package]] name = "icu_normalizer" @@ -985,6 +987,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "jiff-icu" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646" +dependencies = [ + "icu_calendar", + "icu_time", + "jiff", +] + [[package]] name = "jiff-static" version = "0.2.18" @@ -1023,9 +1036,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1058,9 +1071,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" @@ -1283,9 +1296,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1316,9 +1329,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1351,9 +1364,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -1540,9 +1553,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.113" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1585,18 +1598,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1731,6 +1744,7 @@ dependencies = [ "icu_calendar", "icu_locale", "jiff", + "jiff-icu", "nix", "parse_datetime", "uucore", @@ -1897,6 +1911,8 @@ dependencies = [ "icu_locale", "icu_provider", "itertools", + "jiff", + "jiff-icu", "libc", "md-5", "memchr", @@ -1982,18 +1998,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -2004,9 +2020,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2014,9 +2030,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -2027,9 +2043,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -2205,9 +2221,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "write16" @@ -2255,18 +2271,18 @@ checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" dependencies = [ "proc-macro2", "quote", diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 8820d96b98c..3cf27595e24 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -20,13 +20,19 @@ path = "src/date.rs" [features] default = ["i18n-datetime"] -i18n-datetime = ["uucore/i18n-datetime", "dep:icu_calendar", "dep:icu_locale"] +i18n-datetime = [ + "uucore/i18n-datetime", + "dep:icu_calendar", + "dep:icu_locale", + "dep:jiff-icu", +] [dependencies] clap = { workspace = true } fluent = { workspace = true } icu_calendar = { workspace = true, optional = true } icu_locale = { workspace = true, optional = true } +jiff-icu = { workspace = true, optional = true } jiff = { workspace = true, features = [ "tzdb-bundle-platform", "tzdb-zoneinfo", diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index f82fe1c38bf..cc507c78176 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -21,10 +21,7 @@ use uucore::display::Quotable; use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; #[cfg(feature = "i18n-datetime")] -use uucore::i18n::datetime::{ - get_era_year, get_localized_day_name, get_localized_month_name, get_time_locale, - should_use_icu_locale, -}; +use uucore::i18n::datetime::{localize_format_string, should_use_icu_locale}; use uucore::translate; use uucore::{format_usage, show}; #[cfg(windows)] @@ -620,112 +617,14 @@ fn format_date_with_locale_aware_months( format_string: &str, config: &Config, ) -> Result { - // Only use ICU for non-English locales and when format string contains month, day, or era year specifiers - if (format_string.contains("%B") - || format_string.contains("%b") - || format_string.contains("%A") - || format_string.contains("%a") - || format_string.contains("%Y") - || format_string.contains("%Ey")) - && should_use_icu_locale() - { - 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()) - }; - - // Get era year if needed - let era_year = if format_string.contains("%Y") || format_string.contains("%Ey") { - if let (Some(year), Some(month), Some(day)) = - (broken_down.year(), broken_down.month(), broken_down.day()) - { - let (locale, _encoding) = get_time_locale(); - get_era_year(year.into(), month as u8, day as u8, locale) - } else { - None - } - } else { - None - }; - - // Replace format specifiers with NULL-byte placeholders for successful ICU translations only - // Use NULL bytes to avoid collision with user format strings - let mut temp_format = format_string.to_string(); - if !full_month.is_empty() { - temp_format = temp_format.replace("%B", "\0FULL_MONTH\0"); - } - if !abbrev_month.is_empty() { - temp_format = temp_format.replace("%b", "\0ABBREV_MONTH\0"); - } - if !full_day.is_empty() { - temp_format = temp_format.replace("%A", "\0FULL_DAY\0"); - } - if !abbrev_day.is_empty() { - temp_format = temp_format.replace("%a", "\0ABBREV_DAY\0"); - } - if era_year.is_some() { - temp_format = temp_format.replace("%Y", "\0ERA_YEAR\0"); - } - - // Format with the temporary string - let temp_result = broken_down.to_string_with_config(config, &temp_format)?; - - // Replace NULL-byte placeholders with localized names - let mut final_result = temp_result; - if !full_month.is_empty() { - final_result = final_result.replace("\0FULL_MONTH\0", &full_month); - } - if !abbrev_month.is_empty() { - final_result = final_result.replace("\0ABBREV_MONTH\0", &abbrev_month); - } - if !full_day.is_empty() { - final_result = final_result.replace("\0FULL_DAY\0", &full_day); - } - if !abbrev_day.is_empty() { - final_result = final_result.replace("\0ABBREV_DAY\0", &abbrev_day); - } - if let Some(era_year_val) = era_year { - final_result = final_result.replace("\0ERA_YEAR\0", &era_year_val.to_string()); - } + let broken_down = BrokenDownTime::from(date); - return Ok(final_result); + if !should_use_icu_locale() { + return broken_down.to_string_with_config(config, format_string); } - // Fallback to regular formatting - BrokenDownTime::from(date).to_string_with_config(config, format_string) + let fmt = localize_format_string(format_string, &date.date()); + broken_down.to_string_with_config(config, &fmt) } /// Return the appropriate format string for the given settings. diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 507f7740c6e..d18d0630ed5 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -82,6 +82,7 @@ icu_decimal = { workspace = true, optional = true, features = [ ] } icu_locale = { workspace = true, optional = true, features = ["compiled_data"] } icu_provider = { workspace = true, optional = true } +jiff-icu = { workspace = true, optional = true } # Fluent dependencies (always available for localization) fluent = { workspace = true } @@ -153,7 +154,13 @@ 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"] +i18n-datetime = [ + "i18n-common", + "icu_calendar", + "icu_datetime", + "jiff-icu", + "jiff", +] mode = ["libc"] perms = ["entries", "libc", "walkdir"] buf-copy = [] diff --git a/src/uucore/src/lib/features/i18n/datetime.rs b/src/uucore/src/lib/features/i18n/datetime.rs index e5d6a666286..68721e8fbae 100644 --- a/src/uucore/src/lib/features/i18n/datetime.rs +++ b/src/uucore/src/lib/features/i18n/datetime.rs @@ -3,13 +3,17 @@ // 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 +// spell-checker:ignore fieldsets prefs + +//! Locale-aware datetime formatting utilities using ICU and jiff-icu use icu_calendar::Date; +use icu_calendar::cal::{Buddhist, Ethiopian, Iso, Persian}; use icu_datetime::DateTimeFormatter; use icu_datetime::fieldsets; use icu_locale::Locale; +use jiff::civil::Date as JiffDate; +use jiff_icu::ConvertFrom; use std::sync::OnceLock; use crate::i18n::get_locale_from_env; @@ -34,88 +38,6 @@ pub fn should_use_icu_locale() -> bool { *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() -} - /// Determine the appropriate calendar system for a given locale pub fn get_locale_calendar_type(locale: &Locale) -> CalendarType { let locale_str = locale.to_string(); @@ -145,120 +67,90 @@ pub enum CalendarType { Ethiopian, } -/// Convert a Gregorian date to the appropriate calendar system for a locale -/// -/// # Arguments -/// * `year` - Gregorian year -/// * `month` - Month (1-12) -/// * `day` - Day (1-31) -/// * `calendar_type` - Target calendar system -/// -/// # Returns -/// * `Some((era_year, month, day))` - Date in target calendar system -/// * `None` - If conversion fails -pub fn convert_date_to_locale_calendar( - year: i32, - month: u8, - day: u8, - calendar_type: &CalendarType, -) -> Option<(i32, u8, u8)> { - match calendar_type { - CalendarType::Gregorian => Some((year, month, day)), - CalendarType::Buddhist => { - // Buddhist calendar: Gregorian year + 543 - Some((year + 543, month, day)) +/// Transform a strftime format string to use locale-specific calendar values +pub fn localize_format_string(format: &str, date: &JiffDate) -> String { + let (locale, _) = get_time_locale(); + let iso_date = Date::::convert_from(*date); + + let mut fmt = format.to_string(); + + // For non-Gregorian calendars, replace date components with converted values + let calendar_type = get_locale_calendar_type(locale); + if calendar_type != CalendarType::Gregorian { + let (cal_year, cal_month, cal_day) = match calendar_type { + CalendarType::Buddhist => { + let d = iso_date.to_calendar(Buddhist); + (d.extended_year(), d.month().ordinal, d.day_of_month().0) + } + CalendarType::Persian => { + let d = iso_date.to_calendar(Persian); + (d.extended_year(), d.month().ordinal, d.day_of_month().0) + } + CalendarType::Ethiopian => { + let d = iso_date.to_calendar(Ethiopian::new()); + (d.extended_year(), d.month().ordinal, d.day_of_month().0) + } + CalendarType::Gregorian => unreachable!(), + }; + fmt = fmt + .replace("%Y", &cal_year.to_string()) + .replace("%m", &format!("{cal_month:02}")) + .replace("%d", &format!("{cal_day:02}")) + .replace("%e", &format!("{cal_day:2}")); + } + + // Format localized names using ICU DateTimeFormatter + let locale_prefs = locale.clone().into(); + + if fmt.contains("%B") { + if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::M::long()) { + fmt = fmt.replace("%B", &f.format(&iso_date).to_string()); } - CalendarType::Persian => { - // Persian calendar conversion (Solar Hijri) - // March 21 (Nowruz) is roughly the start of the Persian year - let persian_year = if month > 3 || (month == 3 && day >= 21) { - year - 621 // After March 21 - } else { - year - 622 // Before March 21 - }; - Some((persian_year, month, day)) + } + if fmt.contains("%b") || fmt.contains("%h") { + if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::M::medium()) { + let month_abbrev = f.format(&iso_date).to_string(); + fmt = fmt + .replace("%b", &month_abbrev) + .replace("%h", &month_abbrev); } - CalendarType::Ethiopian => { - // Ethiopian calendar conversion - // September 11/12 is roughly the start of the Ethiopian year - let ethiopian_year = if month > 9 || (month == 9 && day >= 11) { - year - 7 // After September 11 - } else { - year - 8 // Before September 11 - }; - Some((ethiopian_year, month, day)) + } + if fmt.contains("%A") { + if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::E::long()) { + fmt = fmt.replace("%A", &f.format(&iso_date).to_string()); } } -} - -/// Get the era year for a given date and locale -pub fn get_era_year(year: i32, month: u8, day: u8, locale: &Locale) -> Option { - // Validate input date - if !(1..=12).contains(&month) || !(1..=31).contains(&day) { - return None; + if fmt.contains("%a") { + if let Ok(f) = DateTimeFormatter::try_new(locale_prefs, fieldsets::E::short()) { + fmt = fmt.replace("%a", &f.format(&iso_date).to_string()); + } } - let calendar_type = get_locale_calendar_type(locale); - match calendar_type { - CalendarType::Gregorian => None, - _ => convert_date_to_locale_calendar(year, month, day, &calendar_type) - .map(|(era_year, _, _)| era_year), - } + fmt } #[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); - } - #[test] fn test_calendar_type_detection() { - let thai_locale = icu_locale::locale!("th-TH"); - let persian_locale = icu_locale::locale!("fa-IR"); - let amharic_locale = icu_locale::locale!("am-ET"); - let english_locale = icu_locale::locale!("en-US"); - + use icu_locale::locale; assert_eq!( - get_locale_calendar_type(&thai_locale), + get_locale_calendar_type(&locale!("th-TH")), CalendarType::Buddhist ); assert_eq!( - get_locale_calendar_type(&persian_locale), + get_locale_calendar_type(&locale!("fa-IR")), CalendarType::Persian ); assert_eq!( - get_locale_calendar_type(&amharic_locale), + get_locale_calendar_type(&locale!("am-ET")), CalendarType::Ethiopian ); assert_eq!( - get_locale_calendar_type(&english_locale), + get_locale_calendar_type(&locale!("en-US")), CalendarType::Gregorian ); } - - #[test] - fn test_era_year_conversion() { - let thai_locale = icu_locale::locale!("th-TH"); - let persian_locale = icu_locale::locale!("fa-IR"); - let amharic_locale = icu_locale::locale!("am-ET"); - - // Test Thai Buddhist calendar (2026 + 543 = 2569) - assert_eq!(get_era_year(2026, 6, 15, &thai_locale), Some(2569)); - - // Test Persian calendar (rough approximation) - assert_eq!(get_era_year(2026, 3, 22, &persian_locale), Some(1405)); - assert_eq!(get_era_year(2026, 3, 19, &persian_locale), Some(1404)); - - // Test Ethiopian calendar (rough approximation) - assert_eq!(get_era_year(2026, 9, 12, &amharic_locale), Some(2019)); - assert_eq!(get_era_year(2026, 9, 10, &amharic_locale), Some(2018)); - } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 1034dfdfc54..3445d4c0a92 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -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 févr février janv janvier mercredi samedi sommes +// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado use std::cmp::Ordering; @@ -1866,3 +1866,104 @@ fn test_date_thai_locale_solar_calendar() { let rfc_output = rfc_result.stdout_str(); assert!(rfc_output.starts_with(¤t_year.to_string())); } + +#[cfg(unix)] +fn check_date(locale: &str, date: &str, fmt: &str, expected: &str) { + let actual = new_ucmd!() + .env("LC_ALL", locale) + .arg("-d") + .arg(date) + .arg(fmt) + .succeeds() + .stdout_str() + .trim() + .to_string(); + assert_eq!(actual, expected, "LC_ALL={locale} date -d '{date}' '{fmt}'"); +} + +#[test] +#[cfg(unix)] +fn test_locale_calendar_conversions() { + // Persian (Solar Hijri) - Nowruz is March 20/21 + for (d, e) in [ + ("2026-01-01", "1404-10-11"), + ("2026-01-26", "1404-11-06"), + ("2026-03-20", "1404-12-29"), + ("2026-03-21", "1405-01-01"), + ("2026-03-22", "1405-01-02"), + ("2026-06-15", "1405-03-25"), + ("2026-12-31", "1405-10-10"), + ("2025-03-20", "1403-12-30"), + ("2025-03-21", "1404-01-01"), + ("2024-03-19", "1402-12-29"), + ("2024-03-20", "1403-01-01"), + ("2000-03-20", "1379-01-01"), + ] { + check_date("fa_IR.UTF-8", d, "+%Y-%m-%d", e); + } + + // Thai Buddhist (year + 543, same month/day) + for (d, e) in [ + ("2026-01-01", "2569-01-01"), + ("2026-01-26", "2569-01-26"), + ("2026-06-15", "2569-06-15"), + ("2026-12-31", "2569-12-31"), + ("2025-01-01", "2568-01-01"), + ("2024-02-29", "2567-02-29"), + ("2000-01-01", "2543-01-01"), + ("1970-01-01", "2513-01-01"), + ] { + check_date("th_TH.UTF-8", d, "+%Y-%m-%d", e); + } + + // Ethiopian (13 months, New Year on Sept 11) + for (d, e) in [ + ("2026-01-01", "2018-04-23"), + ("2026-01-26", "2018-05-18"), + ("2026-09-10", "2018-13-05"), + ("2026-09-11", "2019-01-01"), + ("2026-09-12", "2019-01-02"), + ("2026-12-31", "2019-04-22"), + ("2025-09-11", "2018-01-01"), + ("2025-09-10", "2017-13-05"), + ("2000-09-11", "1993-01-01"), + ] { + check_date("am_ET.UTF-8", d, "+%Y-%m-%d", e); + } +} + +#[test] +#[cfg(unix)] +fn test_locale_month_names() { + // %B full month names: Jan, Jun, Dec for each locale + for (loc, jan, jun, dec) in [ + ("fr_FR.UTF-8", "janvier", "juin", "décembre"), + ("de_DE.UTF-8", "Januar", "Juni", "Dezember"), + ("es_ES.UTF-8", "enero", "junio", "diciembre"), + ("it_IT.UTF-8", "gennaio", "giugno", "dicembre"), + ("pt_BR.UTF-8", "janeiro", "junho", "dezembro"), + ("ja_JP.UTF-8", "1月", "6月", "12月"), + ("zh_CN.UTF-8", "一月", "六月", "十二月"), + ] { + check_date(loc, "2026-01-15", "+%B", jan); + check_date(loc, "2026-06-15", "+%B", jun); + check_date(loc, "2026-12-15", "+%B", dec); + } +} + +#[test] +#[cfg(unix)] +fn test_locale_day_names() { + // %A full day names: Mon (26th), Sun (25th), Sat (24th) Jan 2026 + for (loc, mon, sun, sat) in [ + ("fr_FR.UTF-8", "lundi", "dimanche", "samedi"), + ("de_DE.UTF-8", "Montag", "Sonntag", "Samstag"), + ("es_ES.UTF-8", "lunes", "domingo", "sábado"), + ("ja_JP.UTF-8", "月曜日", "日曜日", "土曜日"), + ("zh_CN.UTF-8", "星期一", "星期日", "星期六"), + ] { + check_date(loc, "2026-01-26", "+%A", mon); + check_date(loc, "2026-01-25", "+%A", sun); + check_date(loc, "2026-01-24", "+%A", sat); + } +}