@@ -17,6 +17,14 @@ use jiff_icu::ConvertFrom;
1717use std:: sync:: OnceLock ;
1818
1919use crate :: i18n:: get_locale_from_env;
20+ /// Controls whether locale name lookups return raw or padded names.
21+ #[ derive( Clone , Copy ) ]
22+ pub enum NamePadding {
23+ /// Raw names with no trailing padding — for `date` and similar utilities.
24+ Raw ,
25+ /// Names padded to uniform display width — for columnar output like `ls`.
26+ Padded ,
27+ }
2028
2129/// Get the locale for time/date formatting from LC_TIME environment variable
2230pub fn get_time_locale ( ) -> & ' static ( Locale , super :: UEncoding ) {
@@ -104,15 +112,26 @@ fn pad_names<const N: usize>(names: [String; N]) -> [String; N] {
104112/// Cached locale name arrays, computed once per process. Each variant is
105113/// `None` when the ICU formatter for that field width cannot be created
106114/// (should only happen for truly broken locale data).
115+ ///
116+ /// Both raw and padded variants are stored: `date` needs raw names (no
117+ /// trailing spaces) while `ls` needs padded names for column alignment.
107118struct CachedLocaleNames {
108- /// `%B` — full month names, padded to uniform display width
119+ /// `%B` — full month names, raw
109120 month_long : Option < [ String ; 12 ] > ,
110- /// `%b` / `%h` — abbreviated month names (trailing dots stripped), padded
121+ /// `%B` — full month names, padded to uniform display width
122+ month_long_padded : Option < [ String ; 12 ] > ,
123+ /// `%b` / `%h` — abbreviated month names (trailing dots stripped), raw
111124 month_abbrev : Option < [ String ; 12 ] > ,
112- /// `%A` — full weekday names, padded
125+ /// `%b` / `%h` — abbreviated month names, padded
126+ month_abbrev_padded : Option < [ String ; 12 ] > ,
127+ /// `%A` — full weekday names, raw
113128 weekday_long : Option < [ String ; 7 ] > ,
114- /// `%a` — abbreviated weekday names, padded
129+ /// `%A` — full weekday names, padded
130+ weekday_long_padded : Option < [ String ; 7 ] > ,
131+ /// `%a` — abbreviated weekday names, raw
115132 weekday_short : Option < [ String ; 7 ] > ,
133+ /// `%a` — abbreviated weekday names, padded
134+ weekday_short_padded : Option < [ String ; 7 ] > ,
116135}
117136
118137/// Return the cached, pre-padded locale names (computed once per process).
@@ -141,40 +160,50 @@ fn get_cached_locale_names() -> &'static CachedLocaleNames {
141160
142161 let month_long = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: M :: long ( ) )
143162 . ok ( )
144- . map ( |f| pad_names ( month_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ) ;
163+ . map ( |f| month_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ;
164+ let month_long_padded = month_long. clone ( ) . map ( pad_names) ;
145165
146166 // ICU's medium format may include trailing periods (e.g., "febr."
147167 // for Hungarian). The standard C/POSIX locale via nl_langinfo
148168 // returns abbreviations WITHOUT trailing periods, so we strip them.
149169 let month_abbrev = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: M :: medium ( ) )
150170 . ok ( )
151171 . 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- )
172+ month_dates
173+ . each_ref ( )
174+ . map ( |d| f. format ( d) . to_string ( ) . trim_end_matches ( '.' ) . to_string ( ) )
157175 } ) ;
176+ let month_abbrev_padded = month_abbrev. clone ( ) . map ( pad_names) ;
158177
159178 let weekday_long = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: E :: long ( ) )
160179 . ok ( )
161- . map ( |f| pad_names ( weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ) ;
180+ . map ( |f| weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ;
181+ let weekday_long_padded = weekday_long. clone ( ) . map ( pad_names) ;
162182
163183 let weekday_short = DateTimeFormatter :: try_new ( locale_prefs, fieldsets:: E :: short ( ) )
164184 . ok ( )
165- . map ( |f| pad_names ( weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ) ;
185+ . map ( |f| weekday_dates. each_ref ( ) . map ( |d| f. format ( d) . to_string ( ) ) ) ;
186+ let weekday_short_padded = weekday_short. clone ( ) . map ( pad_names) ;
166187
167188 CachedLocaleNames {
168189 month_long,
190+ month_long_padded,
169191 month_abbrev,
192+ month_abbrev_padded,
170193 weekday_long,
194+ weekday_long_padded,
171195 weekday_short,
196+ weekday_short_padded,
172197 }
173198 } )
174199}
175200
176- /// Transform a strftime format string to use locale-specific calendar values
177- pub fn localize_format_string ( format : & str , date : JiffDate ) -> String {
201+ /// Transform a strftime format string to use locale-specific calendar values.
202+ ///
203+ /// When `padding` is [`NamePadding::Padded`], month and weekday names are
204+ /// padded to uniform display width (for columnar output like `ls`). When
205+ /// [`NamePadding::Raw`], raw names are used (for `date` and similar utilities).
206+ pub fn localize_format_string ( format : & str , date : JiffDate , padding : NamePadding ) -> String {
178207 const PERCENT_PLACEHOLDER : & str = "\x00 \x00 " ;
179208
180209 let ( locale, _) = get_time_locale ( ) ;
@@ -219,30 +248,51 @@ pub fn localize_format_string(format: &str, date: JiffDate) -> String {
219248 . replace ( "%e" , & format ! ( "{cal_day:2}" ) ) ;
220249 }
221250
222- // Look up the pre-padded locale name from the once-per-process cache.
251+ // Look up locale names from the once-per-process cache.
252+ let pad = matches ! ( padding, NamePadding :: Padded ) ;
223253 let cached = get_cached_locale_names ( ) ;
224254 let month_idx = date. month ( ) as usize - 1 ;
225255 let weekday_idx = date. weekday ( ) . to_monday_zero_offset ( ) as usize ;
226256
227257 if fmt. contains ( "%B" ) {
228- if let Some ( names) = & cached. month_long {
258+ let src = if pad {
259+ & cached. month_long_padded
260+ } else {
261+ & cached. month_long
262+ } ;
263+ if let Some ( names) = src {
229264 fmt = fmt. replace ( "%B" , & names[ month_idx] ) ;
230265 }
231266 }
232267 if fmt. contains ( "%b" ) || fmt. contains ( "%h" ) {
233- if let Some ( names) = & cached. month_abbrev {
268+ let src = if pad {
269+ & cached. month_abbrev_padded
270+ } else {
271+ & cached. month_abbrev
272+ } ;
273+ if let Some ( names) = src {
234274 fmt = fmt
235275 . replace ( "%b" , & names[ month_idx] )
236276 . replace ( "%h" , & names[ month_idx] ) ;
237277 }
238278 }
239279 if fmt. contains ( "%A" ) {
240- if let Some ( names) = & cached. weekday_long {
280+ let src = if pad {
281+ & cached. weekday_long_padded
282+ } else {
283+ & cached. weekday_long
284+ } ;
285+ if let Some ( names) = src {
241286 fmt = fmt. replace ( "%A" , & names[ weekday_idx] ) ;
242287 }
243288 }
244289 if fmt. contains ( "%a" ) {
245- if let Some ( names) = & cached. weekday_short {
290+ let src = if pad {
291+ & cached. weekday_short_padded
292+ } else {
293+ & cached. weekday_short
294+ } ;
295+ if let Some ( names) = src {
246296 fmt = fmt. replace ( "%a" , & names[ weekday_idx] ) ;
247297 }
248298 }
0 commit comments