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) -> icu::locale::DataLocale { icu::locale::DataLocale::default() }
95//! # fn load_data(locale: icu::locale::DataLocale) -> 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//! # struct DataLocale;
131//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> DataLocale { DataLocale }
132//! # fn load_data(locale: DataLocale) -> MyData { MyData {} }
133//! # struct MyData {}
134//! # define_preferences!(
135//! #     /// Name of the preferences struct
136//! #     [Copy]
137//! #     ExampleComponentPreferences,
138//! #     {
139//! #         /// A preference relevant to the component
140//! #         hour_cycle: HourCycle
141//! #     }
142//! # );
143//! #
144//! # pub struct ExampleComponent {
145//! #     data: MyData,
146//! # }
147//! # impl ExampleComponent {
148//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
149//! #         let locale = get_data_locale_from_prefs(prefs);
150//! #         let data = load_data(locale);
151//! #
152//! #         Self { data }
153//! #     }
154//! # }
155//! let loc = locale!("en-US-u-hc-h23");
156//! let tf = ExampleComponent::new(loc.into());
157//! ```
158//!
159//! ## Scenario 2: Compose Preferences and Locale
160//! ```
161//! # use icu::locale::preferences::{
162//! #   define_preferences,
163//! #   extensions::unicode::keywords::HourCycle,
164//! # };
165//! # use icu::locale::locale;
166//! # struct DataLocale;
167//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> DataLocale { DataLocale }
168//! # fn load_data(locale: DataLocale) -> MyData { MyData {} }
169//! # struct MyData {}
170//! # define_preferences!(
171//! #     /// Name of the preferences struct
172//! #     [Copy]
173//! #     ExampleComponentPreferences,
174//! #     {
175//! #         /// A preference relevant to the component
176//! #         hour_cycle: HourCycle
177//! #     }
178//! # );
179//! #
180//! # pub struct ExampleComponent {
181//! #     data: MyData,
182//! # }
183//! # impl ExampleComponent {
184//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
185//! #         let locale = get_data_locale_from_prefs(prefs);
186//! #         let data = load_data(locale);
187//! #
188//! #         Self { data }
189//! #     }
190//! # }
191//! let loc = locale!("en-US-u-hc-h23");
192//! let app_prefs = ExampleComponentPreferences {
193//!     hour_cycle: Some(HourCycle::H12),
194//!     ..Default::default()
195//! };
196//!
197//! let mut combined_prefs = ExampleComponentPreferences::from(loc);
198//! combined_prefs.extend(app_prefs);
199//!
200//! // HourCycle is set from the prefs bag and override the value from the locale
201//! assert_eq!(combined_prefs.hour_cycle, Some(HourCycle::H12));
202//!
203//! let tf = ExampleComponent::new(combined_prefs);
204//! ```
205//!
206//! ## Scenario 3: Merge Preferences from Locale, OS, and Application
207//! ```
208//! # use icu::locale::preferences::{
209//! #   define_preferences,
210//! #   extensions::unicode::keywords::HourCycle,
211//! # };
212//! # use icu::locale::locale;
213//! # struct DataLocale;
214//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> DataLocale { DataLocale }
215//! # fn load_data(locale: DataLocale) -> MyData { MyData {} }
216//! # struct MyData {}
217//! # define_preferences!(
218//! #     /// Name of the preferences struct
219//! #     [Copy]
220//! #     ExampleComponentPreferences,
221//! #     {
222//! #         /// A preference relevant to the component
223//! #         hour_cycle: HourCycle
224//! #     }
225//! # );
226//! #
227//! # pub struct ExampleComponent {
228//! #     data: MyData,
229//! # }
230//! # impl ExampleComponent {
231//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
232//! #         let locale = get_data_locale_from_prefs(prefs);
233//! #         let data = load_data(locale);
234//! #
235//! #         Self { data }
236//! #     }
237//! # }
238//! let loc = locale!("en-US");
239//!
240//! // Simulate OS preferences
241//! let os_prefs = ExampleComponentPreferences {
242//!     hour_cycle: Some(HourCycle::H23),
243//!     ..Default::default()
244//! };
245//!
246//! // Application does not specify hour_cycle
247//! let app_prefs = ExampleComponentPreferences {
248//!     hour_cycle: None,
249//!     ..Default::default()
250//! };
251//!
252//! let mut combined_prefs = ExampleComponentPreferences::from(loc);
253//! combined_prefs.extend(os_prefs);
254//! combined_prefs.extend(app_prefs);
255//!
256//! // HourCycle is set from the OS preferences since the application didn't specify it
257//! assert_eq!(combined_prefs.hour_cycle, Some(HourCycle::H23));
258//!
259//! let tf = ExampleComponent::new(combined_prefs);
260//! ```
261//!
262//! ## Scenario 4: Neither Application nor OS specify the preference
263//! ```
264//! # use icu::locale::preferences::{
265//! #   define_preferences,
266//! #   extensions::unicode::keywords::HourCycle,
267//! # };
268//! # use icu::locale::locale;
269//! # struct DataLocale;
270//! # fn get_data_locale_from_prefs(input: ExampleComponentPreferences) -> DataLocale { DataLocale }
271//! # fn load_data(locale: DataLocale) -> MyData { MyData {} }
272//! # struct MyData {}
273//! # define_preferences!(
274//! #     /// Name of the preferences struct
275//! #     [Copy]
276//! #     ExampleComponentPreferences,
277//! #     {
278//! #         /// A preference relevant to the component
279//! #         hour_cycle: HourCycle
280//! #     }
281//! # );
282//! #
283//! # pub struct ExampleComponent {
284//! #     data: MyData,
285//! # }
286//! # impl ExampleComponent {
287//! #     pub fn new(prefs: ExampleComponentPreferences) -> Self {
288//! #         let locale = get_data_locale_from_prefs(prefs);
289//! #         let data = load_data(locale);
290//! #
291//! #         Self { data }
292//! #     }
293//! # }
294//! let loc = locale!("en-US-u-hc-h23");
295//!
296//! // Simulate OS preferences
297//! let os_prefs = ExampleComponentPreferences::default(); // OS does not specify hour_cycle
298//! let app_prefs = ExampleComponentPreferences::default(); // Application does not specify hour_cycle
299//!
300//! let mut combined_prefs = ExampleComponentPreferences::from(loc);
301//! combined_prefs.extend(os_prefs);
302//! combined_prefs.extend(app_prefs);
303//!
304//! // HourCycle is taken from the locale
305//! assert_eq!(combined_prefs.hour_cycle, Some(HourCycle::H23));
306//!
307//! let tf = ExampleComponent::new(combined_prefs);
308//! ```
309//!
310//! [`ICU4X`]: ../icu/index.html
311//! [`Locale`]: crate::Locale
312
313pub mod extensions;
314mod locale;
315pub use locale::*;
316
317/// A low-level trait implemented on each preference exposed in component preferences.
318///
319/// [`PreferenceKey`] has to be implemented on
320/// preferences that are to be included in Formatter preferences.
321/// The trait may be implemented to indicate that the given preference has
322/// a unicode key corresponding to it or be a custom one.
323///
324/// `ICU4X` provides an implementation of [`PreferenceKey`] for all
325/// Unicode Extension Keys. The only external use of this trait is to implement
326/// it on custom preferences that are to be included in a component preferences bag.
327///
328/// The below example show cases a manual generation of an `em` (emoji) unicode extension key
329/// and a custom struct to showcase the difference in their behavior. For all use purposes,
330/// the [`EmojiPresentationStyle`](crate::preferences::extensions::unicode::keywords::EmojiPresentationStyle) preference exposed by this crate should be used.
331///
332/// # Examples
333/// ```
334/// use icu::locale::{
335///   extensions::unicode::{key, Key, value, Value},
336///   preferences::{
337///     define_preferences, PreferenceKey,
338///     extensions::unicode::errors::PreferencesParseError,
339///   },
340/// };
341///
342/// #[non_exhaustive]
343/// #[derive(Debug, Clone, Eq, PartialEq, Copy, Hash, Default)]
344/// pub enum EmojiPresentationStyle {
345///     Emoji,
346///     Text,
347///     #[default]
348///     Default,
349/// }
350///
351/// impl PreferenceKey for EmojiPresentationStyle {
352///     fn unicode_extension_key() -> Option<Key> {
353///         Some(key!("em"))
354///     }
355///
356///     fn try_from_key_value(
357///         key: &Key,
358///         value: &Value,
359///     ) -> Result<Option<Self>, PreferencesParseError> {
360///         if Self::unicode_extension_key() == Some(*key) {
361///             let subtag = value.as_single_subtag()
362///                               .ok_or(PreferencesParseError::InvalidKeywordValue)?;
363///             match subtag.as_str() {
364///                 "emoji" => Ok(Some(Self::Emoji)),
365///                 "text" => Ok(Some(Self::Text)),
366///                 "default" => Ok(Some(Self::Default)),
367///                 _ => Err(PreferencesParseError::InvalidKeywordValue)
368///             }
369///         } else {
370///             Ok(None)
371///         }
372///     }
373///
374///     fn unicode_extension_value(&self) -> Option<Value> {
375///         Some(match self {
376///             EmojiPresentationStyle::Emoji => value!("emoji"),
377///             EmojiPresentationStyle::Text => value!("text"),
378///             EmojiPresentationStyle::Default => value!("default"),
379///         })
380///     }
381/// }
382///
383/// #[non_exhaustive]
384/// #[derive(Debug, Clone, Eq, PartialEq, Hash)]
385/// pub struct CustomFormat {
386///     value: String
387/// }
388///
389/// impl PreferenceKey for CustomFormat {}
390///
391/// define_preferences!(
392///     MyFormatterPreferences,
393///     {
394///         emoji: EmojiPresentationStyle,
395///         custom: CustomFormat
396///     }
397/// );
398/// ```
399/// [`ICU4X`]: ../icu/index.html
400pub trait PreferenceKey: Sized {
401    /// Optional constructor of the given preference. It takes the
402    /// unicode extension key and if the key matches it attemptes to construct
403    /// the preference based on the given value.
404    /// If the value is not a valid value for the given key, the constructor throws.
405    fn try_from_key_value(
406        _key: &crate::extensions::unicode::Key,
407        _value: &crate::extensions::unicode::Value,
408    ) -> Result<Option<Self>, extensions::unicode::errors::PreferencesParseError> {
409        Ok(None)
410    }
411
412    /// Retrieve unicode extension key corresponding to a given preference.
413    fn unicode_extension_key() -> Option<crate::extensions::unicode::Key> {
414        None
415    }
416
417    /// Retrieve unicode extension value corresponding to the given instance of the preference.
418    fn unicode_extension_value(&self) -> Option<crate::extensions::unicode::Value> {
419        None
420    }
421}
422
423/// A macro to facilitate generation of preferences struct.
424///
425///
426/// The generated preferences struct provides methods for merging and converting between [`Locale`] and
427/// the preference bag. See [`preferences`](crate::preferences) for use cases.
428///
429/// In the example below, the input argument is the generated preferences struct which
430/// can be auto-converted from a Locale, or combined from a Locale and Preferences Bag.
431///
432/// # Examples
433/// ```
434/// use icu::locale::{
435///     preferences::{
436///         define_preferences,
437///         extensions::unicode::keywords::HourCycle
438///     },
439///     locale,
440/// };
441///
442/// define_preferences!(
443///     [Copy]
444///     NoCalendarFormatterPreferences,
445///     {
446///         hour_cycle: HourCycle
447///     }
448/// );
449///
450/// struct NoCalendarFormatter {}
451///
452/// impl NoCalendarFormatter {
453///     pub fn try_new(prefs: NoCalendarFormatterPreferences) -> Result<Self, ()> {
454///         // load data and set struct fields based on the prefs input
455///         Ok(Self {})
456///     }
457/// }
458///
459/// let loc = locale!("en-US");
460///
461/// let tf = NoCalendarFormatter::try_new(loc.into());
462/// ```
463///
464/// [`Locale`]: crate::Locale
465#[macro_export]
466#[doc(hidden)]
467macro_rules! __define_preferences {
468    (
469        $(#[$doc:meta])*
470        $([$derive_attrs:ty])?
471        $name:ident,
472        {
473            $(
474                $(#[$key_doc:meta])*
475                $key:ident: $pref:ty
476            ),*
477        }
478     ) => (
479        $(#[$doc])*
480        #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
481        $(#[derive($derive_attrs)])?
482        #[non_exhaustive]
483        pub struct $name {
484            /// Locale Preferences for the Preferences structure.
485            pub locale_preferences: $crate::preferences::LocalePreferences,
486
487            $(
488                $(#[$key_doc])*
489                pub $key: Option<$pref>,
490            )*
491        }
492
493        impl From<$crate::Locale> for $name {
494            fn from(loc: $crate::Locale) -> Self {
495                $name::from(&loc)
496            }
497        }
498
499        impl From<&$crate::Locale> for $name {
500            fn from(loc: &$crate::Locale) -> Self {
501                $name::from_locale_strict(loc).unwrap_or_else(|e| e)
502            }
503        }
504
505        impl From<$crate::LanguageIdentifier> for $name {
506            fn from(lid: $crate::LanguageIdentifier) -> Self {
507                $name::from(&lid)
508            }
509        }
510
511        impl From<&$crate::LanguageIdentifier> for $name {
512            fn from(lid: &$crate::LanguageIdentifier) -> Self {
513                Self {
514                    locale_preferences: lid.into(),
515
516                    $(
517                        $key: None,
518                    )*
519                }
520            }
521        }
522
523        // impl From<$name> for $crate::Locale {
524        //     fn from(other: $name) -> Self {
525        //         use $crate::preferences::PreferenceKey;
526        //         let mut result = Self::from(other.locale_preferences);
527        //         $(
528        //             if let Some(value) = other.$key {
529        //                 if let Some(ue) = <$pref>::unicode_extension_key() {
530        //                     let val = value.unicode_extension_value().unwrap();
531        //                     result.extensions.unicode.keywords.set(ue, val);
532        //                 }
533        //             }
534        //         )*
535        //         result
536        //     }
537        // }
538
539        impl $name {
540            /// Extends the preferences with the values from another set of preferences.
541            pub fn extend(&mut self, other: $name) {
542                self.locale_preferences.extend(other.locale_preferences);
543                $(
544                    if let Some(value) = other.$key {
545                        self.$key = Some(value);
546                    }
547                )*
548            }
549
550            #[doc = concat!("Construct a `", stringify!($name), "` from a `Locale`")]
551            ///
552            /// Returns `Err` if any of of the preference values are invalid.
553            pub fn from_locale_strict(loc: &$crate::Locale) -> Result<Self, Self> {
554                use $crate::preferences::PreferenceKey;
555
556                let mut is_err = false;
557
558                $(
559                    let mut $key = None;
560                )*
561
562                for (k, v) in loc.extensions.unicode.keywords.iter() {
563                    $(
564
565                        match <$pref>::try_from_key_value(k, v) {
566                            Ok(Some(k)) => {
567                                $key = Some(k);
568                                continue;
569                            }
570                            Ok(None) => {}
571                            Err(_) => {
572                                is_err = true
573                            }
574                        }
575                    )*
576                }
577
578                let r = Self {
579                    locale_preferences: $crate::preferences::LocalePreferences::from_locale_strict(loc).unwrap_or_else(|e| { is_err = true; e }),
580
581                    $(
582                        $key,
583                    )*
584                };
585
586                if is_err {
587                    Err(r)
588                } else {
589                    Ok(r)
590                }
591            }
592        }
593    )
594}
595
596#[macro_export]
597#[doc(hidden)]
598macro_rules! __prefs_convert {
599    (
600        $name1:ty,
601        $name2:ty
602    ) => {
603        impl From<&$name1> for $name2 {
604            fn from(other: &$name1) -> Self {
605                let mut result = Self::default();
606                result.locale_preferences = other.locale_preferences;
607                result
608            }
609        }
610    };
611    (
612        $name1:ty,
613        $name2:ty,
614        {
615            $(
616                $key:ident
617            ),*
618        }
619    ) => {
620        impl From<&$name1> for $name2 {
621            fn from(other: &$name1) -> Self {
622                let mut result = Self::default();
623                result.locale_preferences = other.locale_preferences;
624                $(
625                    result.$key = other.$key;
626                )*
627                result
628            }
629        }
630    };
631}
632
633#[doc(inline)]
634pub use __define_preferences as define_preferences;
635
636#[doc(inline)]
637pub use __prefs_convert as prefs_convert;