1use std::collections::HashMap;
8use std::sync::LazyLock;
9
10use indexmap::IndexMap;
11
12use crate::generated::catalog as g;
13
14pub const DEFAULT_LOCALE: &str = g::DEFAULT_LOCALE;
15
16#[derive(serde::Deserialize)]
17#[serde(rename_all = "camelCase")]
18struct LocaleFile {
19 week_start: WeekStart,
20 likely_region: IndexMap<String, String>,
21 rtl: Vec<String>,
22}
23
24#[derive(serde::Deserialize)]
25struct WeekStart {
26 default: u8,
27 regions: IndexMap<String, Vec<String>>,
28}
29
30struct LocaleData {
31 default_week_start: u8,
32 by_region: HashMap<String, u8>,
33 likely: IndexMap<String, String>,
34 rtl: Vec<String>,
35}
36
37static DATA: LazyLock<LocaleData> = LazyLock::new(|| {
38 let file: LocaleFile = serde_json::from_str(g::LOCALE_JSON).expect("locale.json");
39 let mut by_region = HashMap::new();
40 for (day, regions) in &file.week_start.regions {
41 let day: u8 = day.parse().unwrap_or(1);
42 for r in regions {
43 by_region.insert(r.clone(), day);
44 }
45 }
46 LocaleData { default_week_start: file.week_start.default, by_region, likely: file.likely_region, rtl: file.rtl }
47});
48
49pub fn parse_locale(locale: &str) -> (String, Option<String>) {
52 let normalized = locale.replace('_', "-");
53 let tags: Vec<&str> = normalized.split('-').filter(|t| !t.is_empty()).collect();
54 let language = tags.first().map(|t| t.to_lowercase()).unwrap_or_default();
55 let region = tags.iter().skip(1).find(|t| (t.len() == 2 && t.chars().all(|c| c.is_ascii_alphabetic())) || (t.len() == 3 && t.chars().all(|c| c.is_ascii_digit())));
56 (language, region.map(|r| r.to_uppercase()))
57}
58
59pub fn week_start(locale: &str) -> u8 {
61 let (language, region) = parse_locale(locale);
62 let region = region.or_else(|| DATA.likely.get(&language).cloned());
63 region.and_then(|r| DATA.by_region.get(&r).copied()).unwrap_or(DATA.default_week_start)
64}
65
66pub fn text_direction(locale: &str) -> &'static str {
68 let (language, _) = parse_locale(locale);
69 if DATA.rtl.contains(&language) {
70 "rtl"
71 } else {
72 "ltr"
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn week_start_by_region_likely_regions_cldr_exceptions() {
82 for (locale, day) in [("en-US", 0), ("en-GB", 1), ("de-AT", 1), ("en", 0), ("de", 1), ("pt", 0), ("pt-PT", 0), ("ar-EG", 6), ("fa", 6), ("dv-MV", 5), ("zh-Hant-TW", 0), ("xx", 1)] {
83 assert_eq!(week_start(locale), day, "{locale}");
84 }
85 }
86
87 #[test]
88 fn parsing_and_direction() {
89 assert_eq!(parse_locale("zh_Hant_TW"), ("zh".to_string(), Some("TW".to_string())));
90 assert_eq!(parse_locale("es-419"), ("es".to_string(), Some("419".to_string())));
91 assert_eq!(text_direction("ar-EG"), "rtl");
92 assert_eq!(text_direction("he"), "rtl");
93 assert_eq!(text_direction("de-DE"), "ltr");
94 assert_eq!(DEFAULT_LOCALE, "en-US");
95 }
96}