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