1#[cfg(feature = "datetime")]
4pub mod date;
5#[cfg(feature = "number")]
6pub mod number;
7#[cfg(feature = "number")]
8pub mod unit;
9
10use core::fmt::{self, Display};
11
12use crate::locale::Locale;
13
14pub trait LocalizedDisplay {
34 fn fmt(&self, locale: &Locale, f: &mut fmt::Formatter<'_>) -> fmt::Result;
40
41 fn to_localized_string(&self, locale: &Locale) -> String {
43 struct Adapter<'a, T: LocalizedDisplay + ?Sized>(&'a T, &'a Locale);
44
45 impl<T: LocalizedDisplay + ?Sized> Display for Adapter<'_, T> {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 self.0.fmt(self.1, f)
48 }
49 }
50
51 Adapter(self, locale).to_string()
52 }
53
54 fn localized_fmt<'a>(&'a self, locale: &'a Locale) -> impl Display + 'a
56 where
57 Self: Sized,
58 {
59 struct LocalizedFmt<'a, T: LocalizedDisplay>(&'a T, &'a Locale);
60
61 impl<T: LocalizedDisplay> Display for LocalizedFmt<'_, T> {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 self.0.fmt(self.1, f)
64 }
65 }
66
67 LocalizedFmt(self, locale)
68 }
69}
70
71impl<T: Display> LocalizedDisplay for T {
76 fn fmt(&self, _locale: &Locale, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 Display::fmt(self, f)
78 }
79}
80
81#[cfg(feature = "number")]
84#[doc(hidden)]
85pub struct LocalizedArgument<'a, T> {
86 value: &'a T,
87 locale: &'a Locale,
88}
89
90#[cfg(feature = "number")]
92impl<T: fmt::Debug> fmt::Debug for LocalizedArgument<'_, T> {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 fmt::Debug::fmt(self.value, f)
95 }
96}
97
98#[cfg(feature = "number")]
99impl<'a, T> LocalizedArgument<'a, T> {
100 #[must_use]
102 pub const fn new(value: &'a T, locale: &'a Locale) -> Self {
103 Self { value, locale }
104 }
105}
106
107#[cfg(feature = "number")]
108struct RawArgument<'a, T: LocalizedDisplay + ?Sized>(&'a T, &'a Locale);
109
110#[cfg(feature = "number")]
111impl<T: LocalizedDisplay + ?Sized> Display for RawArgument<'_, T> {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 self.0.fmt(self.1, f)
114 }
115}
116
117#[cfg(feature = "number")]
118fn numeric_argument<T: LocalizedDisplay + ?Sized>(
119 value: &T,
120 locale: &Locale,
121 precision: Option<usize>,
122) -> Option<String> {
123 let numeric = matches!(
124 core::any::type_name::<T>(),
125 "u8" | "u16"
126 | "u32"
127 | "u64"
128 | "u128"
129 | "usize"
130 | "i8"
131 | "i16"
132 | "i32"
133 | "i64"
134 | "i128"
135 | "isize"
136 | "f32"
137 | "f64"
138 );
139 let argument = RawArgument(value, locale);
140 numeric.then(|| precision.map_or_else(|| argument.to_string(), |p| format!("{argument:.p$}")))
141}
142
143#[cfg(feature = "number")]
144impl<T: LocalizedDisplay> Display for LocalizedArgument<'_, T> {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str("\u{2068}")?;
147 if let Some(number) = numeric_argument(self.value, self.locale, f.precision()) {
148 let formatted = number::format_number_text(self.locale, &number);
149 if f.width().is_none() && !f.sign_plus() && !f.sign_aware_zero_pad() {
150 f.write_str(&formatted)?;
153 } else {
154 let digits = formatted
160 .strip_prefix(|minus| matches!(minus, '-' | '\u{2212}'))
161 .unwrap_or(formatted.as_str());
162 f.pad_integral(!number.starts_with('-'), "", digits)?;
163 }
164 } else {
165 self.value.fmt(self.locale, f)?;
166 }
167 f.write_str("\u{2069}")
168 }
169}
170
171#[cfg(feature = "list")]
189#[derive(Debug)]
190pub struct LocalizedList<'a>(pub &'a [&'a str]);
191
192#[cfg(feature = "list")]
193impl LocalizedDisplay for LocalizedList<'_> {
194 fn fmt(&self, locale: &Locale, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 use icu_list::{
196 ListFormatter,
197 options::{ListFormatterOptions, ListLength},
198 };
199
200 let options = ListFormatterOptions::default().with_length(ListLength::Wide);
201 let formatter = ListFormatter::try_new_and(locale.0.clone().into(), options)
202 .unwrap_or_else(|_| {
203 ListFormatter::try_new_and(icu_locale::locale!("en").into(), options)
204 .expect("English list formatter should always be available")
205 });
206
207 let result = formatter.format(self.0.iter().copied());
208 write!(f, "{result}")
209 }
210}
211
212#[cfg(all(test, feature = "number"))]
213mod tests {
214 use super::*;
215 use crate::locale::locales;
216
217 #[test]
218 fn localized_argument_formats_numbers_and_adds_bidi_isolation() {
219 let value = 1234.5_f64;
220 let formatted = format!("{}", LocalizedArgument::new(&value, &locales::DE));
221
222 assert!(formatted.starts_with('\u{2068}'));
223 assert!(formatted.ends_with('\u{2069}'));
224 assert!(formatted.contains("1.234,5"));
225 }
226
227 #[test]
228 fn localized_argument_accepts_non_static_borrowed_text() {
229 let owned = String::from("مرحبا");
230 let borrowed = owned.as_str();
231
232 assert_eq!(
233 LocalizedArgument::new(&borrowed, &locales::AR).to_string(),
234 "\u{2068}مرحبا\u{2069}"
235 );
236 }
237
238 #[test]
239 fn localized_argument_applies_width_and_zero_fill_to_integers() {
240 assert_eq!(
241 format!("{:06}", LocalizedArgument::new(&0_u32, &locales::EN)),
242 "\u{2068}000000\u{2069}"
243 );
244 assert_eq!(
245 format!("{:06}", LocalizedArgument::new(&-42_i32, &locales::EN)),
246 "\u{2068}-00042\u{2069}"
247 );
248 assert_eq!(
249 format!("{:>8}", LocalizedArgument::new(&1234_i64, &locales::EN)),
250 "\u{2068} 1,234\u{2069}"
251 );
252 }
253
254 #[test]
255 fn localized_argument_applies_width_to_string_arguments() {
256 let value = "hi";
257 assert_eq!(
258 format!("{:>8}", LocalizedArgument::new(&value, &locales::EN)),
259 "\u{2068} hi\u{2069}"
260 );
261 }
262
263 #[test]
264 fn localized_argument_applies_precision_and_width_to_floats() {
265 let value = 1234.567_f64;
266 assert_eq!(
267 format!("{:.2}", LocalizedArgument::new(&value, &locales::EN)),
268 "\u{2068}1,234.57\u{2069}"
269 );
270 assert_eq!(
271 format!(
272 "{:>10.2}",
273 LocalizedArgument::new(&1234.5_f64, &locales::EN)
274 ),
275 "\u{2068} 1,234.50\u{2069}"
276 );
277 }
278
279 #[test]
280 fn localized_argument_debug_forwards_to_the_wrapped_value() {
281 #[derive(Debug)]
282 enum Fruit {
283 Apple,
284 }
285
286 assert_eq!(
287 format!("{:?}", LocalizedArgument::new(&Fruit::Apple, &locales::EN)),
288 "Apple"
289 );
290 assert_eq!(
291 format!("{:#?}", LocalizedArgument::new(&Fruit::Apple, &locales::EN)),
292 "Apple"
293 );
294 }
295}