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;