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(PartialEq, Eq, Clone, 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 = 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 !subtag_matches!(subtags::Language, iter, self.language) {
408 return false;
409 }
410 if let Some(ref script) = self.script
411 && !subtag_matches!(subtags::Script, iter, *script)
412 {
413 return false;
414 }
415 if let Some(ref region) = self.region
416 && !subtag_matches!(subtags::Region, iter, *region)
417 {
418 return false;
419 }
420 for variant in self.variants.iter() {
421 if !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_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}