@@ -17,6 +17,7 @@ use jiff_icu::ConvertFrom;
1717use std:: sync:: OnceLock ;
1818
1919use crate :: i18n:: get_locale_from_env;
20+ pub use crate :: time:: NamePadding ;
2021
2122/// Get the locale for time/date formatting from LC_TIME environment variable
2223pub fn get_time_locale ( ) -> & ' static ( Locale , super :: UEncoding ) {
@@ -104,15 +105,26 @@ fn pad_names<const N: usize>(names: [String; N]) -> [String; N] {
104105/// Cached locale name arrays, computed once per process. Each variant is
105106/// `None` when the ICU formatter for that field width cannot be created
106107/// (should only happen for truly broken locale data).
108+ ///
109+ /// Both raw and padded variants are stored: `date` needs raw names (no
110+ /// trailing spaces) while `ls` needs padded names for column alignment.
107111struct CachedLocaleNames {
108- /// `%B` — full month names, padded to uniform display width
112+ /// `%B` — full month names, raw
109113 month_long : Option < [ String ; 12 ] > ,
110- /// `%b` / `%h` — abbreviated month names (trailing dots stripped), padded
114+ /// `%B` — full month names, padded to uniform display width
115+ month_long_padded : Option < [ String ; 12 ] > ,
116+ /// `%b` / `%h` — abbreviated month names (trailing dots stripped), raw
111117 month_abbrev : Option < [ String ; 12 ] > ,
112- /// `%A` — full weekday names, padded
118+ /// `%b` / `%h` — abbreviated month names, padded
119+ month_abbrev_padded : Option < [ String ; 12 ] > ,
120+ /// `%A` — full weekday names, raw
113121 weekday_long : Option < [ String ; 7 ] > ,
114- /// `%a` — abbreviated weekday names, padded
122+ /// `%A` — full weekday names, padded
123+ weekday_long_padded : Option < [ String ; 7 ] > ,
124+ /// `%a` — abbreviated weekday names, raw
115125 weekday_short : Option < [ String ; 7 ] > ,
126+ /// `%a` — abbreviated weekday names, padded
127+ weekday_short_padded : Option < [ String ; 7 ] > ,
116128}
117129
118130/// Return the cached, pre-padded locale names (computed once per process).
@@ -141,40 +153,50 @@ fn get_cached_locale_names() -> &'static CachedLocaleNames {
141153
142154 let month_long = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: M :: long ( ) )
143155 . ok ( )
144- . map ( |f| pad_names ( month_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ) ;
156+ . map ( |f| month_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ;
157+ let month_long_padded = month_long. clone ( ) . map ( pad_names) ;
145158
146159 // ICU's medium format may include trailing periods (e.g., "febr."
147160 // for Hungarian). The standard C/POSIX locale via nl_langinfo
148161 // returns abbreviations WITHOUT trailing periods, so we strip them.
149162 let month_abbrev = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: M :: medium ( ) )
150163 . ok ( )
151164 . map ( |f| {
152- pad_names (
153- month_dates
154- . each_ref ( )
155- . map ( |d| f. format ( d) . to_string ( ) . trim_end_matches ( '.' ) . to_string ( ) ) ,
156- )
165+ month_dates
166+ . each_ref ( )
167+ . map ( |d| f. format ( d) . to_string ( ) . trim_end_matches ( '.' ) . to_string ( ) )
157168 } ) ;
169+ let month_abbrev_padded = month_abbrev. clone ( ) . map ( pad_names) ;
158170
159171 let weekday_long = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: E :: long ( ) )
160172 . ok ( )
161- . map ( |f| pad_names ( weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ) ;
173+ . map ( |f| weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ;
174+ let weekday_long_padded = weekday_long. clone ( ) . map ( pad_names) ;
162175
163176 let weekday_short = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: E :: short ( ) )
164177 . ok ( )
165- . map ( |f| pad_names ( weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ) ;
178+ . map ( |f| weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ;
179+ let weekday_short_padded = weekday_short. clone ( ) . map ( pad_names) ;
166180
167181 CachedLocaleNames {
168182 month_long,
183+ month_long_padded,
169184 month_abbrev,
185+ month_abbrev_padded,
170186 weekday_long,
187+ weekday_long_padded,
171188 weekday_short,
189+ weekday_short_padded,
172190 }
173191 } )
174192}
175193
176- /// Transform a strftime format string to use locale-specific calendar values
177- pub fn localize_format_string ( format : & str , date : JiffDate ) -> String {
194+ /// Transform a strftime format string to use locale-specific calendar values.
195+ ///
196+ /// When `padding` is [`NamePadding::Padded`], month and weekday names are
197+ /// padded to uniform display width (for columnar output like `ls`). When
198+ /// [`NamePadding::Raw`], raw names are used (for `date` and similar utilities).
199+ pub fn localize_format_string ( format : & str , date : JiffDate , padding : NamePadding ) -> String {
178200 const PERCENT_PLACEHOLDER : & str = "\x00 \x00 " ;
179201
180202 let ( locale, _) = get_time_locale ( ) ;
@@ -219,30 +241,51 @@ pub fn localize_format_string(format: &str, date: JiffDate) -> String {
219241 . replace ( "%e" , & format ! ( "{cal_day:2}" ) ) ;
220242 }
221243
222- // Look up the pre-padded locale name from the once-per-process cache.
244+ // Look up locale names from the once-per-process cache.
245+ let pad = matches ! ( padding, NamePadding :: Padded ) ;
223246 let cached = get_cached_locale_names ( ) ;
224247 let month_idx = date. month ( ) as usize - 1 ;
225248 let weekday_idx = date. weekday ( ) . to_monday_zero_offset ( ) as usize ;
226249
227250 if fmt. contains ( "%B" ) {
228- if let Some ( names) = & cached. month_long {
251+ let src = if pad {
252+ & cached. month_long_padded
253+ } else {
254+ & cached. month_long
255+ } ;
256+ if let Some ( names) = src {
229257 fmt = fmt. replace ( "%B" , & names[ month_idx] ) ;
230258 }
231259 }
232260 if fmt. contains ( "%b" ) || fmt. contains ( "%h" ) {
233- if let Some ( names) = & cached. month_abbrev {
261+ let src = if pad {
262+ & cached. month_abbrev_padded
263+ } else {
264+ & cached. month_abbrev
265+ } ;
266+ if let Some ( names) = src {
234267 fmt = fmt
235268 . replace ( "%b" , & names[ month_idx] )
236269 . replace ( "%h" , & names[ month_idx] ) ;
237270 }
238271 }
239272 if fmt. contains ( "%A" ) {
240- if let Some ( names) = & cached. weekday_long {
273+ let src = if pad {
274+ & cached. weekday_long_padded
275+ } else {
276+ & cached. weekday_long
277+ } ;
278+ if let Some ( names) = src {
241279 fmt = fmt. replace ( "%A" , & names[ weekday_idx] ) ;
242280 }
243281 }
244282 if fmt. contains ( "%a" ) {
245- if let Some ( names) = & cached. weekday_short {
283+ let src = if pad {
284+ & cached. weekday_short_padded
285+ } else {
286+ & cached. weekday_short
287+ } ;
288+ if let Some ( names) = src {
246289 fmt = fmt. replace ( "%a" , & names[ weekday_idx] ) ;
247290 }
248291 }
0 commit comments