Skip to main content

inset_embedder/
locale.rs

1//! dart:ui `Locale` (`platform_dispatcher.dart`).
2
3use std::fmt;
4
5/// An identifier used to select a user's language and formatting preferences.
6///
7/// This represents a [Unicode Language Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier)
8/// (i.e. without Locale extensions), except variants are not supported.
9///
10/// Locales are canonicalized according to the "preferred value" entries in the
11/// [IANA Language Subtag Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry).
12/// For example, `const Locale('he')` and `const Locale('iw')` are equal and both have the
13/// [`language_code`](Self::language_code) `he`, because `iw` is a deprecated language subtag
14/// that was replaced by the subtag `he`.
15///
16/// See also:
17///
18///  * `Localizations`, which provides localized resources to the widget tree.
19#[derive(Clone, Debug, PartialEq, Eq, Hash)]
20pub struct Locale {
21    /// The primary language subtag for the locale.
22    ///
23    /// This must not be null. It may be 'und', representing 'undefined'.
24    ///
25    /// This is expected to be string registered in the [IANA Language Subtag
26    /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)
27    /// with the type "language". The string specified must match the case of the
28    /// string in the registry.
29    ///
30    /// Language subtags that are deprecated in the registry and have a preferred
31    /// code are changed to their preferred code. For example, `const
32    /// Locale('he')` and `const Locale('iw')` are equal, and both have the
33    /// `language_code` `he`, because `iw` is a deprecated language subtag that was
34    /// replaced by the subtag `he`.
35    ///
36    /// This must be a valid Unicode Language subtag as listed in [Unicode CLDR
37    /// supplemental data](https://github.com/unicode-org/cldr/blob/main/common/validity/language.xml).
38    ///
39    /// See also:
40    ///
41    ///  * [`Locale::from_subtags`], which describes the conventions for creating
42    ///    [`Locale`] objects.
43    pub language_code: String,
44
45    /// The script subtag for the locale.
46    ///
47    /// This may be null, indicating that there is no specified script subtag.
48    ///
49    /// This must be a valid Unicode Language Identifier script subtag as listed
50    /// in [Unicode CLDR supplemental data](https://github.com/unicode-org/cldr/blob/main/common/validity/script.xml).
51    ///
52    /// See also:
53    ///
54    ///  * [`Locale::from_subtags`], which describes the conventions for creating
55    ///    [`Locale`] objects.
56    pub script_code: Option<String>,
57
58    /// The region subtag for the locale.
59    ///
60    /// This may be null, indicating that there is no specified region subtag.
61    ///
62    /// This is expected to be string registered in the [IANA Language Subtag
63    /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)
64    /// with the type "region". The string specified must match the case of the
65    /// string in the registry.
66    ///
67    /// Region subtags that are deprecated in the registry and have a preferred
68    /// code are changed to their preferred code. For example, `const Locale('de',
69    /// 'DE')` and `const Locale('de', 'DD')` are equal, and both have the
70    /// `country_code` `DE`, because `DD` is a deprecated language subtag that was
71    /// replaced by the subtag `DE`.
72    ///
73    /// See also:
74    ///
75    ///  * [`Locale::from_subtags`], which describes the conventions for creating
76    ///    [`Locale`] objects.
77    pub country_code: Option<String>,
78}
79
80impl Locale {
81    /// Creates a new Locale object. The first argument is the primary language subtag,
82    /// the second is the region (also referred to as 'country') subtag.
83    ///
84    /// For example:
85    ///
86    /// ```
87    /// # use inset_embedder::Locale;
88    /// let swiss_french = Locale::new("fr").country_code("CH");
89    /// let canadian_french = Locale::new("fr").country_code("CA");
90    /// ```
91    ///
92    /// The primary language subtag must not be null. The region subtag is optional.
93    /// When there is no region/country subtag, the parameter should be omitted or set
94    /// to `None` instead of an empty string.
95    ///
96    /// The subtag values are _case sensitive_ and must be one of the valid subtags
97    /// according to CLDR supplemental data:
98    /// [language](https://github.com/unicode-org/cldr/blob/main/common/validity/language.xml),
99    /// [region](https://github.com/unicode-org/cldr/blob/main/common/validity/region.xml).
100    /// The primary language subtag must be at least two and at most eight lowercase letters,
101    /// but not four letters. The region subtag must be two uppercase letters or three digits.
102    /// See the [Unicode Language Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier)
103    /// specification.
104    ///
105    /// Validity is not checked by default, but some methods may throw away invalid data.
106    pub fn new(language_code: impl Into<String>) -> Locale {
107        Locale {
108            language_code: String::new(),
109            script_code: None,
110            country_code: None,
111        }
112        .language_code(language_code)
113    }
114
115    /// Creates a new Locale object from its language, script, and region subtags,
116    /// each optional. The language defaults to `und`.
117    ///
118    /// The keyword arguments specify the subtags of the Locale.
119    ///
120    /// The subtag values are _case sensitive_ and must be valid subtags according to
121    /// CLDR supplemental data:
122    /// [language](https://github.com/unicode-org/cldr/blob/main/common/validity/language.xml),
123    /// [script](https://github.com/unicode-org/cldr/blob/main/common/validity/script.xml) and
124    /// [region](https://github.com/unicode-org/cldr/blob/main/common/validity/region.xml) for
125    /// each of [`language_code`](Self::language_code), [`script_code`](Self::script_code) and
126    /// [`country_code`](Self::country_code) respectively.
127    ///
128    /// The [`country_code`](Self::country_code) subtag is optional. When there is no
129    /// country subtag, the parameter should be omitted instead of using an empty string.
130    ///
131    /// Validity is not checked by default, but some methods may throw away invalid data.
132    pub fn from_subtags() -> Locale {
133        Locale::new("und")
134    }
135
136    /// Dart `Locale.fromSubtags(languageCode:)`; a deprecated subtag becomes its
137    /// preferred value.
138    pub fn language_code(mut self, language_code: impl Into<String>) -> Locale {
139        let language_code = language_code.into();
140        debug_assert!(!language_code.is_empty());
141        self.language_code = canonical_language_subtag(&language_code).to_owned();
142        self
143    }
144
145    /// Dart `Locale.fromSubtags(scriptCode:)`.
146    pub fn script_code(mut self, script_code: impl Into<String>) -> Locale {
147        let script_code = script_code.into();
148        debug_assert!(!script_code.is_empty());
149        self.script_code = Some(script_code);
150        self
151    }
152
153    /// Dart `Locale(languageCode, countryCode)` / `Locale.fromSubtags(countryCode:)`; a
154    /// deprecated subtag becomes its preferred value, and `''` is no country at all.
155    pub fn country_code(mut self, country_code: impl Into<String>) -> Locale {
156        let country_code = country_code.into();
157        self.country_code =
158            (!country_code.is_empty()).then(|| canonical_region_subtag(&country_code).to_owned());
159        self
160    }
161
162    /// Returns a syntactically valid Unicode BCP47 Locale Identifier.
163    ///
164    /// Some examples of such identifiers: "en", "es-419", "hi-Deva-IN" and
165    /// "zh-Hans-CN". See <http://www.unicode.org/reports/tr35/> for technical
166    /// details.
167    pub fn to_language_tag(&self) -> String {
168        self.raw_to_string('-')
169    }
170
171    fn raw_to_string(&self, separator: char) -> String {
172        let mut out = self.language_code.clone();
173        for subtag in [&self.script_code, &self.country_code]
174            .into_iter()
175            .flatten()
176        {
177            out.push(separator);
178            out.push_str(subtag);
179        }
180        out
181    }
182}
183
184impl fmt::Display for Locale {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        f.write_str(&self.raw_to_string('_'))
187    }
188}
189
190/// The preferred value of a deprecated language subtag; the subtag itself otherwise.
191///
192/// Mappings generated for language subtag registry as of 2019-02-27.
193fn canonical_language_subtag(code: &str) -> &str {
194    match code {
195        "in" => "id",   // Indonesian; deprecated 1989-01-01
196        "iw" => "he",   // Hebrew; deprecated 1989-01-01
197        "ji" => "yi",   // Yiddish; deprecated 1989-01-01
198        "jw" => "jv",   // Javanese; deprecated 2001-08-13
199        "mo" => "ro",   // Moldavian, Moldovan; deprecated 2008-11-22
200        "aam" => "aas", // Aramanik; deprecated 2015-02-12
201        "adp" => "dz",  // Adap; deprecated 2015-02-12
202        "aue" => "ktz", // ǂKxʼauǁʼein; deprecated 2015-02-12
203        "ayx" => "nun", // Ayi (China); deprecated 2011-08-16
204        "bgm" => "bcg", // Baga Mboteni; deprecated 2016-05-30
205        "bjd" => "drl", // Bandjigali; deprecated 2012-08-12
206        "ccq" => "rki", // Chaungtha; deprecated 2012-08-12
207        "cjr" => "mom", // Chorotega; deprecated 2010-03-11
208        "cka" => "cmr", // Khumi Awa Chin; deprecated 2012-08-12
209        "cmk" => "xch", // Chimakum; deprecated 2010-03-11
210        "coy" => "pij", // Coyaima; deprecated 2016-05-30
211        "cqu" => "quh", // Chilean Quechua; deprecated 2016-05-30
212        "drh" => "khk", // Darkhat; deprecated 2010-03-11
213        "drw" => "prs", // Darwazi; deprecated 2010-03-11
214        "gav" => "dev", // Gabutamon; deprecated 2010-03-11
215        "gfx" => "vaj", // Mangetti Dune ǃXung; deprecated 2015-02-12
216        "ggn" => "gvr", // Eastern Gurung; deprecated 2016-05-30
217        "gti" => "nyc", // Gbati-ri; deprecated 2015-02-12
218        "guv" => "duz", // Gey; deprecated 2016-05-30
219        "hrr" => "jal", // Horuru; deprecated 2012-08-12
220        "ibi" => "opa", // Ibilo; deprecated 2012-08-12
221        "ilw" => "gal", // Talur; deprecated 2013-09-10
222        "jeg" => "oyb", // Jeng; deprecated 2017-02-23
223        "kgc" => "tdf", // Kasseng; deprecated 2016-05-30
224        "kgh" => "kml", // Upper Tanudan Kalinga; deprecated 2012-08-12
225        "koj" => "kwv", // Sara Dunjo; deprecated 2015-02-12
226        "krm" => "bmf", // Krim; deprecated 2017-02-23
227        "ktr" => "dtp", // Kota Marudu Tinagas; deprecated 2016-05-30
228        "kvs" => "gdj", // Kunggara; deprecated 2016-05-30
229        "kwq" => "yam", // Kwak; deprecated 2015-02-12
230        "kxe" => "tvd", // Kakihum; deprecated 2015-02-12
231        "kzj" => "dtp", // Coastal Kadazan; deprecated 2016-05-30
232        "kzt" => "dtp", // Tambunan Dusun; deprecated 2016-05-30
233        "lii" => "raq", // Lingkhim; deprecated 2015-02-12
234        "lmm" => "rmx", // Lamam; deprecated 2014-02-28
235        "meg" => "cir", // Mea; deprecated 2013-09-10
236        "mst" => "mry", // Cataelano Mandaya; deprecated 2010-03-11
237        "mwj" => "vaj", // Maligo; deprecated 2015-02-12
238        "myt" => "mry", // Sangab Mandaya; deprecated 2010-03-11
239        "nad" => "xny", // Nijadali; deprecated 2016-05-30
240        "ncp" => "kdz", // Ndaktup; deprecated 2018-03-08
241        "nnx" => "ngv", // Ngong; deprecated 2015-02-12
242        "nts" => "pij", // Natagaimas; deprecated 2016-05-30
243        "oun" => "vaj", // ǃOǃung; deprecated 2015-02-12
244        "pcr" => "adx", // Panang; deprecated 2013-09-10
245        "pmc" => "huw", // Palumata; deprecated 2016-05-30
246        "pmu" => "phr", // Mirpur Panjabi; deprecated 2015-02-12
247        "ppa" => "bfy", // Pao; deprecated 2016-05-30
248        "ppr" => "lcq", // Piru; deprecated 2013-09-10
249        "pry" => "prt", // Pray 3; deprecated 2016-05-30
250        "puz" => "pub", // Purum Naga; deprecated 2014-02-28
251        "sca" => "hle", // Sansu; deprecated 2012-08-12
252        "skk" => "oyb", // Sok; deprecated 2017-02-23
253        "tdu" => "dtp", // Tempasuk Dusun; deprecated 2016-05-30
254        "thc" => "tpo", // Tai Hang Tong; deprecated 2016-05-30
255        "thx" => "oyb", // The; deprecated 2015-02-12
256        "tie" => "ras", // Tingal; deprecated 2011-08-16
257        "tkk" => "twm", // Takpa; deprecated 2011-08-16
258        "tlw" => "weo", // South Wemale; deprecated 2012-08-12
259        "tmp" => "tyj", // Tai Mène; deprecated 2016-05-30
260        "tne" => "kak", // Tinoc Kallahan; deprecated 2016-05-30
261        "tnf" => "prs", // Tangshewi; deprecated 2010-03-11
262        "tsf" => "taj", // Southwestern Tamang; deprecated 2015-02-12
263        "uok" => "ema", // Uokha; deprecated 2015-02-12
264        "xba" => "cax", // Kamba (Brazil); deprecated 2016-05-30
265        "xia" => "acn", // Xiandao; deprecated 2013-09-10
266        "xkh" => "waw", // Karahawyana; deprecated 2016-05-30
267        "xsj" => "suj", // Subi; deprecated 2015-02-12
268        "ybd" => "rki", // Yangbye; deprecated 2012-08-12
269        "yma" => "lrr", // Yamphe; deprecated 2012-08-12
270        "ymt" => "mtm", // Mator-Taygi-Karagas; deprecated 2015-02-12
271        "yos" => "zom", // Yos; deprecated 2013-09-10
272        "yuu" => "yug", // Yugh; deprecated 2014-02-28
273        _ => code,
274    }
275}
276
277/// The preferred value of a deprecated region subtag; the subtag itself otherwise.
278///
279/// Mappings generated for language subtag registry as of 2019-02-27.
280fn canonical_region_subtag(code: &str) -> &str {
281    match code {
282        "BU" => "MM", // Burma; deprecated 1989-12-05
283        "DD" => "DE", // German Democratic Republic; deprecated 1990-10-30
284        "FX" => "FR", // Metropolitan France; deprecated 1997-07-14
285        "TP" => "TL", // East Timor; deprecated 2002-05-20
286        "YD" => "YE", // Democratic Yemen; deprecated 1990-08-14
287        "ZR" => "CD", // Zaire; deprecated 1997-07-14
288        _ => code,
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use std::collections::HashSet;
295
296    use super::*;
297
298    #[test]
299    fn deprecated_subtags_canonicalize_to_their_preferred_values() {
300        let hebrew = Locale::new("iw");
301        assert_eq!(hebrew.language_code, "he");
302        assert_eq!(hebrew, Locale::new("he"));
303        let east_germany = Locale::new("de").country_code("DD");
304        assert_eq!(east_germany.country_code.as_deref(), Some("DE"));
305        assert_eq!(east_germany, Locale::new("de").country_code("DE"));
306    }
307
308    #[test]
309    fn formatting_joins_the_subtags_with_the_separator() {
310        let locale = Locale::from_subtags()
311            .language_code("zh")
312            .script_code("Hans")
313            .country_code("CN");
314        assert_eq!(locale.to_string(), "zh_Hans_CN");
315        assert_eq!(locale.to_language_tag(), "zh-Hans-CN");
316        assert_eq!(Locale::new("en").to_string(), "en");
317        assert_eq!(Locale::from_subtags().to_string(), "und");
318    }
319
320    #[test]
321    fn a_locale_equals_and_hashes_by_its_canonical_subtags() {
322        let mut set = HashSet::new();
323        set.insert(Locale::new("fr").country_code("CH"));
324        assert!(set.contains(&Locale::new("fr").country_code("CH")));
325        assert!(!set.contains(&Locale::new("fr")));
326        assert_eq!(Locale::new("fr").country_code(""), Locale::new("fr"));
327        assert_ne!(
328            Locale::new("sr").script_code("Latn"),
329            Locale::new("sr").script_code("Cyrl")
330        );
331    }
332}