Skip to main content

icu_locale_core/preferences/
mod.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
5//! This API provides necessary functionality for building user preferences structs.
6//!
7//! It includes the ability to merge information between the struct and a [`Locale`],
8//! facilitating the resolution of attributes against default values.
9//!
10//! Preferences struct serve as a composable argument to `ICU4X` constructors, allowing
11//! for ergonomic merging between information encoded in multiple sets of user inputs:
12//! Locale, application preferences and operating system preferences.
13//!
14//! The crate is intended primarily to be used by components constructors to normalize the format
15//! of ingesting preferences across all of `ICU4X`.
16//!
17//! # Preferences vs Options
18//!
19//! ICU4X introduces a separation between two classes of parameters that are used
20//! to adjust the behavior of a component.
21//!
22//! `Preferences` represent the user-driven preferences on how the given user wants the internationalization
23//! to behave. Those are items like language, script, calendar and numbering systems etc.
24//!
25//! `Options` represent the developer-driven adjustments that affect how given information is presented
26//! based on the requirements of the application like available space or intended tone.
27//!
28//! # Options Division
29//!
30//! The `Options` themselves are also divided into options that are affecting data slicing, and ones that don't.
31//! This is necessary to allow for DCE and FFI to produce minimal outputs avoiding loading unnecessary data that
32//! is never to be used by a given component.
33//! The result is that some option keys affect specialized constructors such as `try_new_short`, `try_new_long`, which
34//! result in data provider loading only data necessary to format short or long values respectively.
35//! For options that are not affecting data slicing, an `Options` struct is provided that the developer
36//! can fill with selected key values, or use the defaults.
37//!
38//! # Preferences Merging
39//!
40//! In traditional internationalization APIs, the argument passed to constructors is a locale.
41//! ICU4X changes this paradigm by accepting a `Preferences`, which can be extracted from a [`Locale`] and combined with
42//! other `Preferences`s provided by the environment.
43//!
44//! This approach makes it easy for developers to write code that takes just a locale, as in other systems,
45//! as well as handle more sophisticated cases where the application may receive, for example, a locale,
46//! a set of internationalization preferences specified within the application,
47//! and a third set extracted from the operating system's preferences.
48//!
49//! # ECMA-402 vs ICU4X
50//!
51//! The result of the two paradigm shifts presented above is that the way constructors work is different.
52//!
53//! ## ECMA-402
54//! ```ignore
55//! let locale = new Locale("en-US-u-hc-h12");
56//! let options = {
57//!   hourCycle: "h24", // user preference
58//!   timeStyle: "long", // developer option
59//! };
60//!
61//! let dtf = new DateTimeFormat(locale, options);
62//! ```
63//!
64//! ## ICU4X
65//! ```ignore
66//! let loc = locale!("en-US-u-hc-h12");
67//! let prefs = DateTimeFormatterPreferences {
68//!     hour_cycle: HourCycle::H23,
69//! };
70//! let options = DateTimeFormatterOptions {
71//!     time_style: TimeStyle::Long,
72//! };
73//!
74//! let mut combined_prefs = DateTimeFormatterPreferences::from(loc);
75//! combined_prefs.extend(prefs);
76//!
77//! let dtf = DateTimeFormatter::try_new(combined_prefs, options);
78//! ```
79//!
80//! This architecture allows for flexible composition of user and developer settings
81//! sourced from different locations in custom ways based on the needs of each deployment.
82//!
83//! Below are some examples of how the `Preferences` model can be used in different setups.
84//!
85//! # Examples
86//!
87//! ```
88//! use icu::locale::preferences::{
89//!   define_preferences,
90//!   extensions::unicode::keywords::HourCycle,
91//! };
92//! use icu::locale::locale;
93//!
94//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> () { () }
95//! # fn load_data(locale: ()) -> MyData { MyData {} }
96//! # struct MyData {}
97//! define_preferences!(
98//!     /// Name of the preferences struct
99//!     [Copy]
100//!     ExampleComponentPreferences,
101//!     {
102//!         /// A preference relevant to the component
103//!         hour_cycle: HourCycle
104//!     }
105//! );
106//!
107//! pub struct ExampleComponent {
108//!     data: MyData,
109//! }
110//!
111//! impl ExampleComponent {
112//!     pub fn new(prefs: ExampleComponentPreferences) -> Self {
113//!         let locale = get_data_locale_from_prefs(prefs);
114//!         let data = load_data(locale);
115//!
116//!         Self { data }
117//!     }
118//! }
119//! ```
120//!
121//! Now we can use that component in multiple different ways,
122//!
123//! ## Scenario 1: Use Locale as the only input
124//! ```
125//! # use icu::locale::preferences::{
126//! #   define_preferences,
127//! #   extensions::unicode::keywords::HourCycle,
128//! # };
129//! # use icu::locale::locale;
130//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> () { () }
131//! # fn load_data(locale: ()) -> MyData { MyData {} }
132//! # struct MyData {}
133//! # define_preferences!(
134//! #     /// Name of the preferences struct
135//! #     [Copy]
136//! #     ExampleComponentPreferences,
137//! #     {
138//! #         /// A preference relevant to the component
139//! #         hour_cycle: HourCycle
140//! #     }
141//! # );
142//! #
143//! # pub struct ExampleComponent {
144//! #     data: MyData,
145//! # }
146//! # impl ExampleComponent {
147//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
148//! #         let locale = get_data_locale_from_prefs(prefs);
149//! #         let data = load_data(locale);
150//! #
151//! #         Self { data }
152//! #     }
153//! # }
154//! let loc = locale!("en-US-u-hc-h23");
155//! let tf = ExampleComponent::new(loc.into());
156//! ```
157//!
158//! ## Scenario 2: Compose Preferences and Locale
159//! ```
160//! # use icu::locale::preferences::{
161//! #   define_preferences,
162//! #   extensions::unicode::keywords::HourCycle,
163//! # };
164//! # use icu::locale::locale;
165//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> () { () }
166//! # fn load_data(locale: ()) -> MyData { MyData {} }
167//! # struct MyData {}
168//! # define_preferences!(
169//! #     /// Name of the preferences struct
170//! #     [Copy]
171//! #     ExampleComponentPreferences,
172//! #     {
173//! #         /// A preference relevant to the component
174//! #         hour_cycle: HourCycle
175//! #     }
176//! # );
177//! #
178//! # pub struct ExampleComponent {
179//! #     data: MyData,
180//! # }
181//! # impl ExampleComponent {
182//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
183//! #         let locale = get_data_locale_from_prefs(prefs);
184//! #         let data = load_data(locale);
185//! #
186//! #         Self { data }
187//! #     }
188//! # }
189//! let loc = locale!("en-US-u-hc-h23");
190//! let app_prefs = ExampleComponentPreferences {
191//!     hour_cycle: Some(HourCycle::H12),
192//!     ..Default::default()
193//! };
194//!
195//! let mut combined_prefs = ExampleComponentPreferences::from(loc);
196//! combined_prefs.extend(app_prefs);
197//!
198//! // HourCycle is set from the prefs bag and override the value from the locale
199//! assert_eq!(combined_prefs.hour_cycle, Some(HourCycle::H12));
200//!
201//! let tf = ExampleComponent::new(combined_prefs);
202//! ```
203//!
204//! ## Scenario 3: Merge Preferences from Locale, OS, and Application
205//! ```
206//! # use icu::locale::preferences::{
207//! #   define_preferences,
208//! #   extensions::unicode::keywords::HourCycle,
209//! # };
210//! # use icu::locale::locale;
211//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> () { () }
212//! # fn load_data(locale: ()) -> MyData { MyData {} }
213//! # struct MyData {}
214//! # define_preferences!(
215//! #     /// Name of the preferences struct
216//! #     [Copy]
217//! #     ExampleComponentPreferences,
218//! #     {
219//! #         /// A preference relevant to the component
220//! #         hour_cycle: HourCycle
221//! #     }
222//! # );
223//! #
224//! # pub struct ExampleComponent {
225//! #     data: MyData,
226//! # }
227//! # impl ExampleComponent {
228//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
229//! #         let locale = get_data_locale_from_prefs(prefs);
230//! #         let data = load_data(locale);
231//! #
232//! #         Self { data }
233//! #     }
234//! # }
235//! let loc = locale!("en-US");
236//!
237//! // Simulate OS preferences
238//! let os_prefs = ExampleComponentPreferences {
239//!     hour_cycle: Some(HourCycle::H23),
240//!     ..Default::default()
241//! };
242//!
243//! // Application does not specify hour_cycle
244//! let app_prefs = ExampleComponentPreferences {
245//!     hour_cycle: None,
246//!     ..Default::default()
247//! };
248//!
249//! let mut combined_prefs = ExampleComponentPreferences::from(loc);
250//! combined_prefs.extend(os_prefs);
251//! combined_prefs.extend(app_prefs);
252//!
253//! // HourCycle is set from the OS preferences since the application didn't specify it
254//! assert_eq!(combined_prefs.hour_cycle, Some(HourCycle::H23));
255//!
256//! let tf = ExampleComponent::new(combined_prefs);
257//! ```
258//!
259//! ## Scenario 4: Neither Application nor OS specify the preference
260//! ```
261//! # use icu::locale::preferences::{
262//! #   define_preferences,
263//! #   extensions::unicode::keywords::HourCycle,
264//! # };
265//! # use icu::locale::locale;
266//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> () { () }
267//! # fn load_data(locale: ()) -> MyData { MyData {} }
268//! # struct MyData {}
269//! # define_preferences!(
270//! #     /// Name of the preferences struct
271//! #     [Copy]
272//! #     ExampleComponentPreferences,
273//! #     {
274//! #         /// A preference relevant to the component
275//! #         hour_cycle: HourCycle
276//! #     }
277//! # );
278//! #
279//! # pub struct ExampleComponent {
280//! #     data: MyData,
281//! # }
282//! # impl ExampleComponent {
283//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
284//! #         let locale = get_data_locale_from_prefs(prefs);
285//! #         let data = load_data(locale);
286//! #
287//! #         Self { data }
288//! #     }
289//! # }
290//! let loc = locale!("en-US-u-hc-h23");
291//!
292//! // Simulate OS preferences
293//! let os_prefs = ExampleComponentPreferences::default(); // OS does not specify hour_cycle
294//! let app_prefs = ExampleComponentPreferences::default(); // Application does not specify hour_cycle
295//!
296//! let mut combined_prefs = ExampleComponentPreferences::from(loc);
297//! combined_prefs.extend(os_prefs);
298//! combined_prefs.extend(app_prefs);
299//!
300//! // HourCycle is taken from the locale
301//! assert_eq!(combined_prefs.hour_cycle, Some(HourCycle::H23));
302//!
303//! let tf = ExampleComponent::new(combined_prefs);
304//! ```
305//!
306//! [`ICU4X`]: ../icu/index.html
307//! [`Locale`]: crate::Locale
308
309pub mod extensions;
310mod locale;
311pub use locale::*;
312
313/// A low-level trait implemented on each preference exposed in component preferences.
314///
315/// [`PreferenceKey`] has to be implemented on
316/// preferences that are to be included in Formatter preferences.
317/// The trait may be implemented to indicate that the given preference has
318/// a unicode key corresponding to it or be a custom one.
319///
320/// `ICU4X` provides an implementation of [`PreferenceKey`] for all
321/// Unicode Extension Keys. The only external use of this trait is to implement
322/// it on custom preferences that are to be included in a component preferences bag.
323///
324/// The below example show cases a manual generation of an `em` (emoji) unicode extension key
325/// and a custom struct to showcase the difference in their behavior. For all use purposes,
326/// the [`EmojiPresentationStyle`](crate::preferences::extensions::unicode::keywords::EmojiPresentationStyle) preference exposed by this crate should be used.
327///
328/// # Examples
329/// ```
330/// use icu::locale::{
331///   extensions::unicode::{key, Key, value, Value},
332///   preferences::{
333///     define_preferences, PreferenceKey,
334///     extensions::unicode::errors::PreferencesParseError,
335///   },
336/// };
337///
338/// #[non_exhaustive]
339/// #[derive(Debug, Clone, Eq, PartialEq, Copy, Hash, Default)]
340/// pub enum EmojiPresentationStyle {
341///     Emoji,
342///     Text,
343///     #[default]
344///     Default,
345/// }
346///
347/// impl PreferenceKey for EmojiPresentationStyle {
348///     fn unicode_extension_key() -> Option<Key> {
349///         Some(key!("em"))
350///     }
351///
352///     fn try_from_key_value(
353///         key: &Key,
354///         value: &Value,
355///     ) -> Result<Option<Self>, PreferencesParseError> {
356///         if Self::unicode_extension_key() == Some(*key) {
357///             let subtag = value.as_single_subtag()
358///                               .ok_or(PreferencesParseError::InvalidKeywordValue)?;
359///             match subtag.as_str() {
360///                 "emoji" => Ok(Some(Self::Emoji)),
361///                 "text" => Ok(Some(Self::Text)),
362///                 "default" => Ok(Some(Self::Default)),
363///                 _ => Err(PreferencesParseError::InvalidKeywordValue)
364///             }
365///         } else {
366///             Ok(None)
367///         }
368///     }
369///
370///     fn unicode_extension_value(&self) -> Option<Value> {
371///         Some(match self {
372///             EmojiPresentationStyle::Emoji => value!("emoji"),
373///             EmojiPresentationStyle::Text => value!("text"),
374///             EmojiPresentationStyle::Default => value!("default"),
375///         })
376///     }
377/// }
378///
379/// #[non_exhaustive]
380/// #[derive(Debug, Clone, Eq, PartialEq, Hash)]
381/// pub struct CustomFormat {
382///     value: String
383/// }
384///
385/// impl PreferenceKey for CustomFormat {}
386///
387/// define_preferences!(
388///     MyFormatterPreferences,
389///     {
390///         emoji: EmojiPresentationStyle,
391///         custom: CustomFormat
392///     }
393/// );
394/// ```
395/// [`ICU4X`]: ../icu/index.html
396pub trait PreferenceKey: Sized {
397    /// Optional constructor of the given preference. It takes the
398    /// unicode extension key and if the key matches it attemptes to construct
399    /// the preference based on the given value.
400    /// If the value is not a valid value for the given key, the constructor throws.
401    fn try_from_key_value(
402        _key: &crate::extensions::unicode::Key,
403        _value: &crate::extensions::unicode::Value,
404    ) -> Result<Option<Self>, extensions::unicode::errors::PreferencesParseError> {
405        Ok(None)
406    }
407
408    /// Retrieve unicode extension key corresponding to a given preference.
409    fn unicode_extension_key() -> Option<crate::extensions::unicode::Key> {
410        None
411    }
412
413    /// Retrieve unicode extension value corresponding to the given instance of the preference.
414    fn unicode_extension_value(&self) -> Option<crate::extensions::unicode::Value> {
415        None
416    }
417}
418
419/// A macro to facilitate generation of preferences struct.
420///
421///
422/// The generated preferences struct provides methods for merging and converting between [`Locale`] and
423/// the preference bag. See [`preferences`](crate::preferences) for use cases.
424///
425/// In the example below, the input argument is the generated preferences struct which
426/// can be auto-converted from a Locale, or combined from a Locale and Preferences Bag.
427///
428/// # Examples
429/// ```
430/// use icu::locale::{
431///     preferences::{
432///         define_preferences,
433///         extensions::unicode::keywords::HourCycle
434///     },
435///     locale,
436/// };
437///
438/// define_preferences!(
439///     [Copy]
440///     NoCalendarFormatterPreferences,
441///     {
442///         hour_cycle: HourCycle
443///     }
444/// );
445///
446/// struct NoCalendarFormatter {}
447///
448/// impl NoCalendarFormatter {
449///     pub fn try_new(prefs: NoCalendarFormatterPreferences) -> Result<Self, ()> {
450///         // load data and set struct fields based on the prefs input
451///         Ok(Self {})
452///     }
453/// }
454///
455/// let loc = locale!("en-US");
456///
457/// let tf = NoCalendarFormatter::try_new(loc.into());
458/// ```
459///
460/// [`Locale`]: crate::Locale
461#[macro_export]
462#[doc(hidden)]
463macro_rules! __define_preferences {
464    (
465        $(#[$doc:meta])*
466        $([$derive_attrs:ty])?
467        $name:ident,
468        {
469            $(
470                $(#[$key_doc:meta])*
471                $key:ident: $pref:ty
472            ),*
473        }
474     ) => (
475        $(#[$doc])*
476        #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
477        $(#[derive($derive_attrs)])?
478        #[non_exhaustive]
479        pub struct $name {
480            /// Locale Preferences for the Preferences structure.
481            pub locale_preferences: $crate::preferences::LocalePreferences,
482
483            $(
484                $(#[$key_doc])*
485                pub $key: Option<$pref>,
486            )*
487        }
488
489        impl From<$crate::Locale> for $name {
490            fn from(loc: $crate::Locale) -> Self {
491                $name::from(&loc)
492            }
493        }
494
495        impl From<&$crate::Locale> for $name {
496            fn from(loc: &$crate::Locale) -> Self {
497                $name::from_locale_strict(loc).unwrap_or_else(|e| e)
498            }
499        }
500
501        impl From<$crate::LanguageIdentifier> for $name {
502            fn from(lid: $crate::LanguageIdentifier) -> Self {
503                $name::from(&lid)
504            }
505        }
506
507        impl From<&$crate::LanguageIdentifier> for $name {
508            fn from(lid: &$crate::LanguageIdentifier) -> Self {
509                Self {
510                    locale_preferences: lid.into(),
511
512                    $(
513                        $key: None,
514                    )*
515                }
516            }
517        }
518
519        // impl From<$name> for $crate::Locale {
520        //     fn from(other: $name) -> Self {
521        //         use $crate::preferences::PreferenceKey;
522        //         let mut result = Self::from(other.locale_preferences);
523        //         $(
524        //             if let Some(value) = other.$key {
525        //                 if let Some(ue) = <$pref>::unicode_extension_key() {
526        //                     let val = value.unicode_extension_value().unwrap();
527        //                     result.extensions.unicode.keywords.set(ue, val);
528        //                 }
529        //             }
530        //         )*
531        //         result
532        //     }
533        // }
534
535        impl $name {
536            /// Extends the preferences with the values from another set of preferences.
537            pub fn extend(&mut self, other: $name) {
538                self.locale_preferences.extend(other.locale_preferences);
539                $(
540                    if let Some(value) = other.$key {
541                        self.$key = Some(value);
542                    }
543                )*
544            }
545
546            #[doc = concat!("Construct a `", stringify!($name), "` from a `Locale`")]
547            ///
548            /// Returns `Err` if any of of the preference values are invalid.
549            pub fn from_locale_strict(loc: &$crate::Locale) -> Result<Self, Self> {
550                use $crate::preferences::PreferenceKey;
551
552                let mut is_err = false;
553
554                $(
555                    let mut $key = None;
556                )*
557
558                for (k, v) in loc.extensions.unicode.keywords.iter() {
559                    $(
560
561                        match <$pref>::try_from_key_value(k, v) {
562                            Ok(Some(k)) => {
563                                $key = Some(k);
564                                continue;
565                            }
566                            Ok(None) => {}
567                            Err(_) => {
568                                is_err = true
569                            }
570                        }
571                    )*
572                }
573
574                let r = Self {
575                    locale_preferences: $crate::preferences::LocalePreferences::from_locale_strict(loc).unwrap_or_else(|e| { is_err = true; e }),
576
577                    $(
578                        $key,
579                    )*
580                };
581
582                if is_err {
583                    Err(r)
584                } else {
585                    Ok(r)
586                }
587            }
588        }
589    )
590}
591
592#[macro_export]
593#[doc(hidden)]
594macro_rules! __prefs_convert {
595    (
596        $name1:ty,
597        $name2:ty
598    ) => {
599        impl From<&$name1> for $name2 {
600            fn from(other: &$name1) -> Self {
601                let mut result = Self::default();
602                result.locale_preferences = other.locale_preferences;
603                result
604            }
605        }
606    };
607    (
608        $name1:ty,
609        $name2:ty,
610        {
611            $(
612                $key:ident
613            ),*
614        }
615    ) => {
616        impl From<&$name1> for $name2 {
617            fn from(other: &$name1) -> Self {
618                let mut result = Self::default();
619                result.locale_preferences = other.locale_preferences;
620                $(
621                    result.$key = other.$key;
622                )*
623                result
624            }
625        }
626    };
627}
628
629#[doc(inline)]
630pub use __define_preferences as define_preferences;
631
632#[doc(inline)]
633pub use __prefs_convert as prefs_convert;