Skip to main content

es_fluent_shared/
language.rs

1//! Shared language-identifier parsing helpers.
2
3use icu_locale::{Locale, LocaleCanonicalizer};
4use thiserror::Error;
5pub use unic_langid::LanguageIdentifier;
6use unic_langid::LanguageIdentifierError;
7
8/// Errors returned when parsing a language identifier that must already be canonicalized.
9#[derive(Debug, Error)]
10pub enum CanonicalLanguageIdentifierError {
11    /// The identifier could not be parsed as a BCP-47 language identifier.
12    #[error("Invalid language identifier '{name}'")]
13    Invalid {
14        /// The invalid identifier.
15        name: String,
16        /// The parsing error produced by `unic-langid`.
17        #[source]
18        source: LanguageIdentifierError,
19    },
20    /// The identifier parsed as a `unic-langid` identifier but could not be converted to ICU.
21    #[error("Language identifier '{name}' could not be parsed as an ICU locale: {details}")]
22    IcuInvalid {
23        /// The invalid identifier.
24        name: String,
25        /// The ICU parsing error.
26        details: String,
27    },
28    /// The identifier parsed successfully but was not written in canonical form.
29    #[error("Locale directory '{name}' must use canonical BCP-47 form '{canonical}'")]
30    NonCanonical {
31        /// The original identifier.
32        name: String,
33        /// The canonical identifier.
34        canonical: String,
35    },
36}
37
38/// Parses a language identifier and rejects non-canonical locale forms.
39pub fn parse_canonical_language_identifier(
40    name: &str,
41) -> Result<LanguageIdentifier, CanonicalLanguageIdentifierError> {
42    let lang = name.parse::<LanguageIdentifier>().map_err(|source| {
43        CanonicalLanguageIdentifierError::Invalid {
44            name: name.to_string(),
45            source,
46        }
47    })?;
48    let mut locale =
49        name.parse::<Locale>()
50            .map_err(|source| CanonicalLanguageIdentifierError::IcuInvalid {
51                name: name.to_string(),
52                details: source.to_string(),
53            })?;
54    LocaleCanonicalizer::new_extended().canonicalize(&mut locale);
55
56    let canonical = locale.to_string();
57    if canonical != name {
58        return Err(CanonicalLanguageIdentifierError::NonCanonical {
59            name: name.to_string(),
60            canonical,
61        });
62    }
63
64    Ok(lang)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn parses_canonical_language_identifier() {
73        let lang = parse_canonical_language_identifier("de-DE-1901")
74            .expect("canonical locale should parse");
75        assert_eq!(lang.to_string(), "de-DE-1901");
76    }
77
78    #[test]
79    fn rejects_invalid_identifier() {
80        let err = parse_canonical_language_identifier("not-a-lang!")
81            .expect_err("invalid locale should fail");
82        assert!(matches!(
83            err,
84            CanonicalLanguageIdentifierError::Invalid { name, .. } if name == "not-a-lang!"
85        ));
86    }
87
88    #[test]
89    fn rejects_noncanonical_identifier() {
90        let err = parse_canonical_language_identifier("en-us")
91            .expect_err("noncanonical locale should fail");
92        assert!(matches!(
93            err,
94            CanonicalLanguageIdentifierError::NonCanonical { name, canonical }
95                if name == "en-us" && canonical == "en-US"
96        ));
97    }
98
99    #[test]
100    fn rejects_noncanonical_language_identifiers() {
101        let err = parse_canonical_language_identifier("iw")
102            .expect_err("noncanonical identifiers should fail");
103        assert!(matches!(
104            err,
105            CanonicalLanguageIdentifierError::NonCanonical { name, canonical }
106                if name == "iw" && canonical == "he"
107        ));
108    }
109}