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}