1use alloc::format;
30use alloc::string::String;
31use alloc::vec::Vec;
32use datetime::Civil;
33
34const NBSP: char = '\u{A0}';
35const NNBSP: char = '\u{202F}';
36
37pub const DATE_SHORT: u32 = 0;
39pub const DATE_LONG: u32 = 1;
41
42const CUR_PRE: u8 = 0; const CUR_PRE_SP: u8 = 1; const CUR_SUF: u8 = 2; const PCT_SUF: u8 = 0; const PCT_SUF_SP: u8 = 1; const PCT_SUF_NN: u8 = 2; const PCT_PRE: u8 = 3; #[derive(Clone, Copy)]
53struct Data {
54 tag: &'static str,
55 dec: char,
56 grp: char,
57 grp2: u8,
59 min_grp: u8,
61 date: &'static str,
62 date_long: &'static str,
63 time: &'static str,
64 dt_sep: &'static str,
65 months: &'static [&'static str; 12],
66 am_pm: [&'static str; 2],
67 first_day: u8,
69 cur: &'static str,
70 cur_pat: u8,
71 pct: u8,
72 rtl: bool,
73}
74
75const M_EN: [&str; 12] =
76 ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
77const M_DE: [&str; 12] =
78 ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
79const M_FR: [&str; 12] =
80 ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"];
81const M_ES: [&str; 12] =
82 ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"];
83const M_IT: [&str; 12] =
84 ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"];
85const M_NL: [&str; 12] =
86 ["januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"];
87const M_PT: [&str; 12] =
88 ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
89const M_PL: [&str; 12] = [
90 "stycznia", "lutego", "marca", "kwietnia", "maja", "czerwca", "lipca", "sierpnia", "września", "października", "listopada", "grudnia",
91];
92const M_CS: [&str; 12] =
93 ["ledna", "února", "března", "dubna", "května", "června", "července", "srpna", "září", "října", "listopadu", "prosince"];
94const M_SK: [&str; 12] =
95 ["januára", "februára", "marca", "apríla", "mája", "júna", "júla", "augusta", "septembra", "októbra", "novembra", "decembra"];
96const M_SV: [&str; 12] =
97 ["januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", "oktober", "november", "december"];
98const M_DA: [&str; 12] =
99 ["januar", "februar", "marts", "april", "maj", "juni", "juli", "august", "september", "oktober", "november", "december"];
100const M_NB: [&str; 12] =
101 ["januar", "februar", "mars", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"];
102const M_FI: [&str; 12] = [
103 "tammikuuta", "helmikuuta", "maaliskuuta", "huhtikuuta", "toukokuuta", "kesäkuuta", "heinäkuuta", "elokuuta", "syyskuuta",
104 "lokakuuta", "marraskuuta", "joulukuuta",
105];
106const M_RU: [&str; 12] =
107 ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"];
108const M_UK: [&str; 12] =
109 ["січня", "лютого", "березня", "квітня", "травня", "червня", "липня", "серпня", "вересня", "жовтня", "листопада", "грудня"];
110const M_TR: [&str; 12] = ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"];
111const M_EL: [&str; 12] = [
112 "Ιανουαρίου", "Φεβρουαρίου", "Μαρτίου", "Απριλίου", "Μαΐου", "Ιουνίου", "Ιουλίου", "Αυγούστου", "Σεπτεμβρίου", "Οκτωβρίου",
113 "Νοεμβρίου", "Δεκεμβρίου",
114];
115const M_HU: [&str; 12] =
116 ["január", "február", "március", "április", "május", "június", "július", "augusztus", "szeptember", "október", "november", "december"];
117const M_RO: [&str; 12] = [
118 "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie",
119];
120const M_ID: [&str; 12] =
121 ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
122const M_AR: [&str; 12] = ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"];
123const M_HE: [&str; 12] = ["ינואר", "פברואר", "מרץ", "אפריל", "מאי", "יוני", "יולי", "אוגוסט", "ספטמבר", "אוקטובר", "נובמבר", "דצמבר"];
124const M_HI: [&str; 12] = ["जनवरी", "फ़रवरी", "मार्च", "अप्रैल", "मई", "जून", "जुलाई", "अगस्त", "सितंबर", "अक्तूबर", "नवंबर", "दिसंबर"];
125
126const AM_PM: [&str; 2] = ["AM", "PM"];
127
128const DE: Data = Data {
129 tag: "de-DE",
130 dec: ',',
131 grp: '.',
132 grp2: 3,
133 min_grp: 1,
134 date: "dd.MM.yyyy",
135 date_long: "d. MMMM yyyy",
136 time: "HH:mm",
137 dt_sep: ", ",
138 months: &M_DE,
139 am_pm: AM_PM,
140 first_day: 0,
141 cur: "EUR",
142 cur_pat: CUR_SUF,
143 pct: PCT_SUF_SP,
144 rtl: false,
145};
146const EN: Data = Data {
147 tag: "en-US",
148 dec: '.',
149 grp: ',',
150 date: "M/d/yyyy",
151 date_long: "MMMM d, yyyy",
152 time: "h:mm a",
153 months: &M_EN,
154 first_day: 6,
155 cur: "USD",
156 cur_pat: CUR_PRE,
157 pct: PCT_SUF,
158 ..DE
159};
160const EN_001: Data = Data { tag: "en-001", date: "dd/MM/yyyy", date_long: "d MMMM yyyy", time: "HH:mm", first_day: 0, ..EN };
162const EN_GB: Data = Data { tag: "en-GB", cur: "GBP", ..EN_001 };
163const FR: Data = Data {
164 tag: "fr-FR",
165 grp: NNBSP,
166 date: "dd/MM/yyyy",
167 date_long: "d MMMM yyyy",
168 dt_sep: " ",
169 months: &M_FR,
170 pct: PCT_SUF_NN,
171 ..DE
172};
173const ES: Data =
174 Data { tag: "es-ES", min_grp: 2, date: "dd/MM/yyyy", date_long: "d 'de' MMMM 'de' yyyy", time: "H:mm", months: &M_ES, ..DE };
175const IT: Data = Data { tag: "it-IT", date: "dd/MM/yyyy", date_long: "d MMMM yyyy", months: &M_IT, pct: PCT_SUF, ..DE };
176const NL: Data = Data {
177 tag: "nl-NL",
178 date: "dd-MM-yyyy",
179 date_long: "d MMMM yyyy",
180 dt_sep: " ",
181 months: &M_NL,
182 cur_pat: CUR_PRE_SP,
183 pct: PCT_SUF,
184 ..DE
185};
186const PT: Data = Data {
187 tag: "pt-BR",
188 date: "dd/MM/yyyy",
189 date_long: "d 'de' MMMM 'de' yyyy",
190 dt_sep: " ",
191 months: &M_PT,
192 first_day: 6,
193 cur: "BRL",
194 cur_pat: CUR_PRE_SP,
195 pct: PCT_SUF,
196 ..DE
197};
198const PL: Data = Data { tag: "pl-PL", grp: NBSP, min_grp: 2, date_long: "d MMMM yyyy", months: &M_PL, cur: "PLN", pct: PCT_SUF, ..DE };
199const CS: Data =
200 Data { tag: "cs-CZ", grp: NBSP, date: "d. M. yyyy", time: "H:mm", dt_sep: " ", months: &M_CS, cur: "CZK", ..DE };
201const SV: Data = Data {
202 tag: "sv-SE",
203 grp: NBSP,
204 date: "yyyy-MM-dd",
205 date_long: "d MMMM yyyy",
206 dt_sep: " ",
207 months: &M_SV,
208 cur: "SEK",
209 ..DE
210};
211const RU: Data =
212 Data { tag: "ru-RU", grp: NBSP, date_long: "d MMMM yyyy 'г.'", months: &M_RU, cur: "RUB", ..DE };
213
214static TABLE: &[Data] = &[
215 DE,
217 Data { tag: "de-AT", grp: NBSP, cur_pat: CUR_PRE_SP, ..DE },
218 Data { tag: "de-CH", dec: '.', grp: '’', cur: "CHF", cur_pat: CUR_PRE_SP, pct: PCT_SUF, ..DE },
219 Data { tag: "de-LI", dec: '.', grp: '’', cur: "CHF", cur_pat: CUR_PRE_SP, pct: PCT_SUF, ..DE },
220 EN,
221 EN_GB,
222 Data { tag: "en-IE", cur: "EUR", ..EN_001 },
223 Data { tag: "en-AU", time: "h:mm a", cur: "AUD", ..EN_001 },
224 Data { tag: "en-NZ", date: "d/MM/yyyy", time: "h:mm a", cur: "NZD", ..EN_001 },
225 Data { tag: "en-CA", date: "yyyy-MM-dd", time: "h:mm a", first_day: 6, cur: "CAD", ..EN },
226 Data { tag: "en-IN", grp2: 2, date: "dd/MM/yyyy", date_long: "d MMMM yyyy", first_day: 6, cur: "INR", ..EN },
227 EN_001,
228 FR,
229 Data { tag: "fr-BE", ..FR },
230 Data { tag: "fr-CA", grp: NBSP, date: "yyyy-MM-dd", time: "HH 'h' mm", first_day: 6, cur: "CAD", ..FR },
231 Data { tag: "fr-CH", date: "dd.MM.yyyy", cur: "CHF", ..FR },
232 ES,
233 Data { tag: "es-MX", dec: '.', grp: ',', min_grp: 1, first_day: 6, cur: "MXN", cur_pat: CUR_PRE, pct: PCT_SUF, ..ES },
234 Data { tag: "es-US", dec: '.', grp: ',', min_grp: 1, time: "h:mm a", first_day: 6, cur: "USD", cur_pat: CUR_PRE, pct: PCT_SUF, ..ES },
235 IT,
236 Data { tag: "it-CH", dec: '.', grp: '’', date: "dd.MM.yyyy", cur: "CHF", cur_pat: CUR_PRE_SP, ..IT },
237 NL,
238 Data { tag: "nl-BE", date: "d/MM/yyyy", cur_pat: CUR_SUF, ..NL },
239 PT,
240 Data { tag: "pt-PT", grp: NBSP, min_grp: 2, first_day: 0, cur: "EUR", cur_pat: CUR_SUF, ..PT },
241 PL,
242 CS,
243 Data { tag: "sk-SK", months: &M_SK, cur: "EUR", ..CS },
244 SV,
245 Data { tag: "da-DK", date: "dd.MM.yyyy", date_long: "d. MMMM yyyy", time: "HH.mm", months: &M_DA, cur: "DKK", grp: '.', ..SV },
246 Data { tag: "nb-NO", date: "dd.MM.yyyy", date_long: "d. MMMM yyyy", dt_sep: ", ", months: &M_NB, cur: "NOK", ..SV },
247 Data { tag: "fi-FI", date: "d.M.yyyy", date_long: "d. MMMM yyyy", time: "H.mm", months: &M_FI, cur: "EUR", ..SV },
248 RU,
249 Data { tag: "uk-UA", date_long: "d MMMM yyyy 'р.'", months: &M_UK, cur: "UAH", pct: PCT_SUF, ..RU },
250 Data { tag: "tr-TR", date_long: "d MMMM yyyy", dt_sep: " ", months: &M_TR, cur: "TRY", cur_pat: CUR_PRE, pct: PCT_PRE, ..DE },
251 Data { tag: "el-GR", date: "d/M/yyyy", date_long: "d MMMM yyyy", months: &M_EL, pct: PCT_SUF, ..DE },
252 Data {
253 tag: "hu-HU",
254 grp: NBSP,
255 date: "yyyy. MM. dd.",
256 date_long: "yyyy. MMMM d.",
257 time: "H:mm",
258 dt_sep: " ",
259 months: &M_HU,
260 cur: "HUF",
261 pct: PCT_SUF,
262 ..DE
263 },
264 Data { tag: "ro-RO", date_long: "d MMMM yyyy", months: &M_RO, cur: "RON", ..DE },
265 Data { tag: "ja-JP", date: "yyyy/MM/dd", date_long: "yyyy年M月d日", time: "H:mm", dt_sep: " ", first_day: 6, cur: "JPY", ..EN },
266 Data { tag: "zh-CN", date: "yyyy/M/d", date_long: "yyyy年M月d日", time: "HH:mm", dt_sep: " ", first_day: 0, cur: "CNY", ..EN },
267 Data { tag: "zh-TW", date: "yyyy/M/d", date_long: "yyyy年M月d日", time: "HH:mm", dt_sep: " ", cur: "TWD", ..EN },
268 Data { tag: "ko-KR", date: "yyyy. M. d.", date_long: "yyyy년 M월 d일", time: "HH:mm", dt_sep: " ", cur: "KRW", ..EN },
269 Data {
270 tag: "ar-EG",
271 date: "d/M/yyyy",
272 date_long: "d MMMM yyyy",
273 time: "HH:mm",
274 dt_sep: "، ",
275 months: &M_AR,
276 first_day: 5,
277 cur: "EGP",
278 cur_pat: CUR_SUF,
279 rtl: true,
280 ..EN
281 },
282 Data { tag: "ar-SA", date: "d/M/yyyy", date_long: "d MMMM yyyy", time: "HH:mm", dt_sep: "، ", months: &M_AR, cur: "SAR", cur_pat: CUR_SUF, rtl: true, ..EN },
283 Data {
284 tag: "he-IL",
285 date: "d.M.yyyy",
286 date_long: "d בMMMM yyyy",
287 time: "H:mm",
288 months: &M_HE,
289 cur: "ILS",
290 cur_pat: CUR_SUF,
291 rtl: true,
292 ..EN
293 },
294 Data { tag: "hi-IN", grp2: 2, date: "d/M/yyyy", date_long: "d MMMM yyyy", months: &M_HI, am_pm: ["am", "pm"], cur: "INR", ..EN },
295 Data {
296 tag: "id-ID",
297 dec: ',',
298 grp: '.',
299 date: "dd/MM/yyyy",
300 date_long: "d MMMM yyyy",
301 time: "HH.mm",
302 months: &M_ID,
303 cur: "IDR",
304 ..EN
305 },
306];
307
308static CURRENCIES: &[(&str, &str, &str, u8)] = &[
310 ("EUR", "€", "€", 2),
311 ("USD", "$", "US$", 2),
312 ("GBP", "£", "£", 2),
313 ("CHF", "CHF", "CHF", 2),
314 ("JPY", "¥", "¥", 0),
315 ("CNY", "¥", "CN¥", 2),
316 ("CAD", "$", "CA$", 2),
317 ("AUD", "$", "A$", 2),
318 ("NZD", "$", "NZ$", 2),
319 ("INR", "₹", "₹", 2),
320 ("BRL", "R$", "R$", 2),
321 ("MXN", "$", "MX$", 2),
322 ("PLN", "zł", "PLN", 2),
323 ("CZK", "Kč", "CZK", 2),
324 ("SEK", "kr", "SEK", 2),
325 ("DKK", "kr.", "DKK", 2),
326 ("NOK", "kr", "NOK", 2),
327 ("RUB", "₽", "RUB", 2),
328 ("UAH", "₴", "UAH", 2),
329 ("TRY", "₺", "TRY", 2),
330 ("HUF", "Ft", "HUF", 2),
331 ("RON", "RON", "RON", 2),
332 ("KRW", "₩", "₩", 0),
333 ("TWD", "$", "NT$", 2),
334 ("ILS", "₪", "₪", 2),
335 ("EGP", "ج.م.", "EGP", 2),
336 ("SAR", "ر.س.", "SAR", 2),
337 ("IDR", "Rp", "IDR", 2),
338];
339
340#[derive(Clone, Copy)]
342pub struct Locale {
343 d: &'static Data,
344}
345
346impl Locale {
347 pub fn get(tag: &str) -> Locale {
349 Locale { d: lookup(tag) }
350 }
351
352 pub fn tag(&self) -> &'static str {
354 self.d.tag
355 }
356 pub fn decimal(&self) -> char {
358 self.d.dec
359 }
360 pub fn group(&self) -> char {
362 self.d.grp
363 }
364 pub fn first_weekday(&self) -> u32 {
366 self.d.first_day as u32
367 }
368 pub fn rtl(&self) -> bool {
370 self.d.rtl
371 }
372 pub fn currency(&self) -> &'static str {
374 self.d.cur
375 }
376 pub fn month_name(&self, m: u32) -> &'static str {
378 self.d.months[((m.max(1) - 1) % 12) as usize]
379 }
380
381 fn group_digits(&self, digits: &str, out: &mut String) {
383 let n = digits.len();
384 if n < 3 + self.d.min_grp as usize {
385 out.push_str(digits);
386 return;
387 }
388 let mut cuts: Vec<usize> = Vec::new();
389 let mut p = n as isize - 3;
390 while p > 0 {
391 cuts.push(p as usize);
392 p -= self.d.grp2.max(1) as isize;
393 }
394 for (k, c) in digits.chars().enumerate() {
395 if cuts.contains(&k) {
396 out.push(self.d.grp);
397 }
398 out.push(c);
399 }
400 }
401
402 pub fn fmt_int(&self, n: i64) -> String {
404 let mut out = String::new();
405 if n < 0 {
406 out.push('-');
407 }
408 self.group_digits(&format!("{}", n.unsigned_abs()), &mut out);
409 out
410 }
411
412 pub fn fmt_num(&self, v: f64, decimals: u32) -> String {
414 self.num(v, decimals, true)
415 }
416
417 pub(crate) fn num(&self, v: f64, decimals: u32, grouped: bool) -> String {
418 if v.is_nan() {
419 return String::from("NaN");
420 }
421 if v.is_infinite() {
422 return String::from(if v < 0.0 { "-∞" } else { "∞" });
423 }
424 let s = format!("{:.*}", decimals.min(17) as usize, if v < 0.0 { -v } else { v });
425 let (int, frac) = s.split_once('.').unwrap_or((&s, ""));
426 let mut out = String::new();
427 if v < 0.0 && s.bytes().any(|b| (b'1'..=b'9').contains(&b)) {
428 out.push('-');
429 }
430 if grouped {
431 self.group_digits(int, &mut out);
432 } else {
433 out.push_str(int);
434 }
435 if !frac.is_empty() {
436 out.push(self.d.dec);
437 out.push_str(frac);
438 }
439 out
440 }
441
442 pub(crate) fn num_plain(&self, v: f64) -> String {
444 let mut s = self.num(v, 6, false);
445 if s.contains(self.d.dec) {
446 while s.ends_with('0') {
447 s.pop();
448 }
449 if s.ends_with(self.d.dec) {
450 s.pop();
451 }
452 }
453 s
454 }
455
456 pub fn fmt_percent(&self, fraction: f64, decimals: u32) -> String {
458 let n = self.fmt_num(fraction * 100.0, decimals);
459 match self.d.pct {
460 PCT_SUF_SP => format!("{n}{NBSP}%"),
461 PCT_SUF_NN => format!("{n}{NNBSP}%"),
462 PCT_PRE => format!("%{n}"),
463 _ => format!("{n}%"),
464 }
465 }
466
467 pub fn fmt_currency(&self, v: f64, code: &str) -> String {
469 let c = CURRENCIES.iter().find(|c| c.0.eq_ignore_ascii_case(code));
470 let (sym, dec) = match c {
471 Some(c) if c.0 == self.d.cur => (c.1, c.3),
472 Some(c) => (c.2, c.3),
473 None => (code, 2),
474 };
475 let n = self.fmt_num(if v < 0.0 { -v } else { v }, dec as u32);
476 let minus = if v < 0.0 && n.bytes().any(|b| (b'1'..=b'9').contains(&b)) { "-" } else { "" };
477 let letter = sym.chars().last().is_some_and(char::is_alphabetic);
478 match self.d.cur_pat {
479 CUR_SUF => format!("{minus}{n}{NBSP}{sym}"),
480 CUR_PRE_SP => format!("{minus}{sym}{NBSP}{n}"),
481 _ if letter => format!("{minus}{sym}{NBSP}{n}"),
482 _ => format!("{minus}{sym}{n}"),
483 }
484 }
485
486 pub fn fmt_money(&self, v: f64) -> String {
488 self.fmt_currency(v, self.d.cur)
489 }
490
491 pub fn fmt_size(&self, bytes: u64) -> String {
493 let units: [&str; 6] =
494 if self.d.tag.starts_with("fr") { ["o", "ko", "Mo", "Go", "To", "Po"] } else { ["B", "KB", "MB", "GB", "TB", "PB"] };
495 if bytes < 1024 {
496 return format!("{bytes}{NBSP}{}", units[0]);
497 }
498 let mut v = bytes as f64;
499 let mut k = 0;
500 while v >= 1024.0 && k < 5 {
501 v /= 1024.0;
502 k += 1;
503 }
504 format!("{}{NBSP}{}", self.fmt_num(v, if v < 9.95 { 1 } else { 0 }), units[k])
505 }
506
507 pub fn fmt_date(&self, c: &Civil, style: u32) -> String {
509 pattern(self.d, if style == DATE_LONG { self.d.date_long } else { self.d.date }, c)
510 }
511
512 pub fn fmt_time(&self, c: &Civil) -> String {
514 pattern(self.d, self.d.time, c)
515 }
516
517 pub fn fmt_date_time(&self, c: &Civil) -> String {
519 let mut s = self.fmt_date(c, DATE_SHORT);
520 s.push_str(self.d.dt_sep);
521 s.push_str(&self.fmt_time(c));
522 s
523 }
524}
525
526fn pattern(d: &Data, pat: &str, c: &Civil) -> String {
528 let mut out = String::new();
529 let b: Vec<char> = pat.chars().collect();
530 let mut i = 0;
531 while i < b.len() {
532 let ch = b[i];
533 if ch == '\'' {
534 let end = b[i + 1..].iter().position(|&x| x == '\'').map_or(b.len(), |p| i + 1 + p);
535 if end == i + 1 {
536 out.push('\''); }
538 out.extend(&b[i + 1..end]);
539 i = end + 1;
540 continue;
541 }
542 if !ch.is_ascii_alphabetic() {
543 out.push(ch);
544 i += 1;
545 continue;
546 }
547 let mut n = 1;
548 while i + n < b.len() && b[i + n] == ch {
549 n += 1;
550 }
551 i += n;
552 let h12 = match c.hour % 12 {
553 0 => 12,
554 h => h,
555 };
556 match (ch, n) {
557 ('d', 1) => out.push_str(&format!("{}", c.day)),
558 ('d', _) => out.push_str(&format!("{:02}", c.day)),
559 ('M', 1) => out.push_str(&format!("{}", c.month)),
560 ('M', 2) => out.push_str(&format!("{:02}", c.month)),
561 ('M', _) => out.push_str(d.months[((c.month.max(1) - 1) % 12) as usize]),
562 ('y', 2) => out.push_str(&format!("{:02}", c.year.rem_euclid(100))),
563 ('y', _) => out.push_str(&format!("{}", c.year)),
564 ('H', 1) => out.push_str(&format!("{}", c.hour)),
565 ('H', _) => out.push_str(&format!("{:02}", c.hour)),
566 ('h', 1) => out.push_str(&format!("{}", h12)),
567 ('h', _) => out.push_str(&format!("{:02}", h12)),
568 ('m', _) => out.push_str(&format!("{:02}", c.minute)),
569 ('s', _) => out.push_str(&format!("{:02}", c.second)),
570 ('a', _) => out.push_str(d.am_pm[(c.hour >= 12) as usize]),
571 _ => {}
572 }
573 }
574 out
575}
576
577fn lookup(tag: &str) -> &'static Data {
578 let Some(t) = plat::locale_tag(tag) else { return &TABLE[index_of("en-001")] };
579 if let Some(d) = TABLE.iter().find(|d| d.tag.eq_ignore_ascii_case(&t)) {
580 return d;
581 }
582 let mut parts = t.split('-');
583 let mut lang = parts.next().unwrap_or("");
584 let rest: Vec<&str> = parts.collect();
585 let script = rest.iter().find(|p| p.len() == 4).copied();
586 let mut region = rest.iter().find(|p| p.len() == 2 || (p.len() == 3 && p.bytes().all(|b| b.is_ascii_digit()))).copied();
587 lang = match lang {
589 "no" | "nn" => "nb",
590 "iw" => "he",
591 "in" => "id",
592 l => l,
593 };
594 if lang == "zh" {
595 region = match (script, region) {
596 (Some("Hant"), None) | (_, Some("HK" | "MO")) => Some("TW"),
597 (_, r) => r,
598 };
599 }
600 if let Some(r) = region {
601 let want = format!("{lang}-{r}");
602 if let Some(d) = TABLE.iter().find(|d| d.tag.eq_ignore_ascii_case(&want)) {
603 return d;
604 }
605 if lang == "en" {
607 return &TABLE[index_of("en-001")];
608 }
609 }
610 TABLE.iter().find(|d| d.tag.split('-').next() == Some(lang)).unwrap_or(&TABLE[index_of("en-001")])
611}
612
613fn index_of(tag: &str) -> usize {
614 TABLE.iter().position(|d| d.tag == tag).unwrap_or(0)
615}
616
617static CURRENT: rt::sync::SpinLock<Option<(Locale, String)>> = rt::sync::SpinLock::new(None);
620
621pub fn locale() -> Locale {
623 CURRENT.lock().as_ref().map_or(Locale { d: &TABLE[0] }, |c| c.0)
624}
625
626pub fn locale_tag() -> String {
628 CURRENT.lock().as_ref().map_or_else(|| String::from("de-DE"), |c| c.1.clone())
629}
630
631pub fn set_locale(tag: &str) {
633 let t = plat::locale_tag(tag).unwrap_or_else(|| String::from(tag));
634 *CURRENT.lock() = Some((Locale::get(&t), t));
635}
636
637pub fn fmt_int(n: i64) -> String {
639 locale().fmt_int(n)
640}
641pub fn fmt_num(v: f64, decimals: u32) -> String {
643 locale().fmt_num(v, decimals)
644}
645pub fn fmt_percent(fraction: f64, decimals: u32) -> String {
647 locale().fmt_percent(fraction, decimals)
648}
649pub fn fmt_currency(v: f64, code: &str) -> String {
651 locale().fmt_currency(v, code)
652}
653pub fn fmt_money(v: f64) -> String {
655 locale().fmt_money(v)
656}
657pub fn fmt_size(bytes: u64) -> String {
659 locale().fmt_size(bytes)
660}
661pub fn fmt_date(c: &Civil, style: u32) -> String {
663 locale().fmt_date(c, style)
664}
665pub fn fmt_time(c: &Civil) -> String {
667 locale().fmt_time(c)
668}
669pub fn fmt_date_time(c: &Civil) -> String {
671 locale().fmt_date_time(c)
672}
673
674#[derive(Clone, Copy, PartialEq, Eq, Debug)]
678#[non_exhaustive]
679pub enum Plural {
680 Zero,
681 One,
682 Two,
683 Few,
684 Many,
685 Other,
686}
687
688impl Plural {
689 pub fn name(self) -> &'static str {
691 match self {
692 Plural::Zero => "zero",
693 Plural::One => "one",
694 Plural::Two => "two",
695 Plural::Few => "few",
696 Plural::Many => "many",
697 Plural::Other => "other",
698 }
699 }
700}
701
702pub fn plural_int(lang: &str, n: i64) -> Plural {
704 plural(lang, n.unsigned_abs(), 0, 0)
705}
706
707pub fn plural(lang: &str, i: u64, v: u32, f: u64) -> Plural {
710 use Plural::*;
711 let l = lang.split(['-', '_']).next().unwrap_or("").to_ascii_lowercase();
712 let int = v == 0; let (i10, i100) = (i % 10, i % 100);
714 let (f10, f100) = (f % 10, f % 100);
715 match l.as_str() {
716 "ja" | "zh" | "ko" | "vi" | "th" | "id" | "ms" | "lo" | "km" | "my" | "yue" => Other,
717 "fr" | "pt" => {
718 if i <= 1 { One } else { Other }
719 }
720 "hi" | "bn" | "gu" | "kn" | "mr" | "fa" | "am" | "zu" => {
721 if i == 0 || (i == 1 && f == 0) { One } else { Other }
722 }
723 "ru" | "uk" | "be" => {
724 if !int {
725 Other
726 } else if i10 == 1 && i100 != 11 {
727 One
728 } else if (2..=4).contains(&i10) && !(12..=14).contains(&i100) {
729 Few
730 } else {
731 Many
732 }
733 }
734 "pl" => {
735 if !int {
736 Other
737 } else if i == 1 {
738 One
739 } else if (2..=4).contains(&i10) && !(12..=14).contains(&i100) {
740 Few
741 } else {
742 Many
743 }
744 }
745 "cs" | "sk" => {
746 if !int {
747 Many
748 } else if i == 1 {
749 One
750 } else if (2..=4).contains(&i) {
751 Few
752 } else {
753 Other
754 }
755 }
756 "hr" | "sr" | "bs" | "sh" => {
757 if (int && i10 == 1 && i100 != 11) || (f10 == 1 && f100 != 11) {
758 One
759 } else if (int && (2..=4).contains(&i10) && !(12..=14).contains(&i100)) || ((2..=4).contains(&f10) && !(12..=14).contains(&f100)) {
760 Few
761 } else {
762 Other
763 }
764 }
765 "sl" => {
766 if !int {
767 Few
768 } else if i100 == 1 {
769 One
770 } else if i100 == 2 {
771 Two
772 } else if (3..=4).contains(&i100) {
773 Few
774 } else {
775 Other
776 }
777 }
778 "ro" => {
779 if int && i == 1 {
780 One
781 } else if !int || i == 0 || (1..=19).contains(&i100) {
782 Few
783 } else {
784 Other
785 }
786 }
787 "lt" => {
788 if !int {
789 Many
790 } else if i10 == 1 && !(11..=19).contains(&i100) {
791 One
792 } else if i10 >= 2 && !(11..=19).contains(&i100) {
793 Few
794 } else {
795 Other
796 }
797 }
798 "lv" => {
799 if int && (i10 == 0 || (11..=19).contains(&i100)) {
800 Zero
801 } else if int && i10 == 1 && i100 != 11 {
802 One
803 } else {
804 Other
805 }
806 }
807 "ar" => {
808 if !int {
809 Other
810 } else if i == 0 {
811 Zero
812 } else if i == 1 {
813 One
814 } else if i == 2 {
815 Two
816 } else if (3..=10).contains(&i100) {
817 Few
818 } else if (11..=99).contains(&i100) {
819 Many
820 } else {
821 Other
822 }
823 }
824 "he" | "iw" => {
825 if (int && i == 1) || (i == 0 && !int) {
826 One
827 } else if int && i == 2 {
828 Two
829 } else {
830 Other
831 }
832 }
833 _ => {
835 if int && i == 1 { One } else { Other }
836 }
837 }
838}