Skip to main content

icu_locale_core/preferences/
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::extensions::unicode::{SubdivisionId, SubdivisionSuffix};
6use crate::preferences::extensions::unicode::keywords::{RegionOverride, RegionalSubdivision};
7#[cfg(feature = "alloc")]
8use crate::subtags::Variants;
9use crate::subtags::{Language, Region, Script, Variant};
10use crate::DataLocale;
11
12/// The structure storing locale subtags used in preferences.
13#[derive(#[automatically_derived]
impl ::core::fmt::Debug for LocalePreferences {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f,
            "LocalePreferences", "language", &self.language, "script",
            &self.script, "region", &self.region, "variant", &self.variant,
            "region_override", &&self.region_override)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for LocalePreferences {
    #[inline]
    fn clone(&self) -> LocalePreferences {
        let _: ::core::clone::AssertParamIsClone<Language>;
        let _: ::core::clone::AssertParamIsClone<Option<Script>>;
        let _: ::core::clone::AssertParamIsClone<Option<RegionalSubdivision>>;
        let _: ::core::clone::AssertParamIsClone<Option<Variant>>;
        let _: ::core::clone::AssertParamIsClone<Option<RegionOverride>>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for LocalePreferences { }Copy, #[automatically_derived]
impl ::core::cmp::PartialEq for LocalePreferences {
    #[inline]
    fn eq(&self, other: &LocalePreferences) -> bool {
        self.language == other.language && self.script == other.script &&
                    self.region == other.region && self.variant == other.variant
            && self.region_override == other.region_override
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for LocalePreferences {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Language>;
        let _: ::core::cmp::AssertParamIsEq<Option<Script>>;
        let _: ::core::cmp::AssertParamIsEq<Option<RegionalSubdivision>>;
        let _: ::core::cmp::AssertParamIsEq<Option<Variant>>;
        let _: ::core::cmp::AssertParamIsEq<Option<RegionOverride>>;
    }
}Eq, #[automatically_derived]
impl ::core::hash::Hash for LocalePreferences {
    #[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.variant, state);
        ::core::hash::Hash::hash(&self.region_override, state)
    }
}Hash)]
14pub struct LocalePreferences {
15    /// Preference of Language
16    pub(crate) language: Language,
17    /// Preference of Script
18    pub(crate) script: Option<Script>,
19    /// Preference of Region/Subdivision
20    pub(crate) region: Option<RegionalSubdivision>,
21    /// Preference of Variant
22    pub(crate) variant: Option<Variant>,
23    /// Preference of Unicode region override
24    pub(crate) region_override: Option<RegionOverride>,
25}
26
27impl LocalePreferences {
28    /// Convert to a [`DataLocale`], with region-based fallback priority
29    ///
30    /// Most users should use `icu_provider::marker::make_locale()` instead.
31    pub const fn to_data_locale_region_priority(self) -> DataLocale {
32        DataLocale::from_parts(
33            self.language,
34            self.script,
35            if let Some(region) = self.region_override {
36                Some(region.0)
37            } else if let Some(region) = self.region {
38                Some(region.0)
39            } else {
40                None
41            },
42            self.variant,
43        )
44    }
45
46    /// Convert to a `DataLocale`, with language-based fallback priority
47    ///
48    /// Most users should use `icu_provider::marker::make_locale()` instead.
49    pub const fn to_data_locale_language_priority(self) -> DataLocale {
50        DataLocale::from_parts(
51            self.language,
52            self.script,
53            if let Some(region) = self.region {
54                Some(region.0)
55            } else {
56                None
57            },
58            self.variant,
59        )
60    }
61}
62impl Default for LocalePreferences {
63    fn default() -> Self {
64        Self::default()
65    }
66}
67
68impl From<&crate::Locale> for LocalePreferences {
69    fn from(loc: &crate::Locale) -> Self {
70        Self::from_locale_strict(loc).unwrap_or_else(|e| e)
71    }
72}
73
74impl From<&crate::LanguageIdentifier> for LocalePreferences {
75    fn from(lid: &crate::LanguageIdentifier) -> Self {
76        Self {
77            language: lid.language,
78            script: lid.script,
79            region: lid.region.map(|region| {
80                RegionalSubdivision(SubdivisionId {
81                    region,
82                    suffix: SubdivisionSuffix::UNKNOWN,
83                })
84            }),
85            variant: lid.variants.iter().copied().next(),
86            region_override: None,
87        }
88    }
89}
90
91/// ✨ *Enabled with the `alloc` Cargo feature.*
92#[cfg(feature = "alloc")]
93impl From<LocalePreferences> for crate::Locale {
94    fn from(prefs: LocalePreferences) -> Self {
95        Self {
96            id: crate::LanguageIdentifier {
97                language: prefs.language,
98                script: prefs.script,
99                region: prefs.region.map(|sd| sd.region),
100                variants: prefs
101                    .variant
102                    .map(Variants::from_variant)
103                    .unwrap_or_default(),
104            },
105            extensions: {
106                let mut extensions = crate::extensions::Extensions::default();
107                if let Some(sd) = prefs.region.filter(|sd| !sd.suffix.is_unknown()) {
108                    extensions
109                        .unicode
110                        .keywords
111                        .set(RegionalSubdivision::UNICODE_EXTENSION_KEY, sd.into());
112                }
113                if let Some(rg) = prefs.region_override {
114                    extensions
115                        .unicode
116                        .keywords
117                        .set(RegionOverride::UNICODE_EXTENSION_KEY, rg.into());
118                }
119                extensions
120            },
121        }
122    }
123}
124
125impl LocalePreferences {
126    /// Constructs a new [`LocalePreferences`] struct with the defaults.
127    pub const fn default() -> Self {
128        Self {
129            language: Language::UNKNOWN,
130            script: None,
131            region: None,
132            variant: None,
133            region_override: None,
134        }
135    }
136
137    /// Construct a `LocalePreferences` from a `Locale`
138    ///
139    /// Returns `Err` if any of of the preference values are invalid.
140    pub fn from_locale_strict(loc: &crate::Locale) -> Result<Self, Self> {
141        let mut is_err = false;
142
143        let subdivision = if let Some(sd) = loc
144            .extensions
145            .unicode
146            .keywords
147            .get(&RegionalSubdivision::UNICODE_EXTENSION_KEY)
148        {
149            if let Ok(sd) = RegionalSubdivision::try_from(sd) {
150                Some(sd)
151            } else {
152                is_err = true;
153                None
154            }
155        } else {
156            None
157        };
158
159        let region = if let Some(sd) = subdivision {
160            if let Some(region) = loc.id.region {
161                // Discard the subdivison if it doesn't match the region
162                Some(RegionalSubdivision(SubdivisionId {
163                    region,
164                    suffix: if sd.region == region {
165                        sd.suffix
166                    } else {
167                        is_err = true;
168                        SubdivisionSuffix::UNKNOWN
169                    },
170                }))
171            } else {
172                // Use the subdivision's region if there's no region
173                Some(sd)
174            }
175        } else {
176            loc.id.region.map(|region| {
177                RegionalSubdivision(SubdivisionId {
178                    region,
179                    suffix: SubdivisionSuffix::UNKNOWN,
180                })
181            })
182        };
183        let region_override = loc
184            .extensions
185            .unicode
186            .keywords
187            .get(&RegionOverride::UNICODE_EXTENSION_KEY)
188            .and_then(|v| {
189                RegionOverride::try_from(v)
190                    .inspect_err(|_| is_err = true)
191                    .ok()
192            });
193
194        (if is_err { Err } else { Ok })(Self {
195            language: loc.id.language,
196            script: loc.id.script,
197            region,
198            variant: loc.id.variants.iter().copied().next(),
199            region_override,
200        })
201    }
202
203    /// Preference of Language
204    #[deprecated(since = "2.2.0", note = "convert to `DataLocale` to access fields")]
205    pub const fn language(&self) -> Language {
206        self.to_data_locale_language_priority().language
207    }
208
209    /// Preference of Region
210    #[deprecated(since = "2.2.0", note = "convert to `DataLocale` to access fields")]
211    pub const fn region(&self) -> Option<Region> {
212        self.to_data_locale_region_priority().region
213    }
214
215    /// Extends the preferences with the values from another set of preferences.
216    pub fn extend(&mut self, other: LocalePreferences) {
217        if !other.language.is_unknown() {
218            self.language = other.language;
219        }
220        if let Some(script) = other.script {
221            self.script = Some(script);
222        }
223        if let Some(sd) = other.region {
224            // Use the other region if it's different, or if it has a subdivision
225            if !sd.suffix.is_unknown() || Some(sd.region) != self.region.map(|sd| sd.region) {
226                self.region = Some(sd);
227            }
228        }
229        if let Some(variant) = other.variant {
230            self.variant = Some(variant);
231        }
232        if let Some(region_override) = other.region_override {
233            self.region_override = Some(region_override);
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::Locale;
242
243    #[test]
244    fn test_data_locale_conversion() {
245        #[derive(Debug)]
246        struct TestCase<'a> {
247            input: &'a str,
248            language_priority: &'a str,
249            region_priority: &'a str,
250        }
251        let test_cases = [
252            TestCase {
253                input: "en",
254                language_priority: "en",
255                region_priority: "en",
256            },
257            TestCase {
258                input: "en-US",
259                language_priority: "en-US",
260                region_priority: "en-US",
261            },
262            TestCase {
263                input: "en-u-sd-ustx",
264                language_priority: "en-US-u-sd-ustx",
265                region_priority: "en-US-u-sd-ustx",
266            },
267            TestCase {
268                input: "en-US-u-sd-ustx",
269                language_priority: "en-US-u-sd-ustx",
270                region_priority: "en-US-u-sd-ustx",
271            },
272            TestCase {
273                input: "en-u-rg-gbzzzz",
274                language_priority: "en",
275                region_priority: "en-GB",
276            },
277            TestCase {
278                input: "en-US-u-rg-gbzzzz",
279                language_priority: "en-US",
280                region_priority: "en-GB",
281            },
282            TestCase {
283                input: "!en-US-u-sd-gbzzzz",
284                language_priority: "en-US",
285                region_priority: "en-US",
286            },
287            TestCase {
288                input: "en-u-rg-gbzzzz-sd-ustx",
289                language_priority: "en-US-u-sd-ustx",
290                region_priority: "en-GB",
291            },
292            TestCase {
293                input: "en-US-u-rg-gbzzzz-sd-ustx",
294                language_priority: "en-US-u-sd-ustx",
295                region_priority: "en-GB",
296            },
297            TestCase {
298                input: "en-US-u-rg-gbeng-sd-ustx",
299                language_priority: "en-US-u-sd-ustx",
300                region_priority: "en-GB-u-sd-gbeng",
301            },
302            TestCase {
303                input: "!en-TR-u-rg-true",
304                language_priority: "en-TR",
305                region_priority: "en-TR",
306            },
307            TestCase {
308                input: "!en-US-u-sd-tx",
309                language_priority: "en-US",
310                region_priority: "en-US",
311            },
312            TestCase {
313                input: "!en-GB-u-rg-tx",
314                language_priority: "en-GB",
315                region_priority: "en-GB",
316            },
317            TestCase {
318                input: "en-US-u-rg-eng",
319                language_priority: "en-US",
320                region_priority: "en-EN-u-sd-eng",
321            },
322            TestCase {
323                // All alphabetic values of `-u-sd` are valid, as they are of length 3+, so there's
324                // always at least a one-character subdivision. Numeric regions can lead to invalid
325                // values though.
326                input: "!en-001-u-sd-001",
327                language_priority: "en-001",
328                region_priority: "en-001",
329            },
330        ];
331        for test_case in test_cases.iter() {
332            let prefs = if let Some(locale) = test_case.input.strip_prefix("!") {
333                LocalePreferences::from_locale_strict(&Locale::try_from_str(locale).unwrap())
334                    .expect_err(locale)
335            } else {
336                LocalePreferences::from_locale_strict(
337                    &Locale::try_from_str(test_case.input).unwrap(),
338                )
339                .expect(test_case.input)
340            };
341            assert_eq!(
342                prefs.to_data_locale_language_priority().to_string(),
343                test_case.language_priority,
344                "{test_case:?}"
345            );
346            assert_eq!(
347                prefs.to_data_locale_region_priority().to_string(),
348                test_case.region_priority,
349                "{test_case:?}"
350            );
351        }
352    }
353}