Skip to main content

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