1use super::{DateTimeFormatInfo, GregorianCalendar, ICultureDataProvider, NumberFormatInfo};
12use crate::{FerroLocator, LocatorExtensions};
13use bitflags::bitflags;
14use std::cell::RefCell;
15use std::fmt;
16use std::hash::{Hash, Hasher};
17use std::rc::Rc;
18
19const INVARIANT_LCID: i32 = 0x007F;
21
22const CUSTOM_UNSPECIFIED_LCID: i32 = 0x1000;
24
25static KNOWN_CULTURES: &[(i32, &str)] = &[
27 (0x0401, "ar-SA"), (0x0402, "bg-BG"), (0x0403, "ca-ES"), (0x0404, "zh-TW"), (0x0405, "cs-CZ"),
28 (0x0406, "da-DK"), (0x0407, "de-DE"), (0x0408, "el-GR"), (0x0409, "en-US"), (0x040A, "es-ES_tradnl"),
29 (0x040B, "fi-FI"), (0x040C, "fr-FR"), (0x040D, "he-IL"), (0x040E, "hu-HU"), (0x040F, "is-IS"),
30 (0x0410, "it-IT"), (0x0411, "ja-JP"), (0x0412, "ko-KR"), (0x0413, "nl-NL"), (0x0414, "nb-NO"),
31 (0x0415, "pl-PL"), (0x0416, "pt-BR"), (0x0418, "ro-RO"), (0x0419, "ru-RU"), (0x041A, "hr-HR"),
32 (0x041B, "sk-SK"), (0x041C, "sq-AL"), (0x041D, "sv-SE"), (0x041E, "th-TH"), (0x041F, "tr-TR"),
33 (0x0420, "ur-PK"), (0x0421, "id-ID"), (0x0422, "uk-UA"), (0x0423, "be-BY"), (0x0424, "sl-SI"),
34 (0x0425, "et-EE"), (0x0426, "lv-LV"), (0x0427, "lt-LT"), (0x0429, "fa-IR"), (0x042A, "vi-VN"),
35 (0x042B, "hy-AM"), (0x042D, "eu-ES"), (0x042F, "mk-MK"), (0x0436, "af-ZA"), (0x0437, "ka-GE"),
36 (0x0439, "hi-IN"), (0x043E, "ms-MY"), (0x043F, "kk-KZ"), (0x0445, "bn-IN"), (0x0446, "pa-IN"),
37 (0x0447, "gu-IN"), (0x0449, "ta-IN"), (0x044A, "te-IN"), (0x044B, "kn-IN"), (0x044C, "ml-IN"),
38 (0x044E, "mr-IN"), (0x0456, "gl-ES"), (0x0804, "zh-CN"), (0x0807, "de-CH"), (0x0809, "en-GB"),
39 (0x080A, "es-MX"), (0x080C, "fr-BE"), (0x0810, "it-CH"), (0x0813, "nl-BE"), (0x0814, "nn-NO"),
40 (0x0816, "pt-PT"), (0x081D, "sv-FI"), (0x0C04, "zh-HK"), (0x0C07, "de-AT"), (0x0C09, "en-AU"),
41 (0x0C0A, "es-ES"), (0x0C0C, "fr-CA"), (0x1004, "zh-SG"), (0x1009, "en-CA"), (0x100C, "fr-CH"),
42 (0x1404, "zh-MO"),
43];
44
45bitflags! {
46 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50 pub struct CultureTypes: i32 {
51 const NEUTRAL_CULTURES = 0x1;
54 const SPECIFIC_CULTURES = 0x2;
56 const INSTALLED_WIN32_CULTURES = 0x4;
58 const ALL_CULTURES = 0x7;
60 }
61}
62
63crate::ferro_thread_local! {
64 static CURRENT_CULTURE: RefCell<CultureInfo> = RefCell::new(CultureInfo::invariant_culture());
65 static CURRENT_UI_CULTURE: RefCell<CultureInfo> = RefCell::new(CultureInfo::invariant_culture());
66}
67
68#[derive(Clone)]
73pub struct CultureInfo {
74 name: Rc<str>,
75 lcid: i32,
76 date_time_format: Option<Rc<DateTimeFormatInfo>>,
79 number_format: Option<Rc<NumberFormatInfo>>,
81}
82
83impl CultureInfo {
84 pub fn invariant_culture() -> CultureInfo {
86 crate::ferro_thread_local! {
87 static INVARIANT: CultureInfo = CultureInfo { name: Rc::from(""), lcid: INVARIANT_LCID, date_time_format: None, number_format: None };
88 }
89 INVARIANT.with(Clone::clone)
90 }
91
92 pub fn current_culture() -> CultureInfo {
95 CURRENT_CULTURE.with(|culture| culture.borrow().clone())
96 }
97
98 pub fn set_current_culture(culture: CultureInfo) {
100 CURRENT_CULTURE.with(|current| *current.borrow_mut() = culture);
101 }
102
103 pub fn current_ui_culture() -> CultureInfo {
106 CURRENT_UI_CULTURE.with(|culture| culture.borrow().clone())
107 }
108
109 pub fn set_current_ui_culture(culture: CultureInfo) {
111 CURRENT_UI_CULTURE.with(|current| *current.borrow_mut() = culture);
112 }
113
114 pub fn get_culture_info(name: &str) -> CultureInfo {
117 if name.is_empty() {
118 return Self::invariant_culture();
119 }
120
121 let lcid = KNOWN_CULTURES
122 .iter()
123 .find(|(_, known)| known.eq_ignore_ascii_case(name))
124 .map_or(CUSTOM_UNSPECIFIED_LCID, |(lcid, _)| *lcid);
125
126 CultureInfo { name: Rc::from(name), lcid, date_time_format: None, number_format: None }
127 }
128
129 pub fn get_culture_info_by_ietf_language_tag(name: &str) -> Option<CultureInfo> {
134 if name == "zh-CHT" || name == "zh-CHS" {
136 return None;
137 }
138
139 let ci = Self::get_culture_info(name);
140
141 if ci.lcid > 0xffff || ci.lcid == 0x040a {
143 return None;
144 }
145
146 Some(ci)
147 }
148
149 pub fn get_culture_info_by_lcid(lcid: i32) -> Option<CultureInfo> {
152 if lcid == INVARIANT_LCID {
153 return Some(Self::invariant_culture());
154 }
155
156 KNOWN_CULTURES
157 .iter()
158 .find(|(known, _)| *known == lcid)
159 .map(|(lcid, name)| CultureInfo { name: Rc::from(*name), lcid: *lcid, date_time_format: None, number_format: None })
160 }
161
162 pub fn get_cultures(types: CultureTypes) -> Vec<CultureInfo> {
174 let Some(provider) = FerroLocator::current().get_service::<dyn ICultureDataProvider>() else {
175 return vec![Self::invariant_culture()];
176 };
177
178 let mut cultures = Vec::new();
179 if types.contains(CultureTypes::NEUTRAL_CULTURES) {
180 cultures.push(Self::invariant_culture());
181 }
182 cultures.extend(provider.get_culture_names(types).iter().map(|name| Self::get_culture_info(name)));
183 cultures
184 }
185
186 #[inline]
188 pub fn name(&self) -> &str {
189 &self.name
190 }
191
192 #[inline]
194 pub fn lcid(&self) -> i32 {
195 self.lcid
196 }
197
198 pub fn two_letter_iso_language_name(&self) -> &str {
201 if self.name.is_empty() {
202 return "iv";
203 }
204 let end = self.name.find(['-', '_']).unwrap_or(self.name.len());
205 &self.name[..end]
206 }
207
208 pub fn parent(&self) -> CultureInfo {
213 let Some(separator) = self.name.rfind(['-', '_']) else {
214 return Self::invariant_culture();
215 };
216
217 let (language, last) = (&self.name[..separator], &self.name[separator + 1..]);
218
219 if language.eq_ignore_ascii_case("zh") {
220 const SIMPLIFIED: [&str; 3] = ["CN", "SG", "MY"];
221 const TRADITIONAL: [&str; 3] = ["TW", "HK", "MO"];
222
223 if SIMPLIFIED.iter().any(|region| region.eq_ignore_ascii_case(last)) {
224 return Self::get_culture_info("zh-Hans");
225 }
226
227 if TRADITIONAL.iter().any(|region| region.eq_ignore_ascii_case(last)) {
228 return Self::get_culture_info("zh-Hant");
229 }
230 }
231
232 Self::get_culture_info(language)
233 }
234
235 #[inline]
237 pub fn is_invariant(&self) -> bool {
238 self.name.is_empty()
239 }
240
241 pub fn date_time_format(&self) -> Rc<DateTimeFormatInfo> {
250 match &self.date_time_format {
251 Some(info) => Rc::clone(info),
252 None => DateTimeFormatInfo::for_culture(self),
253 }
254 }
255
256 pub fn with_date_time_format(&self, date_time_format: DateTimeFormatInfo) -> CultureInfo {
259 CultureInfo { date_time_format: Some(Rc::new(date_time_format)), ..self.clone() }
260 }
261
262 pub fn with_number_format(&self, number_format: NumberFormatInfo) -> CultureInfo {
265 CultureInfo { number_format: Some(Rc::new(number_format)), ..self.clone() }
266 }
267
268 pub fn calendar(&self) -> GregorianCalendar {
270 GregorianCalendar
271 }
272
273 pub fn text_info(&self) -> Rc<super::TextInfo> {
278 super::TextInfo::for_culture(self)
279 }
280}
281
282impl CultureInfo {
283 pub fn number_format(&self) -> Rc<super::NumberFormatInfo> {
291 match &self.number_format {
294 Some(info) => Rc::clone(info),
295 None => super::NumberFormatInfo::for_culture(self),
296 }
297 }
298}
299
300impl PartialEq for CultureInfo {
301 fn eq(&self, other: &Self) -> bool {
302 self.name.eq_ignore_ascii_case(&other.name)
303 }
304}
305
306impl Eq for CultureInfo {}
307
308impl Hash for CultureInfo {
309 fn hash<H: Hasher>(&self, state: &mut H) {
310 for byte in self.name.bytes() {
311 state.write_u8(byte.to_ascii_lowercase());
312 }
313 }
314}
315
316impl fmt::Display for CultureInfo {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 f.write_str(&self.name)
319 }
320}
321
322impl fmt::Debug for CultureInfo {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 write!(f, "CultureInfo({:?})", &*self.name)
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn lookup_by_name_and_lcid() {
334 assert!(CultureInfo::invariant_culture().is_invariant());
335 assert_eq!(CultureInfo::invariant_culture().lcid(), 0x7F);
336 let culture = CultureInfo::get_culture_info("ja-JP");
337 assert_eq!(culture.lcid(), 0x0411);
338 assert_eq!(culture.two_letter_iso_language_name(), "ja");
339 assert_eq!(CultureInfo::get_culture_info_by_lcid(0x0409).unwrap().name(), "en-US");
340 assert!(CultureInfo::get_culture_info_by_lcid(0x7777).is_none());
341 assert_eq!(CultureInfo::get_culture_info("EN-us"), CultureInfo::get_culture_info("en-US"));
342 }
343
344 #[test]
345 fn the_cultures_are_the_ones_of_the_provider() {
346 use crate::utilities::TestCultureDataProvider;
347
348 let _scope = FerroLocator::enter_scope();
349 let names = |types| CultureInfo::get_cultures(types).iter().map(|c| c.name().to_string()).collect::<Vec<_>>();
350
351 assert_eq!(names(CultureTypes::SPECIFIC_CULTURES), [""]);
353 assert_eq!(names(CultureTypes::ALL_CULTURES), [""]);
354
355 TestCultureDataProvider::register();
356 assert_eq!(names(CultureTypes::SPECIFIC_CULTURES), ["en-GB", "en-US"]);
357 assert_eq!(names(CultureTypes::NEUTRAL_CULTURES), ["", "en"]);
359 assert_eq!(names(CultureTypes::ALL_CULTURES), ["", "en", "en-GB", "en-US"]);
360 assert!(names(CultureTypes::INSTALLED_WIN32_CULTURES).is_empty());
361 }
362
363 #[test]
364 fn parent_drops_the_last_subtag() {
365 assert_eq!(CultureInfo::get_culture_info("ja-JP").parent().name(), "ja");
366 assert!(CultureInfo::get_culture_info("ja").parent().is_invariant());
367 assert!(CultureInfo::invariant_culture().parent().is_invariant());
368 assert_eq!(CultureInfo::get_culture_info("zh-Hans-CN").parent().name(), "zh-Hans");
369 assert_eq!(CultureInfo::get_culture_info("zh-CN").parent().name(), "zh-Hans");
370 assert_eq!(CultureInfo::get_culture_info("zh-TW").parent().name(), "zh-Hant");
371 assert_eq!(CultureInfo::get_culture_info("zh-Hant").parent().name(), "zh");
372 }
373}