Skip to main content

icu_locale_core/
locale.rs

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 ).
4
5use crate::parser::*;
6use crate::subtags::Subtag;
7use crate::{LanguageIdentifier, extensions, subtags};
8#[cfg(feature = "alloc")]
9use alloc::borrow::Cow;
10use core::cmp::Ordering;
11#[cfg(feature = "alloc")]
12use core::str::FromStr;
13
14/// A core struct representing a [`Unicode Locale Identifier`].
15///
16/// A locale is made of two parts:
17///  * Unicode Language Identifier
18///  * A set of Unicode Extensions
19///
20/// [`Locale`] exposes all of the same fields and methods as [`LanguageIdentifier`], and
21/// on top of that is able to parse, manipulate and serialize unicode extension fields.
22///
23/// # Ordering
24///
25/// This type deliberately does not implement `Ord` or `PartialOrd` because there are
26/// multiple possible orderings. Depending on your use case, two orderings are available:
27///
28/// 1. A string ordering, suitable for stable serialization: [`Locale::strict_cmp`]
29/// 2. A struct ordering, suitable for use with a `BTreeSet`: [`Locale::total_cmp`]
30///
31/// See issue: <https://github.com/unicode-org/icu4x/issues/1215>
32///
33/// # Parsing
34///
35/// Unicode recognizes three levels of standard conformance for a locale:
36///
37///  * *well-formed* - syntactically correct
38///  * *valid* - well-formed and only uses registered language subtags, extensions, keywords, types...
39///  * *canonical* - valid and no deprecated codes or structure.
40///
41/// Any syntactically invalid subtags will cause the parsing to fail with an error.
42///
43/// This operation normalizes syntax to be well-formed. No legacy subtag replacements is performed.
44/// For validation and canonicalization, see `LocaleCanonicalizer`.
45///
46/// ICU4X's Locale parsing does not allow for non-BCP-47-compatible locales [allowed by UTS 35 for backwards compatability][tr35-bcp].
47/// Furthermore, it currently does not allow for language tags to have more than three characters.
48///
49/// # Serde
50///
51/// This type implements `serde::Serialize` and `serde::Deserialize` if the
52/// `"serde"` Cargo feature is enabled on the crate.
53///
54/// The value will be serialized as a string and parsed when deserialized.
55/// For tips on efficient storage and retrieval of locales, see [`crate::zerovec`].
56///
57/// # Examples
58///
59/// Simple example:
60///
61/// ```
62/// use icu::locale::{
63///     extensions::unicode::{key, value},
64///     locale,
65///     subtags::{language, region},
66/// };
67///
68/// let loc = locale!("en-US-u-ca-buddhist");
69///
70/// assert_eq!(loc.id.language, language!("en"));
71/// assert_eq!(loc.id.script, None);
72/// assert_eq!(loc.id.region, Some(region!("US")));
73/// assert_eq!(loc.id.variants.len(), 0);
74/// assert_eq!(
75///     loc.extensions.unicode.keywords.get(&key!("ca")),
76///     Some(&value!("buddhist"))
77/// );
78/// ```
79///
80/// More complex example:
81///
82/// ```
83/// use icu::locale::{Locale, subtags::*};
84///
85/// let loc: Locale = "eN-latn-Us-Valencia-u-hC-H12"
86///     .parse()
87///     .expect("Failed to parse.");
88///
89/// assert_eq!(loc.id.language, "en".parse::<Language>().unwrap());
90/// assert_eq!(loc.id.script, "Latn".parse::<Script>().ok());
91/// assert_eq!(loc.id.region, "US".parse::<Region>().ok());
92/// assert_eq!(
93///     loc.id.variants.first(),
94///     "valencia".parse::<Variant>().ok().as_ref()
95/// );
96/// ```
97///
98/// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/tr35.html#Unicode_locale_identifier
99/// [tr35-bcp]: https://unicode.org/reports/tr35/#BCP_47_Conformance
100#[derive(#[automatically_derived]
#[allow(clippy::exhaustive_structs)]
impl ::core::cmp::PartialEq for Locale {
    #[inline]
    fn eq(&self, other: &Locale) -> bool {
        self.id == other.id && self.extensions == other.extensions
    }
}PartialEq, #[automatically_derived]
#[allow(clippy::exhaustive_structs)]
impl ::core::cmp::Eq for Locale {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<LanguageIdentifier>;
        let _: ::core::cmp::AssertParamIsEq<extensions::Extensions>;
    }
}Eq, #[automatically_derived]
#[allow(clippy::exhaustive_structs)]
impl ::core::clone::Clone for Locale {
    #[inline]
    fn clone(&self) -> Locale {
        Locale {
            id: ::core::clone::Clone::clone(&self.id),
            extensions: ::core::clone::Clone::clone(&self.extensions),
        }
    }
}Clone, #[automatically_derived]
#[allow(clippy::exhaustive_structs)]
impl ::core::hash::Hash for Locale {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&self.id, state);
        ::core::hash::Hash::hash(&self.extensions, state)
    }
}Hash)] // no Ord or PartialOrd: see docs
101#[allow(clippy::exhaustive_structs)] // This struct is stable (and invoked by a macro)
102pub struct Locale {
103    /// The basic language/script/region components in the locale identifier along with any variants.
104    pub id: LanguageIdentifier,
105    /// Any extensions present in the locale identifier.
106    pub extensions: extensions::Extensions,
107}
108
109#[test]
110// Expected sizes are based on a 64-bit architecture
111#[cfg(target_pointer_width = "64")]
112fn test_sizes() {
113    assert_eq!(size_of::<subtags::Language>(), 3);
114    assert_eq!(size_of::<subtags::Script>(), 4);
115    assert_eq!(size_of::<subtags::Region>(), 3);
116    assert_eq!(size_of::<subtags::Variant>(), 8);
117    assert_eq!(size_of::<subtags::Variants>(), 16);
118    assert_eq!(size_of::<LanguageIdentifier>(), 32);
119
120    assert_eq!(size_of::<extensions::transform::Transform>(), 56);
121    assert_eq!(size_of::<Option<LanguageIdentifier>>(), 32);
122    assert_eq!(size_of::<extensions::transform::Fields>(), 24);
123
124    assert_eq!(size_of::<extensions::unicode::Attributes>(), 16);
125    assert_eq!(size_of::<extensions::unicode::Keywords>(), 24);
126    assert_eq!(size_of::<Vec<extensions::other::Other>>(), 24);
127    assert_eq!(size_of::<extensions::private::Private>(), 16);
128    assert_eq!(size_of::<extensions::Extensions>(), 136);
129
130    assert_eq!(size_of::<Locale>(), 168);
131}
132
133impl Locale {
134    /// The unknown locale "und".
135    pub const UNKNOWN: Self = const {
        match crate::Locale::try_from_utf8_with_single_variant_single_keyword_unicode_extension("und".as_bytes())
            {
            Ok((language, script, region, variant, keyword)) =>
                crate::Locale {
                    id: crate::LanguageIdentifier {
                        language,
                        script,
                        region,
                        variants: match variant {
                            Some(v) => crate::subtags::Variants::from_variant(v),
                            None => crate::subtags::Variants::new(),
                        },
                    },
                    extensions: match keyword {
                        Some((key, value)) =>
                            crate::extensions::Extensions::from_unicode(crate::extensions::unicode::Unicode {
                                    keywords: crate::extensions::unicode::Keywords::new_single(key,
                                        crate::extensions::unicode::Value::from_subtag(value)),
                                    attributes: crate::extensions::unicode::Attributes::new(),
                                }),
                        None => crate::extensions::Extensions::new(),
                    },
                },
            _ => {
                ::core::panicking::panic_fmt(format_args!("Invalid locale: und . Note the locale! macro only supports up to one variant tag; and one unicode keyword, other extension are not supported. Use runtime parsing instead."));
            }
        }
    }crate::locale!("und");
136
137    /// A constructor which takes a utf8 slice, parses it and
138    /// produces a well-formed [`Locale`].
139    ///
140    /// ✨ *Enabled with the `alloc` Cargo feature.*
141    ///
142    /// Note: Support for the legacy `_` separator has been dropped since 2.0.0.
143    /// Users of ICU4X need to convert the `_` to `-` before calling the
144    /// function.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use icu::locale::Locale;
150    ///
151    /// Locale::try_from_str("en-US-u-hc-h12").unwrap();
152    /// ```
153    #[inline]
154    #[cfg(feature = "alloc")]
155    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
156        Self::try_from_utf8(s.as_bytes())
157    }
158
159    /// See [`Self::try_from_str`]
160    ///
161    /// ✨ *Enabled with the `alloc` Cargo feature.*
162    #[cfg(feature = "alloc")]
163    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
164        parse_locale(code_units)
165    }
166
167    /// Normalize the locale (operating on UTF-8 formatted byte slices)
168    ///
169    /// This operation will normalize casing.
170    ///
171    /// ✨ *Enabled with the `alloc` Cargo feature.*
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use icu::locale::Locale;
177    ///
178    /// assert_eq!(
179    ///     Locale::normalize_utf8(b"pL-latn-pl-U-HC-H12").as_deref(),
180    ///     Ok("pl-Latn-PL-u-hc-h12")
181    /// );
182    /// ```
183    #[cfg(feature = "alloc")]
184    pub fn normalize_utf8(input: &[u8]) -> Result<Cow<'_, str>, ParseError> {
185        let locale = Self::try_from_utf8(input)?;
186        Ok(writeable::to_string_or_borrow(&locale, input))
187    }
188
189    /// Normalize the locale (operating on strings)
190    ///
191    /// This operation will normalize casing.
192    ///
193    /// ✨ *Enabled with the `alloc` Cargo feature.*
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use icu::locale::Locale;
199    ///
200    /// assert_eq!(
201    ///     Locale::normalize("pL-latn-pl-U-HC-H12").as_deref(),
202    ///     Ok("pl-Latn-PL-u-hc-h12")
203    /// );
204    /// ```
205    #[cfg(feature = "alloc")]
206    pub fn normalize(input: &str) -> Result<Cow<'_, str>, ParseError> {
207        Self::normalize_utf8(input.as_bytes())
208    }
209
210    /// Compare this [`Locale`] with BCP-47 bytes.
211    ///
212    /// The return value is equivalent to what would happen if you first converted this
213    /// [`Locale`] to a BCP-47 string and then performed a byte comparison.
214    ///
215    /// This function is case-sensitive and results in a *total order*, so it is appropriate for
216    /// binary search. The only argument producing [`Ordering::Equal`] is `self.to_string()`.
217    ///
218    /// # Examples
219    ///
220    /// Sorting a list of locales with this method requires converting one of them to a string:
221    ///
222    /// ```
223    /// use icu::locale::Locale;
224    /// use std::cmp::Ordering;
225    /// use writeable::Writeable;
226    ///
227    /// // Random input order:
228    /// let bcp47_strings: &[&str] = &[
229    ///     "und-u-ca-hebrew",
230    ///     "ar-Latn",
231    ///     "zh-Hant-TW",
232    ///     "zh-TW",
233    ///     "und-fonipa",
234    ///     "zh-Hant",
235    ///     "ar-SA",
236    /// ];
237    ///
238    /// let mut locales = bcp47_strings
239    ///     .iter()
240    ///     .map(|s| s.parse().unwrap())
241    ///     .collect::<Vec<Locale>>();
242    /// locales.sort_by(|a, b| {
243    ///     let b = b.write_to_string();
244    ///     a.strict_cmp(b.as_bytes())
245    /// });
246    /// let strict_cmp_strings = locales
247    ///     .iter()
248    ///     .map(|l| l.to_string())
249    ///     .collect::<Vec<String>>();
250    ///
251    /// // Output ordering, sorted alphabetically
252    /// let expected_ordering: &[&str] = &[
253    ///     "ar-Latn",
254    ///     "ar-SA",
255    ///     "und-fonipa",
256    ///     "und-u-ca-hebrew",
257    ///     "zh-Hant",
258    ///     "zh-Hant-TW",
259    ///     "zh-TW",
260    /// ];
261    ///
262    /// assert_eq!(expected_ordering, strict_cmp_strings);
263    /// ```
264    pub fn strict_cmp(&self, other: &[u8]) -> Ordering {
265        writeable::cmp_utf8(self, other)
266    }
267
268    #[expect(clippy::type_complexity)]
269    pub(crate) fn as_tuple(
270        &self,
271    ) -> (
272        (
273            subtags::Language,
274            Option<subtags::Script>,
275            Option<subtags::Region>,
276            &subtags::Variants,
277        ),
278        (
279            (
280                &extensions::unicode::Attributes,
281                &extensions::unicode::Keywords,
282            ),
283            (
284                Option<(
285                    subtags::Language,
286                    Option<subtags::Script>,
287                    Option<subtags::Region>,
288                    &subtags::Variants,
289                )>,
290                &extensions::transform::Fields,
291            ),
292            &extensions::private::Private,
293            &[extensions::other::Other],
294        ),
295    ) {
296        (self.id.as_tuple(), self.extensions.as_tuple())
297    }
298
299    /// Returns an ordering suitable for use in [`BTreeSet`].
300    ///
301    /// Unlike [`Locale::strict_cmp`], the ordering may or may not be equivalent
302    /// to string ordering, and it may or may not be stable across ICU4X releases.
303    ///
304    /// # Examples
305    ///
306    /// This method returns a nonsensical ordering derived from the fields of the struct:
307    ///
308    /// ```
309    /// use icu::locale::Locale;
310    /// use std::cmp::Ordering;
311    ///
312    /// // Input strings, sorted alphabetically
313    /// let bcp47_strings: &[&str] = &[
314    ///     "ar-Latn",
315    ///     "ar-SA",
316    ///     "und-fonipa",
317    ///     "und-u-ca-hebrew",
318    ///     "zh-Hant",
319    ///     "zh-Hant-TW",
320    ///     "zh-TW",
321    /// ];
322    /// assert!(bcp47_strings.windows(2).all(|w| w[0] < w[1]));
323    ///
324    /// let mut locales = bcp47_strings
325    ///     .iter()
326    ///     .map(|s| s.parse().unwrap())
327    ///     .collect::<Vec<Locale>>();
328    /// locales.sort_by(Locale::total_cmp);
329    /// let total_cmp_strings = locales
330    ///     .iter()
331    ///     .map(|l| l.to_string())
332    ///     .collect::<Vec<String>>();
333    ///
334    /// // Output ordering, sorted arbitrarily
335    /// let expected_ordering: &[&str] = &[
336    ///     "ar-SA",
337    ///     "ar-Latn",
338    ///     "und-u-ca-hebrew",
339    ///     "und-fonipa",
340    ///     "zh-TW",
341    ///     "zh-Hant",
342    ///     "zh-Hant-TW",
343    /// ];
344    ///
345    /// assert_eq!(expected_ordering, total_cmp_strings);
346    /// ```
347    ///
348    /// Use a wrapper to add a [`Locale`] to a [`BTreeSet`]:
349    ///
350    /// ```no_run
351    /// use icu::locale::Locale;
352    /// use std::cmp::Ordering;
353    /// use std::collections::BTreeSet;
354    ///
355    /// #[derive(PartialEq, Eq)]
356    /// struct LocaleTotalOrd(Locale);
357    ///
358    /// impl Ord for LocaleTotalOrd {
359    ///     fn cmp(&self, other: &Self) -> Ordering {
360    ///         self.0.total_cmp(&other.0)
361    ///     }
362    /// }
363    ///
364    /// impl PartialOrd for LocaleTotalOrd {
365    ///     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
366    ///         Some(self.cmp(other))
367    ///     }
368    /// }
369    ///
370    /// let _: BTreeSet<LocaleTotalOrd> = unimplemented!();
371    /// ```
372    ///
373    /// [`BTreeSet`]: alloc::collections::BTreeSet
374    pub fn total_cmp(&self, other: &Self) -> Ordering {
375        self.as_tuple().cmp(&other.as_tuple())
376    }
377
378    /// Compare this `Locale` with a potentially unnormalized BCP-47 string.
379    ///
380    /// The return value is equivalent to what would happen if you first parsed the
381    /// BCP-47 string to a `Locale` and then performed a structural comparison.
382    ///
383    /// ✨ *Enabled with the `alloc` Cargo feature.*
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// use icu::locale::Locale;
389    ///
390    /// let bcp47_strings: &[&str] = &[
391    ///     "pl-LaTn-pL",
392    ///     "uNd",
393    ///     "UND-FONIPA",
394    ///     "UnD-t-m0-TrUe",
395    ///     "uNd-u-CA-Japanese",
396    ///     "ZH",
397    /// ];
398    ///
399    /// for a in bcp47_strings {
400    ///     assert!(a.parse::<Locale>().unwrap().normalizing_eq(a));
401    /// }
402    /// ```
403    #[cfg(feature = "alloc")]
404    pub fn normalizing_eq(&self, other: &str) -> bool {
405        macro_rules! subtag_matches {
406            ($T:ty, $iter:ident, $expected:expr) => {
407                $iter
408                    .next()
409                    .map(|b| <$T>::try_from_utf8(b) == Ok($expected))
410                    .unwrap_or(false)
411            };
412        }
413
414        let mut iter = SubtagIterator::new(other.as_bytes());
415        if !subtag_matches!(subtags::Language, iter, self.id.language) {
416            return false;
417        }
418        if let Some(ref script) = self.id.script
419            && !subtag_matches!(subtags::Script, iter, *script)
420        {
421            return false;
422        }
423        if let Some(ref region) = self.id.region
424            && !subtag_matches!(subtags::Region, iter, *region)
425        {
426            return false;
427        }
428        for variant in self.id.variants.iter() {
429            if !subtag_matches!(subtags::Variant, iter, *variant) {
430                return false;
431            }
432        }
433        if !self.extensions.is_empty() {
434            match extensions::Extensions::try_from_iter(&mut iter) {
435                Ok(exts) => {
436                    if self.extensions != exts {
437                        return false;
438                    }
439                }
440                Err(_) => {
441                    return false;
442                }
443            }
444        }
445        iter.next().is_none()
446    }
447
448    #[doc(hidden)] // macro use
449    #[expect(clippy::type_complexity)]
450    pub const fn try_from_utf8_with_single_variant_single_keyword_unicode_extension(
451        code_units: &[u8],
452    ) -> Result<
453        (
454            subtags::Language,
455            Option<subtags::Script>,
456            Option<subtags::Region>,
457            Option<subtags::Variant>,
458            Option<(extensions::unicode::Key, Option<Subtag>)>,
459        ),
460        ParseError,
461    > {
462        parse_locale_with_single_variant_single_keyword_unicode_keyword_extension(
463            code_units,
464            ParserMode::Locale,
465        )
466    }
467
468    pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
469    where
470        F: FnMut(&str) -> Result<(), E>,
471    {
472        self.id.for_each_subtag_str(f)?;
473        self.extensions.for_each_subtag_str(f)?;
474        Ok(())
475    }
476}
477
478impl AsRef<LanguageIdentifier> for Locale {
479    fn as_ref(&self) -> &LanguageIdentifier {
480        &self.id
481    }
482}
483
484/// ✨ *Enabled with the `alloc` Cargo feature.*
485#[cfg(feature = "alloc")]
486impl FromStr for Locale {
487    type Err = ParseError;
488
489    #[inline]
490    fn from_str(s: &str) -> Result<Self, Self::Err> {
491        Self::try_from_str(s)
492    }
493}
494
495impl From<LanguageIdentifier> for Locale {
496    fn from(id: LanguageIdentifier) -> Self {
497        Self {
498            id,
499            extensions: extensions::Extensions::default(),
500        }
501    }
502}
503
504impl From<Locale> for LanguageIdentifier {
505    fn from(loc: Locale) -> Self {
506        loc.id
507    }
508}
509
510impl core::fmt::Debug for Locale {
511    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
512        writeable::Writeable::write_to(self, f)
513    }
514}
515
516impl writeable::Writeable for Locale {
    fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W)
        -> core::fmt::Result {
        let mut initial = true;
        self.for_each_subtag_str(&mut |subtag|
                    {
                        if initial {
                            initial = false;
                        } else { sink.write_char('-')?; }
                        sink.write_str(subtag)
                    })
    }
    #[inline]
    fn writeable_length_hint(&self) -> writeable::LengthHint {
        let mut result = writeable::LengthHint::exact(0);
        let mut initial = true;
        self.for_each_subtag_str::<core::convert::Infallible,
                _>(&mut |subtag|
                        {
                            if initial { initial = false; } else { result += 1; }
                            result += subtag.len();
                            Ok(())
                        }).expect("infallible");
        result
    }
    fn writeable_borrow(&self) -> Option<&str> {
        let selff = self;
        if selff.extensions.is_empty() {
            selff.id.writeable_borrow()
        } else { None }
    }
}
/// This trait is implemented for compatibility with [`fmt!`](core::fmt).
/// To create a string, [`Writeable::write_to_string`] is usually more efficient.
impl core::fmt::Display for Locale {
    #[inline]
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        ::writeable::Writeable::write_to(&self, f)
    }
}impl_writeable_for_each_subtag_str_no_test!(Locale, selff, selff.extensions.is_empty() => selff.id.writeable_borrow());
517
518#[test]
519fn test_writeable() {
520    use writeable::assert_writeable_eq;
521    assert_writeable_eq!(Locale::UNKNOWN, "und");
522    assert_writeable_eq!("und-001".parse::<Locale>().unwrap(), "und-001");
523    assert_writeable_eq!("und-Mymr".parse::<Locale>().unwrap(), "und-Mymr");
524    assert_writeable_eq!("my-Mymr-MM".parse::<Locale>().unwrap(), "my-Mymr-MM");
525    assert_writeable_eq!(
526        "my-Mymr-MM-posix".parse::<Locale>().unwrap(),
527        "my-Mymr-MM-posix",
528    );
529    assert_writeable_eq!(
530        "zh-macos-posix".parse::<Locale>().unwrap(),
531        "zh-macos-posix",
532    );
533    assert_writeable_eq!(
534        "my-t-my-d0-zawgyi".parse::<Locale>().unwrap(),
535        "my-t-my-d0-zawgyi",
536    );
537    assert_writeable_eq!(
538        "ar-SA-u-ca-islamic-civil".parse::<Locale>().unwrap(),
539        "ar-SA-u-ca-islamic-civil",
540    );
541    assert_writeable_eq!(
542        "en-001-x-foo-bar".parse::<Locale>().unwrap(),
543        "en-001-x-foo-bar",
544    );
545    assert_writeable_eq!("und-t-m0-true".parse::<Locale>().unwrap(), "und-t-m0-true",);
546}
547
548/// # Examples
549///
550/// ```
551/// use icu::locale::Locale;
552/// use icu::locale::{locale, subtags::language};
553///
554/// assert_eq!(Locale::from(language!("en")), locale!("en"));
555/// ```
556impl From<subtags::Language> for Locale {
557    fn from(language: subtags::Language) -> Self {
558        Self {
559            id: language.into(),
560            extensions: extensions::Extensions::new(),
561        }
562    }
563}
564
565/// # Examples
566///
567/// ```
568/// use icu::locale::Locale;
569/// use icu::locale::{locale, subtags::script};
570///
571/// assert_eq!(Locale::from(Some(script!("latn"))), locale!("und-Latn"));
572/// ```
573impl From<Option<subtags::Script>> for Locale {
574    fn from(script: Option<subtags::Script>) -> Self {
575        Self {
576            id: script.into(),
577            extensions: extensions::Extensions::new(),
578        }
579    }
580}
581
582/// # Examples
583///
584/// ```
585/// use icu::locale::Locale;
586/// use icu::locale::{locale, subtags::region};
587///
588/// assert_eq!(Locale::from(Some(region!("US"))), locale!("und-US"));
589/// ```
590impl From<Option<subtags::Region>> for Locale {
591    fn from(region: Option<subtags::Region>) -> Self {
592        Self {
593            id: region.into(),
594            extensions: extensions::Extensions::new(),
595        }
596    }
597}
598
599/// # Examples
600///
601/// ```
602/// use icu::locale::Locale;
603/// use icu::locale::{
604///     locale,
605///     subtags::{language, region, script},
606/// };
607///
608/// assert_eq!(
609///     Locale::from((
610///         language!("en"),
611///         Some(script!("Latn")),
612///         Some(region!("US"))
613///     )),
614///     locale!("en-Latn-US")
615/// );
616/// ```
617impl
618    From<(
619        subtags::Language,
620        Option<subtags::Script>,
621        Option<subtags::Region>,
622    )> for Locale
623{
624    fn from(
625        lsr: (
626            subtags::Language,
627            Option<subtags::Script>,
628            Option<subtags::Region>,
629        ),
630    ) -> Self {
631        Self {
632            id: lsr.into(),
633            extensions: extensions::Extensions::new(),
634        }
635    }
636}