1 // This file is part of ICU4X. For terms of use, please see the file
2 // called LICENSE at the top level of the ICU4X source tree
3 // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
5 use crate::ordering::SubtagOrderingResult;
7 parse_locale, parse_locale_with_single_variant_single_keyword_unicode_keyword_extension,
8 ParserError, ParserMode, SubtagIterator,
10 use crate::{extensions, subtags, LanguageIdentifier};
11 use alloc::string::String;
12 use core::cmp::Ordering;
13 use core::str::FromStr;
14 use tinystr::TinyAsciiStr;
15 use writeable::Writeable;
17 /// A core struct representing a [`Unicode Locale Identifier`].
19 /// A locale is made of two parts:
20 /// * Unicode Language Identifier
21 /// * A set of Unicode Extensions
23 /// [`Locale`] exposes all of the same fields and methods as [`LanguageIdentifier`], and
24 /// on top of that is able to parse, manipulate and serialize unicode extension fields.
31 /// extensions_unicode_key as key, extensions_unicode_value as value,
32 /// locale, subtags_language as language, subtags_region as region,
35 /// let loc = locale!("en-US-u-ca-buddhist");
37 /// assert_eq!(loc.id.language, language!("en"));
38 /// assert_eq!(loc.id.script, None);
39 /// assert_eq!(loc.id.region, Some(region!("US")));
40 /// assert_eq!(loc.id.variants.len(), 0);
42 /// loc.extensions.unicode.keywords.get(&key!("ca")),
43 /// Some(&value!("buddhist"))
49 /// Unicode recognizes three levels of standard conformance for a locale:
51 /// * *well-formed* - syntactically correct
52 /// * *valid* - well-formed and only uses registered language subtags, extensions, keywords, types...
53 /// * *canonical* - valid and no deprecated codes or structure.
55 /// At the moment parsing normalizes a well-formed locale identifier converting
56 /// `_` separators to `-` and adjusting casing to conform to the Unicode standard.
58 /// Any bogus subtags will cause the parsing to fail with an error.
59 /// No subtag validation or canonicalization is performed.
64 /// use icu::locid::{subtags::*, Locale};
66 /// let loc: Locale = "eN_latn_Us-Valencia_u-hC-H12"
68 /// .expect("Failed to parse.");
70 /// assert_eq!(loc.id.language, "en".parse::<Language>().unwrap());
71 /// assert_eq!(loc.id.script, "Latn".parse::<Script>().ok());
72 /// assert_eq!(loc.id.region, "US".parse::<Region>().ok());
74 /// loc.id.variants.get(0),
75 /// "valencia".parse::<Variant>().ok().as_ref()
78 /// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/tr35.html#Unicode_locale_identifier
79 #[derive(Default, PartialEq, Eq, Clone, Hash)]
80 #[allow(clippy::exhaustive_structs)] // This struct is stable (and invoked by a macro)
82 /// The basic language/script/region components in the locale identifier along with any variants.
83 pub id: LanguageIdentifier,
84 /// Any extensions present in the locale identifier.
85 pub extensions: extensions::Extensions,
90 assert_eq!(core::mem::size_of::<subtags::Language>(), 3);
91 assert_eq!(core::mem::size_of::<subtags::Script>(), 4);
92 assert_eq!(core::mem::size_of::<subtags::Region>(), 3);
93 assert_eq!(core::mem::size_of::<subtags::Variant>(), 8);
94 assert_eq!(core::mem::size_of::<subtags::Variants>(), 16);
95 assert_eq!(core::mem::size_of::<LanguageIdentifier>(), 32);
97 assert_eq!(core::mem::size_of::<extensions::transform::Transform>(), 56);
98 assert_eq!(core::mem::size_of::<Option<LanguageIdentifier>>(), 32);
99 assert_eq!(core::mem::size_of::<extensions::transform::Fields>(), 24);
101 assert_eq!(core::mem::size_of::<extensions::unicode::Attributes>(), 24);
102 assert_eq!(core::mem::size_of::<extensions::unicode::Keywords>(), 24);
103 assert_eq!(core::mem::size_of::<Vec<extensions::other::Other>>(), 24);
104 assert_eq!(core::mem::size_of::<extensions::private::Private>(), 24);
105 assert_eq!(core::mem::size_of::<extensions::Extensions>(), 152);
107 assert_eq!(core::mem::size_of::<Locale>(), 184);
111 /// A constructor which takes a utf8 slice, parses it and
112 /// produces a well-formed [`Locale`].
117 /// use icu::locid::Locale;
119 /// Locale::try_from_bytes(b"en-US-u-hc-h12").unwrap();
121 pub fn try_from_bytes(v: &[u8]) -> Result<Self, ParserError> {
125 /// The default undefined locale "und". Same as [`default()`](Default::default()).
130 /// use icu::locid::Locale;
132 /// assert_eq!(Locale::default(), Locale::UND);
134 pub const UND: Self = Self {
135 id: LanguageIdentifier::UND,
136 extensions: extensions::Extensions::new(),
139 /// This is a best-effort operation that performs all available levels of canonicalization.
141 /// At the moment the operation will normalize casing and the separator, but in the future
142 /// it may also validate and update from deprecated subtags to canonical ones.
147 /// use icu::locid::Locale;
150 /// Locale::canonicalize("pL_latn_pl-U-HC-H12").as_deref(),
151 /// Ok("pl-Latn-PL-u-hc-h12")
154 pub fn canonicalize<S: AsRef<[u8]>>(input: S) -> Result<String, ParserError> {
155 let locale = Self::try_from_bytes(input.as_ref())?;
156 Ok(locale.write_to_string().into_owned())
159 /// Compare this [`Locale`] with BCP-47 bytes.
161 /// The return value is equivalent to what would happen if you first converted this
162 /// [`Locale`] to a BCP-47 string and then performed a byte comparison.
164 /// This function is case-sensitive and results in a *total order*, so it is appropriate for
165 /// binary search. The only argument producing [`Ordering::Equal`] is `self.to_string()`.
170 /// use icu::locid::Locale;
171 /// use std::cmp::Ordering;
173 /// let bcp47_strings: &[&str] = &[
178 /// "und-u-ca-hebrew",
179 /// "und-u-ca-japanese",
183 /// for ab in bcp47_strings.windows(2) {
186 /// assert!(a.cmp(b) == Ordering::Less);
187 /// let a_loc = a.parse::<Locale>().unwrap();
188 /// assert!(a_loc.strict_cmp(a.as_bytes()) == Ordering::Equal);
189 /// assert!(a_loc.strict_cmp(b.as_bytes()) == Ordering::Less);
192 pub fn strict_cmp(&self, other: &[u8]) -> Ordering {
193 self.strict_cmp_iter(other.split(|b| *b == b'-')).end()
196 /// Compare this [`Locale`] with an iterator of BCP-47 subtags.
198 /// This function has the same equality semantics as [`Locale::strict_cmp`]. It is intended as
199 /// a more modular version that allows multiple subtag iterators to be chained together.
201 /// For an additional example, see [`SubtagOrderingResult`].
206 /// use icu::locid::locale;
207 /// use std::cmp::Ordering;
209 /// let subtags: &[&[u8]] =
210 /// &[b"ca", b"ES", b"valencia", b"u", b"ca", b"hebrew"];
212 /// let loc = locale!("ca-ES-valencia-u-ca-hebrew");
215 /// loc.strict_cmp_iter(subtags.iter().copied()).end()
218 /// let loc = locale!("ca-ES-valencia");
221 /// loc.strict_cmp_iter(subtags.iter().copied()).end()
224 /// let loc = locale!("ca-ES-valencia-u-nu-arab");
226 /// Ordering::Greater,
227 /// loc.strict_cmp_iter(subtags.iter().copied()).end()
230 pub fn strict_cmp_iter<'l, I>(&self, mut subtags: I) -> SubtagOrderingResult<I>
232 I: Iterator<Item = &'l [u8]>,
234 let r = self.for_each_subtag_str(&mut |subtag| {
235 if let Some(other) = subtags.next() {
236 match subtag.as_bytes().cmp(other) {
237 Ordering::Equal => Ok(()),
238 not_equal => Err(not_equal),
241 Err(Ordering::Greater)
245 Ok(_) => SubtagOrderingResult::Subtags(subtags),
246 Err(o) => SubtagOrderingResult::Ordering(o),
250 /// Compare this `Locale` with a potentially unnormalized BCP-47 string.
252 /// The return value is equivalent to what would happen if you first parsed the
253 /// BCP-47 string to a `Locale` and then performed a structucal comparison.
258 /// use icu::locid::Locale;
259 /// use std::cmp::Ordering;
261 /// let bcp47_strings: &[&str] = &[
266 /// "uNd-u-CA-Japanese",
270 /// for a in bcp47_strings {
271 /// assert!(a.parse::<Locale>().unwrap().normalizing_eq(a));
274 pub fn normalizing_eq(&self, other: &str) -> bool {
275 macro_rules! subtag_matches {
276 ($T:ty, $iter:ident, $expected:expr) => {
279 .map(|b| <$T>::try_from_bytes(b) == Ok($expected))
284 let mut iter = SubtagIterator::new(other.as_bytes());
285 if !subtag_matches!(subtags::Language, iter, self.id.language) {
288 if let Some(ref script) = self.id.script {
289 if !subtag_matches!(subtags::Script, iter, *script) {
293 if let Some(ref region) = self.id.region {
294 if !subtag_matches!(subtags::Region, iter, *region) {
298 for variant in self.id.variants.iter() {
299 if !subtag_matches!(subtags::Variant, iter, *variant) {
303 if !self.extensions.is_empty() {
304 match extensions::Extensions::try_from_iter(&mut iter) {
306 if self.extensions != exts {
315 iter.next().is_none()
319 #[allow(clippy::type_complexity)]
320 pub const fn try_from_bytes_with_single_variant_single_keyword_unicode_extension(
325 Option<subtags::Script>,
326 Option<subtags::Region>,
327 Option<subtags::Variant>,
328 Option<(extensions::unicode::Key, Option<TinyAsciiStr<8>>)>,
332 parse_locale_with_single_variant_single_keyword_unicode_keyword_extension(
338 pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
340 F: FnMut(&str) -> Result<(), E>,
342 self.id.for_each_subtag_str(f)?;
343 self.extensions.for_each_subtag_str(f)?;
348 impl FromStr for Locale {
349 type Err = ParserError;
351 fn from_str(source: &str) -> Result<Self, Self::Err> {
352 Self::try_from_bytes(source.as_bytes())
356 impl From<LanguageIdentifier> for Locale {
357 fn from(id: LanguageIdentifier) -> Self {
360 extensions: extensions::Extensions::default(),
365 impl From<Locale> for LanguageIdentifier {
366 fn from(loc: Locale) -> Self {
371 impl AsRef<LanguageIdentifier> for Locale {
372 fn as_ref(&self) -> &LanguageIdentifier {
377 impl AsMut<LanguageIdentifier> for Locale {
378 fn as_mut(&mut self) -> &mut LanguageIdentifier {
383 impl core::fmt::Debug for Locale {
384 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
385 writeable::Writeable::write_to(self, f)
389 impl_writeable_for_each_subtag_str_no_test!(Locale, selff, selff.extensions.is_empty() => selff.id.write_to_string());
392 fn test_writeable() {
393 use writeable::assert_writeable_eq;
394 assert_writeable_eq!(Locale::UND, "und");
395 assert_writeable_eq!("und-001".parse::<Locale>().unwrap(), "und-001");
396 assert_writeable_eq!("und-Mymr".parse::<Locale>().unwrap(), "und-Mymr");
397 assert_writeable_eq!("my-Mymr-MM".parse::<Locale>().unwrap(), "my-Mymr-MM");
398 assert_writeable_eq!(
399 "my-Mymr-MM-posix".parse::<Locale>().unwrap(),
402 assert_writeable_eq!(
403 "zh-macos-posix".parse::<Locale>().unwrap(),
406 assert_writeable_eq!(
407 "my-t-my-d0-zawgyi".parse::<Locale>().unwrap(),
410 assert_writeable_eq!(
411 "ar-SA-u-ca-islamic-civil".parse::<Locale>().unwrap(),
412 "ar-SA-u-ca-islamic-civil",
414 assert_writeable_eq!(
415 "en-001-x-foo-bar".parse::<Locale>().unwrap(),
418 assert_writeable_eq!("und-t-m0-true".parse::<Locale>().unwrap(), "und-t-m0-true",);
424 /// use icu::locid::Locale;
425 /// use icu::locid::{locale, subtags_language as language};
427 /// assert_eq!(Locale::from(language!("en")), locale!("en"));
429 impl From<subtags::Language> for Locale {
430 fn from(language: subtags::Language) -> Self {
441 /// use icu::locid::Locale;
442 /// use icu::locid::{locale, subtags_script as script};
444 /// assert_eq!(Locale::from(Some(script!("latn"))), locale!("und-Latn"));
446 impl From<Option<subtags::Script>> for Locale {
447 fn from(script: Option<subtags::Script>) -> Self {
458 /// use icu::locid::Locale;
459 /// use icu::locid::{locale, subtags_region as region};
461 /// assert_eq!(Locale::from(Some(region!("US"))), locale!("und-US"));
463 impl From<Option<subtags::Region>> for Locale {
464 fn from(region: Option<subtags::Region>) -> Self {
475 /// use icu::locid::Locale;
476 /// use icu::locid::{
477 /// locale, subtags_language as language, subtags_region as region,
478 /// subtags_script as script,
484 /// Some(script!("Latn")),
485 /// Some(region!("US"))
487 /// locale!("en-Latn-US")
493 Option<subtags::Script>,
494 Option<subtags::Region>,
500 Option<subtags::Script>,
501 Option<subtags::Region>,