Skip to main content

ferroui_base/utilities/
culture_info.rs

1//! A minimal counterpart of .NET's `System.Globalization.CultureInfo`: a
2//! BCP-47 language tag plus its Windows LCID where known. It carries no
3//! formatting data; the text subsystem only needs culture identity (font
4//! matching, shaping language, localized font names).
5//!
6//! Date and time conventions are reached through
7//! [`CultureInfo::date_time_format`]; see
8//! [`DateTimeFormatInfo`](super::DateTimeFormatInfo) for the cultures that
9//! have data of their own.
10
11use 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
19/// LCID of the invariant culture.
20const INVARIANT_LCID: i32 = 0x007F;
21
22/// LCID reported for cultures without a Windows LCID (`LOCALE_CUSTOM_UNSPECIFIED`).
23const CUSTOM_UNSPECIFIED_LCID: i32 = 0x1000;
24
25/// Windows language identifiers of the cultures fonts commonly carry localized names for.
26static 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    /// The types of culture lists [`CultureInfo::get_cultures`] retrieves (C#
47    /// `CultureTypes`). The obsolete members of the managed enumeration are
48    /// not ported.
49    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50    pub struct CultureTypes: i32 {
51        /// Cultures that are associated with a language but are not specific
52        /// to a country or region. The invariant culture is listed with them.
53        const NEUTRAL_CULTURES = 0x1;
54        /// Cultures that are specific to a country or region.
55        const SPECIFIC_CULTURES = 0x2;
56        /// The cultures installed in the operating system.
57        const INSTALLED_WIN32_CULTURES = 0x4;
58        /// All cultures.
59        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/// Identifies a culture by its language tag.
69///
70/// Cloning is cheap (one reference count). Equality compares names ignoring
71/// ASCII case.
72#[derive(Clone)]
73pub struct CultureInfo {
74    name: Rc<str>,
75    lcid: i32,
76    /// Date and time conventions attached to this instance; they replace
77    /// the ones the culture data provider has for the name.
78    date_time_format: Option<Rc<DateTimeFormatInfo>>,
79    /// Number conventions attached to this instance.
80    number_format: Option<Rc<NumberFormatInfo>>,
81}
82
83impl CultureInfo {
84    /// The culture-independent (invariant) culture; its name is empty.
85    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    /// The culture used by the current thread. Defaults to the invariant
93    /// culture until the platform layer sets it.
94    pub fn current_culture() -> CultureInfo {
95        CURRENT_CULTURE.with(|culture| culture.borrow().clone())
96    }
97
98    /// Sets the culture used by the current thread.
99    pub fn set_current_culture(culture: CultureInfo) {
100        CURRENT_CULTURE.with(|current| *current.borrow_mut() = culture);
101    }
102
103    /// The UI culture used by the current thread. Defaults to the invariant
104    /// culture until the platform layer sets it.
105    pub fn current_ui_culture() -> CultureInfo {
106        CURRENT_UI_CULTURE.with(|culture| culture.borrow().clone())
107    }
108
109    /// Sets the UI culture used by the current thread.
110    pub fn set_current_ui_culture(culture: CultureInfo) {
111        CURRENT_UI_CULTURE.with(|current| *current.borrow_mut() = culture);
112    }
113
114    /// The culture for a language tag such as `en-US` (C# `GetCultureInfo(string)`).
115    /// An empty name gives the invariant culture.
116    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    /// The culture for an IETF language tag (C# `GetCultureInfoByIetfLanguageTag`),
130    /// or `None` for a name that is not an IETF tag (C# throws
131    /// `CultureNotFoundException`): the old names `zh-CHT` and `zh-CHS`, an
132    /// alternate sort order and the traditional sort of Spanish.
133    pub fn get_culture_info_by_ietf_language_tag(name: &str) -> Option<CultureInfo> {
134        // Disallow old zh-CHT/zh-CHS names
135        if name == "zh-CHT" || name == "zh-CHS" {
136            return None;
137        }
138
139        let ci = Self::get_culture_info(name);
140
141        // Disallow alt sorts and es-es_TS
142        if ci.lcid > 0xffff || ci.lcid == 0x040a {
143            return None;
144        }
145
146        Some(ci)
147    }
148
149    /// The culture for a Windows language identifier (C# `GetCultureInfo(int)`),
150    /// or `None` when the identifier is not known (C# throws `CultureNotFoundException`).
151    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    /// The cultures of the given types (C# `GetCultures`).
163    ///
164    /// The managed runtime lists the cultures of its globalization library.
165    /// The base library has the data of the invariant culture only, so the
166    /// list is the one the registered
167    /// [`ICultureDataProvider`](super::ICultureDataProvider) gives
168    /// ([`get_culture_names`](super::ICultureDataProvider::get_culture_names)),
169    /// preceded by the invariant culture when the neutral cultures are asked
170    /// for. Without a provider the list is the invariant culture alone,
171    /// whatever the types: what the managed runtime returns in its invariant
172    /// globalization mode.
173    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    /// The language tag (C# `Name`); empty for the invariant culture.
187    #[inline]
188    pub fn name(&self) -> &str {
189        &self.name
190    }
191
192    /// The Windows language identifier (C# `LCID`).
193    #[inline]
194    pub fn lcid(&self) -> i32 {
195        self.lcid
196    }
197
198    /// The primary language subtag (C# `TwoLetterISOLanguageName`); `iv` for
199    /// the invariant culture.
200    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    /// The culture this one specializes (C# `Parent`): the tag without its
209    /// last subtag; the parent of a language-only culture is the invariant
210    /// culture. Chinese regional cultures have their script culture as parent
211    /// (`zh-CN` -> `zh-Hans`, `zh-TW` -> `zh-Hant`), as in .NET.
212    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    /// Whether this is the invariant culture.
236    #[inline]
237    pub fn is_invariant(&self) -> bool {
238        self.name.is_empty()
239    }
240
241    /// The date and time conventions of the culture (C# `DateTimeFormat`):
242    /// the ones attached with [`with_date_time_format`](Self::with_date_time_format),
243    /// else the data the registered
244    /// [`ICultureDataProvider`](super::ICultureDataProvider) has for the
245    /// culture or the nearest of its parents, else the invariant conventions.
246    ///
247    /// Unlike .NET, the library itself carries the data of the invariant
248    /// culture only.
249    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    /// A culture with the same identity and the given date and time
257    /// conventions (the C# `new CultureInfo(name) { DateTimeFormat = .. }`).
258    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    /// A culture with the same identity and the given number conventions
263    /// (the C# `new CultureInfo(name) { NumberFormat = .. }`).
264    pub fn with_number_format(&self, number_format: NumberFormatInfo) -> CultureInfo {
265        CultureInfo { number_format: Some(Rc::new(number_format)), ..self.clone() }
266    }
267
268    /// The calendar of the culture (C# `Calendar`); always the Gregorian one.
269    pub fn calendar(&self) -> GregorianCalendar {
270        GregorianCalendar
271    }
272
273    /// The text conventions of the culture (C# `TextInfo`): the data the
274    /// registered [`ICultureDataProvider`](super::ICultureDataProvider) has
275    /// for the culture or the nearest of its parents, else the invariant
276    /// conventions.
277    pub fn text_info(&self) -> Rc<super::TextInfo> {
278        super::TextInfo::for_culture(self)
279    }
280}
281
282impl CultureInfo {
283    /// The number conventions of the culture (C# `NumberFormat`).
284    ///
285    /// Only the invariant conventions are built in: those of other cultures
286    /// come from the provider registered with the locator (see
287    /// [`ICultureDataProvider`](super::ICultureDataProvider)); a culture
288    /// without data gets the conventions of its nearest parent that has
289    /// data and finally the invariant ones.
290    pub fn number_format(&self) -> Rc<super::NumberFormatInfo> {
291        // number format: owned by numeric agent (the lookup of the data of a culture name
292        // belongs in `NumberFormatInfo::for_culture`, through `ICultureDataProvider::get_number_format`).
293        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        // Without a provider: the invariant culture, whatever the types.
352        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        // The invariant culture is listed with the neutral ones.
358        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}