diff --git a/Cargo.lock b/Cargo.lock index dfb866147d4..859048336fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,15 @@ dependencies = [ name = "icu_plurals_data" version = "1.4.0" +[[package]] +name = "icu_preferences" +version = "0.0.1" +dependencies = [ + "icu_datetime", + "icu_locid", + "tinystr", +] + [[package]] name = "icu_properties" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index 409db1922e0..b72ce87dd5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ members = [ "utils/ixdtf", "utils/litemap", "utils/pattern", + "utils/preferences", "utils/resb", "utils/tinystr", "utils/tzif", diff --git a/components/locid/src/extensions/unicode/keywords.rs b/components/locid/src/extensions/unicode/keywords.rs index f934841d83a..d668b9e6728 100644 --- a/components/locid/src/extensions/unicode/keywords.rs +++ b/components/locid/src/extensions/unicode/keywords.rs @@ -358,6 +358,11 @@ impl Keywords { } } + /// Produce an ordered iterator over key-value pairs + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + pub(crate) fn for_each_subtag_str(&self, f: &mut F) -> Result<(), E> where F: FnMut(&str) -> Result<(), E>, diff --git a/components/locid/src/extensions/unicode/value.rs b/components/locid/src/extensions/unicode/value.rs index 98d9d903d44..110a54ead2a 100644 --- a/components/locid/src/extensions/unicode/value.rs +++ b/components/locid/src/extensions/unicode/value.rs @@ -191,6 +191,15 @@ macro_rules! extensions_unicode_value { ); R }}; + ($value:literal, $value2:literal) => {{ + let v: &str = concat!($value, "-", $value2); + let R: $crate::extensions::unicode::Value = + match $crate::extensions::unicode::Value::try_from_bytes(v.as_bytes()) { + Ok(r) => r, + _ => panic!(concat!("Invalid Unicode extension value: ", $value)), + }; + R + }}; } #[doc(inline)] pub use extensions_unicode_value as value; diff --git a/utils/preferences/Cargo.toml b/utils/preferences/Cargo.toml new file mode 100644 index 00000000000..65d794cca36 --- /dev/null +++ b/utils/preferences/Cargo.toml @@ -0,0 +1,29 @@ +# This file is part of ICU4X. For terms of use, please see the file +# called LICENSE at the top level of the ICU4X source tree +# (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +[package] +name = "icu_preferences" +description = "API for resolving preferences" +version = "0.0.1" +categories = ["internationalization"] +license-file = "LICENSE" + +authors.workspace = true +edition.workspace = true +include.workspace = true +repository.workspace = true +rust-version.workspace = true + +[package.metadata.workspaces] +independent = true + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +icu_locid = { workspace = true } +tinystr = { workspace = true } + +[dev-dependencies] +icu_datetime = { workspace = true } diff --git a/utils/preferences/LICENSE b/utils/preferences/LICENSE new file mode 100644 index 00000000000..9845aa5f488 --- /dev/null +++ b/utils/preferences/LICENSE @@ -0,0 +1,44 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. diff --git a/utils/preferences/README.md b/utils/preferences/README.md new file mode 100644 index 00000000000..c71986d717e --- /dev/null +++ b/utils/preferences/README.md @@ -0,0 +1,59 @@ +# tinystr [![crates.io](https://img.shields.io/crates/v/tinystr)](https://crates.io/crates/tinystr) + + + +## `icu_preferences` + +`icu_preferences` is a utility crate of the [`ICU4X`] project. + +This API provides necessary functionality for building user preferences structs with ability +to `merge` information between the struct and a [`Locale`] and facilitate resolution of the +attributes against default values. + +The crate is intended primarily to be used by components constructors to normalize the format +of ingesting preferences across all of [`ICU4X`]. + +## Examples: + +```rust +use icu_preferences::preferences; +use icu_datetime::options::preferences::HourCycle; +use icu_locid::{LanguageIdentifier, extensions_unicode_key, Locale}; + +pub fn get_defaults(lid: &Option) -> ExampleComponentResolvedPreferences { + unimplemented!() +} + +preferences!( + ExampleComponentPreferences, + ExampleComponentResolvedPreferences, + { + hour_cycle => Option, HourCycle, Some(extensions_unicode_key!("hc")) + } +); + +pub struct ExampleComponent { + resolved_prefs: ExampleComponentResolvedPreferences, +} + +impl ExampleComponent { + pub fn new(prefs: ExampleComponentPreferences) -> Self { + // Retrieve the default values for the given [`LanguageIdentifier`]. + let mut resolved_prefs = get_defaults(&prefs.lid); + + // Resolve them against provided preferences. + resolved_prefs.extend(&prefs); + + Self { resolved_prefs } + } +} +``` + +[`ICU4X`]: ../icu/index.html +[`Locale`]: icu_locid::Locale + + + +## More Information + +For more information on development, authorship, contributing etc. please visit [`ICU4X home page`](https://github.com/unicode-org/icu4x). diff --git a/utils/preferences/src/extensions/mod.rs b/utils/preferences/src/extensions/mod.rs new file mode 100644 index 00000000000..7fbfcd52e68 --- /dev/null +++ b/utils/preferences/src/extensions/mod.rs @@ -0,0 +1 @@ +pub mod unicode; diff --git a/utils/preferences/src/extensions/unicode/errors.rs b/utils/preferences/src/extensions/unicode/errors.rs new file mode 100644 index 00000000000..f64df2a648b --- /dev/null +++ b/utils/preferences/src/extensions/unicode/errors.rs @@ -0,0 +1,9 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#[non_exhaustive] +pub enum Error { + UnknownKeyword, + UnknownKeywordValue, +} diff --git a/utils/preferences/src/extensions/unicode/keywords/calendar.rs b/utils/preferences/src/extensions/unicode/keywords/calendar.rs new file mode 100644 index 00000000000..80fa3649764 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/calendar.rs @@ -0,0 +1,35 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use crate::preferences::PreferenceKey; +use icu_locid::extensions::unicode; + +// https://github.com/unicode-org/cldr/blob/main/common/bcp47/calendar.xml +enum_keyword!(IslamicCalendar { + "umalqura" => Umalqura, + "tbla" => Tbla, + "civil" => Civil, + "rgsa" => Rgsa +}); + +enum_keyword!(Calendar { + "buddhist" => Buddhist, + "chinese" => Chinese, + "coptic" => Coptic, + "dangi" => Dangi, + "ethioaa" => Ethioaa, + "ethiopic" => Ethiopic, + "gregory" => Gregory, + "hebrew" => Hebrew, + "indian" => Indian, + "islamic" => Islamic(IslamicCalendar) { + "umalqura" => Umalqura + }, + "iso8601" => Iso8601, + "japanese" => Japanese, + "persian" => Persian, + "roc" => Roc +}, "ca"); diff --git a/utils/preferences/src/extensions/unicode/keywords/collation.rs b/utils/preferences/src/extensions/unicode/keywords/collation.rs new file mode 100644 index 00000000000..caf8c627ba6 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/collation.rs @@ -0,0 +1,15 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use icu_locid::extensions::unicode; + +enum_keyword!(Collation { + "standard" => Standard, + "search" => Search, + "phonetic" => Phonetic, + "pinyin" => Pinyin, + "searchjl" => Searchjl +}); diff --git a/utils/preferences/src/extensions/unicode/keywords/currency.rs b/utils/preferences/src/extensions/unicode/keywords/currency.rs new file mode 100644 index 00000000000..548c51b0df6 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/currency.rs @@ -0,0 +1,17 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::extensions::unicode::errors::Error; +use crate::preferences::PreferenceKey; +use crate::struct_keyword; +use icu_locid::extensions::unicode; +use tinystr::TinyAsciiStr; + +struct_keyword!(Currency, "cu", TinyAsciiStr<3>, convert_value_to_currency); + +fn convert_value_to_currency(input: &unicode::Value) -> Result { + //XXX: Validate + let i = TinyAsciiStr::from_str(&input.to_string()).unwrap(); + Ok(Currency(i)) +} diff --git a/utils/preferences/src/extensions/unicode/keywords/currency_format.rs b/utils/preferences/src/extensions/unicode/keywords/currency_format.rs new file mode 100644 index 00000000000..01dfe330d32 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/currency_format.rs @@ -0,0 +1,12 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use icu_locid::extensions::unicode; + +enum_keyword!(CurrencyFormat { + "standard" => Standard, + "account" => Account +}); diff --git a/utils/preferences/src/extensions/unicode/keywords/dictionary_break.rs b/utils/preferences/src/extensions/unicode/keywords/dictionary_break.rs new file mode 100644 index 00000000000..470ff6e23c9 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/dictionary_break.rs @@ -0,0 +1,17 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::extensions::unicode::errors::Error; +use crate::preferences::PreferenceKey; +use crate::struct_keyword; +use icu_locid::extensions::unicode; +use icu_locid::subtags::Script; +use std::str::FromStr; + +struct_keyword!(DictionaryBreak, "dx", Script, convert_value_to_db); + +fn convert_value_to_db(input: &unicode::Value) -> Result { + let i = Script::from_str(&input.to_string()).unwrap(); + Ok(DictionaryBreak(i)) +} diff --git a/utils/preferences/src/extensions/unicode/keywords/emoji.rs b/utils/preferences/src/extensions/unicode/keywords/emoji.rs new file mode 100644 index 00000000000..a3a44ef89bb --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/emoji.rs @@ -0,0 +1,13 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use icu_locid::extensions::unicode; + +enum_keyword!(Emoji { + "emoji" => Emoji, + "text" => Text, + "default" => Default +}); diff --git a/utils/preferences/src/extensions/unicode/keywords/first_day.rs b/utils/preferences/src/extensions/unicode/keywords/first_day.rs new file mode 100644 index 00000000000..ec3cb24e3d3 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/first_day.rs @@ -0,0 +1,17 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use icu_locid::extensions::unicode; + +enum_keyword!(FirstDay { + "sun" => Sun, + "mon" => Mon, + "tue" => Tue, + "wed" => Wed, + "thu" => Thu, + "fri" => Fri, + "sat" => Sat +}); diff --git a/utils/preferences/src/extensions/unicode/keywords/hour_cycle.rs b/utils/preferences/src/extensions/unicode/keywords/hour_cycle.rs new file mode 100644 index 00000000000..92e14c651bf --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/hour_cycle.rs @@ -0,0 +1,16 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use crate::preferences::PreferenceKey; +use icu_locid::extensions::unicode; + +enum_keyword!(HourCycle, +{ + H11 => "h11", + H12 => "h12", + H23 => "h23", + H24 => "h24" +}, "hc"); diff --git a/utils/preferences/src/extensions/unicode/keywords/line_break.rs b/utils/preferences/src/extensions/unicode/keywords/line_break.rs new file mode 100644 index 00000000000..69ab90ed18b --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/line_break.rs @@ -0,0 +1,14 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::enum_keyword; +use crate::extensions::unicode::errors::Error; +use icu_locid::extensions::unicode; + +enum_keyword!(LineBreak, +{ + Strict => "strict", + Normal => "normal", + Loose => "loose" +}); diff --git a/utils/preferences/src/extensions/unicode/keywords/mod.rs b/utils/preferences/src/extensions/unicode/keywords/mod.rs new file mode 100644 index 00000000000..4d4a8299507 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/mod.rs @@ -0,0 +1,133 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::extensions::unicode::errors::Error; +use crate::preferences::PreferenceKey; +use icu_locid::extensions::unicode; + +mod calendar; +mod collation; +mod currency; +mod currency_format; +mod dictionary_break; +mod emoji; +mod first_day; +// mod hour_cycle; +// mod line_break; +// mod numbering_system; + +pub use calendar::*; +pub use collation::*; +pub use currency::*; +pub use currency_format::*; +pub use dictionary_break::*; +pub use emoji::*; +pub use first_day::*; +// pub use hour_cycle::*; +// pub use line_break::*; +// pub use numbering_system::*; + +#[non_exhaustive] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Keyword { + Calendar(Calendar), + CurrencyFormat(CurrencyFormat), + Collation(Collation), + Currency(Currency), + DictionaryBreak(DictionaryBreak), + Emoji(Emoji), + FirstDay(FirstDay), + // HourCycle(HourCycle), + // LineBreak(LineBreak), + + // lw line break word + // ms meaurement system + // mu measurement unit + // NumberingSystem(NumberingSystem), + // rg region override + // sd region subdivision + // ss sentence supression + // tz timezone + // va variant +} + +impl TryFrom<(&unicode::Key, &unicode::Value)> for Keyword { + type Error = Error; + + fn try_from((key, value): (&unicode::Key, &unicode::Value)) -> Result { + match key { + // _ if HourCycle::matches_ue_key(key) => Ok(Self::HourCycle(value.try_into()?)), + _ if Calendar::matches_ue_key(key) => Ok(Self::Calendar(value.try_into()?)), + // _ if NumberingSystem::matches_ue_key(key) => { + // Ok(Self::NumberingSystem(value.try_into()?)) + // } + _ => Err(Error::UnknownKeyword), + } + } +} + +#[macro_export] +macro_rules! enum_keyword { + ($name:ident { + $($key:expr => $variant:ident $(($v2:ident) {$($subk:expr => $subv:ident),*})?),* $(,)? + }) => { + #[non_exhaustive] + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub enum $name { + $($variant + $((Option<$v2>))? + ),* + } + + impl TryFrom<&unicode::Value> for $name { + type Error = Error; + + fn try_from(s: &unicode::Value) -> Result { + Ok(match s { + $( + _ if *s == unicode::value!($key) => $name::$variant $(($v2::try_from(s).ok()))?, + $( + $(_ if *s == unicode::value!($key, $subk) => $name::$variant(Some($v2::$subv))),*, + )? + )* + _ => return Err(Error::UnknownKeywordValue), + }) + } + } + }; + ($name:ident { + $($key:expr => $variant:ident $(($v2:ident) {$($subk:expr => $subv:ident),*})?),* $(,)? + }, $ext_key:literal) => { + + enum_keyword!($name {$($key => $variant $(($v2) {$($subk => $subv),*})?),*}); + + impl PreferenceKey for $name { + fn unicode_extension_key() -> Option { + Some(unicode::key!($ext_key)) + } + } + }; +} + +#[macro_export] +macro_rules! struct_keyword { + ($name:ident, $ext_key:literal, $value:ty, $try_from:tt) => { + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub struct $name($value); + + impl TryFrom<&unicode::Value> for $name { + type Error = Error; + + fn try_from(i: &unicode::Value) -> Result { + $try_from(i) + } + } + + impl PreferenceKey for $name { + fn unicode_extension_key() -> Option { + Some(unicode::key!($ext_key)) + } + } + }; +} diff --git a/utils/preferences/src/extensions/unicode/keywords/numbering_system.rs b/utils/preferences/src/extensions/unicode/keywords/numbering_system.rs new file mode 100644 index 00000000000..507cc07acd9 --- /dev/null +++ b/utils/preferences/src/extensions/unicode/keywords/numbering_system.rs @@ -0,0 +1,33 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::extensions::unicode::errors::Error; +use crate::preferences::PreferenceKey; +use icu_locid::extensions::unicode; +use tinystr::TinyAsciiStr; + +// XXX: Associated types for `NumberingSystem::Latn` for known ones + `::Custom("dd")` +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NumberingSystem(pub TinyAsciiStr<8>); + +impl PreferenceKey for NumberingSystem { + fn unicode_extension_key() -> Option { + Some(unicode::key!("nu")) + } +} + +impl TryFrom<&unicode::Value> for NumberingSystem { + type Error = Error; + + fn try_from(i: &unicode::Value) -> Result { + //XXX: Perf + let s = i.to_string(); + let v = TinyAsciiStr::from_str(&s); + if let Ok(v) = v { + Ok(NumberingSystem(v)) + } else { + Err(Error::UnknownKeywordValue) + } + } +} diff --git a/utils/preferences/src/extensions/unicode/mod.rs b/utils/preferences/src/extensions/unicode/mod.rs new file mode 100644 index 00000000000..50d9a80c90a --- /dev/null +++ b/utils/preferences/src/extensions/unicode/mod.rs @@ -0,0 +1,6 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +pub mod errors; +pub mod keywords; diff --git a/utils/preferences/src/lib.rs b/utils/preferences/src/lib.rs new file mode 100644 index 00000000000..92cf9a6a66f --- /dev/null +++ b/utils/preferences/src/lib.rs @@ -0,0 +1,60 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +//! # `icu_preferences` +//! +//! `icu_preferences` is a utility crate of the [`ICU4X`] project. +//! +//! This API provides necessary functionality for building user preferences structs with ability +//! to `merge` information between the struct and a [`Locale`] and facilitate resolution of the +//! attributes against default values. +//! +//! The crate is intended primarily to be used by components constructors to normalize the format +//! of ingesting preferences across all of [`ICU4X`]. +//! +//! # Examples: +//! +//! ``` +//! use icu_preferences::{ +//! preferences, +//! extensions::unicode::keywords::HourCycle, +//! }; +//! use icu_locid::{LanguageIdentifier, extensions_unicode_key, Locale}; +//! +//! pub fn get_defaults(lid: &Option) -> ExampleComponentResolvedPreferences { +//! unimplemented!() +//! } +//! +//! preferences!( +//! ExampleComponentPreferences, +//! ExampleComponentResolvedPreferences, +//! { +//! hour_cycle => HourCycle +//! } +//! ); +//! +//! pub struct ExampleComponent { +//! resolved_prefs: ExampleComponentResolvedPreferences, +//! } +//! +//! impl ExampleComponent { +//! pub fn new(prefs: ExampleComponentPreferences) -> Self { +//! // Retrieve the default values for the given [`LanguageIdentifier`]. +//! let mut resolved_prefs = get_defaults(&prefs.lid); +//! +//! // Resolve them against provided preferences. +//! resolved_prefs.extend(prefs); +//! +//! Self { resolved_prefs } +//! } +//! } +//! ``` +//! +//! [`ICU4X`]: ../icu/index.html +//! [`Locale`]: icu_locid::Locale +#[macro_use] +pub mod preferences; +#[macro_use] +mod options; +pub mod extensions; diff --git a/utils/preferences/src/options.rs b/utils/preferences/src/options.rs new file mode 100644 index 00000000000..4b1be24083d --- /dev/null +++ b/utils/preferences/src/options.rs @@ -0,0 +1,37 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#[macro_export] +macro_rules! options { + ($name:ident, + $resolved_name:ident, + {$($key:ident => $pref:ty),*} + ) => ( + #[derive(Default, Debug, PartialEq)] + #[non_exhaustive] + pub struct $name { + $( + pub $key: Option<$pref>, + )* + } + + #[non_exhaustive] + #[derive(Debug, PartialEq)] + pub struct $resolved_name { + $( + pub $key: $pref, + )* + } + + impl $name { + pub fn extend(&mut self, other: $name) { + $( + if let Some(value) = other.$key { + self.$key = Some(value); + } + )* + } + } + ) +} diff --git a/utils/preferences/src/preferences.rs b/utils/preferences/src/preferences.rs new file mode 100644 index 00000000000..9a85e48bc41 --- /dev/null +++ b/utils/preferences/src/preferences.rs @@ -0,0 +1,117 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_locid::extensions::unicode::Key; + +pub trait PreferenceKey { + // type UEKey: Option; - doesn't work? + + fn unicode_extension_key() -> Option { + None + } + + fn matches_ue_key(key: &Key) -> bool { + Self::unicode_extension_key() == Some(*key) + } +} + +#[macro_export] +macro_rules! preferences { + ($name:ident, + $resolved_name:ident, + {$($key:ident => $pref:ty),*} + ) => ( + #[derive(Default, Debug)] + #[non_exhaustive] + pub struct $name { + pub lid: Option, + $( + pub $key: Option<$pref>, + )* + } + + #[non_exhaustive] + #[derive(Debug, Clone)] + pub struct $resolved_name { + pub lid: icu_locid::LanguageIdentifier, + + $( + pub $key: $pref, + )* + } + + impl From<(LanguageIdentifier, $resolved_name)> for $resolved_name { + fn from(input: (LanguageIdentifier, $resolved_name)) -> Self { + Self { + lid: input.0, + $( + $key: input.1.$key, + )* + } + } + } + + impl From for $name { + fn from(loc: Locale) -> Self { + use icu_preferences::preferences::PreferenceKey; + + let lid = Some(loc.id); + + $( + let mut $key = None; + )* + + for (k, v) in loc.extensions.unicode.keywords.iter() { + $( + if <$pref>::matches_ue_key(k) { + if let Ok(r) = TryInto::try_into(v) { + $key = Some(r); + } + } + )* + } + + Self { + lid, + $( + $key, + )* + } + } + } + + impl From<&$name> for Locale { + fn from(input: &$name) -> Locale { + todo!() + } + } + + impl From<&$resolved_name> for Locale { + fn from(input: &$resolved_name) -> Locale { + todo!() + } + } + + impl $name { + pub fn extend>(&mut self, other: T) { + let other = other.into(); + $( + if let Some(value) = other.$key { + self.$key = Some(value); + } + )* + } + } + + impl $resolved_name { + pub fn extend(&mut self, prefs: $name) { + $( + if let Some(v) = prefs.$key { + self.$key = v; + } + )* + } + } + ) +} diff --git a/utils/preferences/tests/dtf/data_provider.rs b/utils/preferences/tests/dtf/data_provider.rs new file mode 100644 index 00000000000..67cdf7a5ba2 --- /dev/null +++ b/utils/preferences/tests/dtf/data_provider.rs @@ -0,0 +1,50 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use super::*; +use icu_locid::{langid, LanguageIdentifier}; +use icu_preferences::extensions::unicode::keywords; +use tinystr::tinystr; + +struct DefaultPrefs { + pub und: DateTimeFormatResolvedPreferences, + pub list: &'static [DateTimeFormatResolvedPreferences], +} + +const DEFAULT_PREFS: DefaultPrefs = DefaultPrefs { + und: DateTimeFormatResolvedPreferences { + lid: LanguageIdentifier::UND, + hour_cycle: keywords::HourCycle::H23, + calendar: keywords::Calendar::Gregory, + numbering_system: keywords::NumberingSystem(tinystr!(8, "latn")), + }, + list: &[DateTimeFormatResolvedPreferences { + lid: langid!("en-US"), + hour_cycle: keywords::HourCycle::H12, + calendar: keywords::Calendar::Gregory, + numbering_system: keywords::NumberingSystem(tinystr!(8, "latn")), + }], +}; + +pub fn get_default_prefs(lid: &Option) -> DateTimeFormatResolvedPreferences { + lid.as_ref() + .and_then(|lid| { + DEFAULT_PREFS + .list + .iter() + .find(|dtfrp| dtfrp.lid.language == lid.language) + }) + .cloned() + .unwrap_or(DEFAULT_PREFS.und) +} + +pub fn resolve_options(options: &DateTimeFormatOptions) -> DateTimeFormatResolvedOptions { + DateTimeFormatResolvedOptions { + date_length: options.date_length.unwrap_or(length::Date::Short), + time_length: options.time_length.unwrap_or(length::Time::Short), + day_period: DayPeriod::Short, + locale_matcher: LocaleMatcher::BestFit, + time_zone: options.time_zone.unwrap_or(false), + } +} diff --git a/utils/preferences/tests/dtf/mod.rs b/utils/preferences/tests/dtf/mod.rs new file mode 100644 index 00000000000..4e270d43379 --- /dev/null +++ b/utils/preferences/tests/dtf/mod.rs @@ -0,0 +1,60 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +mod data_provider; +mod options; + +use data_provider::{get_default_prefs, resolve_options}; +use icu_datetime::options::length; +use icu_locid::{LanguageIdentifier, Locale}; +use icu_preferences::{extensions::unicode::keywords, options, preferences}; +use options::{DayPeriod, LocaleMatcher}; + +preferences!( + DateTimeFormatPreferences, + DateTimeFormatResolvedPreferences, + { + hour_cycle => keywords::HourCycle, + calendar => keywords::Calendar, + numbering_system => keywords::NumberingSystem + } +); + +options!( + DateTimeFormatOptions, + DateTimeFormatResolvedOptions, + { + date_length => length::Date, + time_length => length::Time, + day_period => DayPeriod, + locale_matcher => LocaleMatcher, + time_zone => bool + } +); + +pub struct DateTimeFormat { + prefs: DateTimeFormatResolvedPreferences, + options: DateTimeFormatResolvedOptions, +} + +impl DateTimeFormat { + pub fn new(prefs: DateTimeFormatPreferences, options: DateTimeFormatOptions) -> Self { + let mut resolved = get_default_prefs(&prefs.lid); + + resolved.extend(prefs); + + Self { + prefs: resolved, + options: resolve_options(&options), + } + } + + pub fn resolved_preferences(&self) -> &DateTimeFormatResolvedPreferences { + &self.prefs + } + + pub fn resolved_options(&self) -> &DateTimeFormatResolvedOptions { + &self.options + } +} diff --git a/utils/preferences/tests/dtf/options.rs b/utils/preferences/tests/dtf/options.rs new file mode 100644 index 00000000000..0522cbd1195 --- /dev/null +++ b/utils/preferences/tests/dtf/options.rs @@ -0,0 +1,25 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#[derive(Debug, PartialEq)] +pub enum DayPeriod { + Short, +} + +impl Default for DayPeriod { + fn default() -> Self { + Self::Short + } +} + +#[derive(Debug, PartialEq)] +pub enum LocaleMatcher { + BestFit, +} + +impl Default for LocaleMatcher { + fn default() -> Self { + Self::BestFit + } +} diff --git a/utils/preferences/tests/dtf_os_prefs_tests.rs b/utils/preferences/tests/dtf_os_prefs_tests.rs new file mode 100644 index 00000000000..c1acc20637d --- /dev/null +++ b/utils/preferences/tests/dtf_os_prefs_tests.rs @@ -0,0 +1,149 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +mod dtf; + +use icu_locid::{locale, LanguageIdentifier}; +use icu_preferences::extensions::unicode::keywords; + +use dtf::*; + +fn get_os_dtf_preferences(lid: &LanguageIdentifier) -> Option { + Some(dtf::DateTimeFormatPreferences { + lid: Some(lid.clone()), + hour_cycle: Some(keywords::HourCycle::H23), + ..Default::default() + }) +} + +fn get_os_dtf_options() -> Option { + use icu_datetime::options::length; + + Some(dtf::DateTimeFormatOptions { + date_length: Some(length::Date::Long), + time_zone: Some(true), + ..Default::default() + }) +} + +// In this scenario we showcase retrieval of OS regional preferences. +// The result chain is: OS > Locale > Defaults. +#[test] +fn dtf_get_os_prefs() { + let loc = locale!("en-US"); + + let os_prefs = get_os_dtf_preferences(&loc.id); + let mut prefs: DateTimeFormatPreferences = loc.into(); + if let Some(os_prefs) = os_prefs { + prefs.extend(os_prefs); + } + + let dtf = DateTimeFormat::new(prefs, Default::default()); + + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H23 + ); +} + +// In this scenario we showcase retrieval of OS regional preferences. +// The priority is in locale unicode extension overriding OS preferences. +// The result chain is: Locale > OS > Defaults. +#[test] +fn dtf_locale_override_os_prefs() { + let loc = locale!("en-US-u-hc-h11"); + + let os_prefs = get_os_dtf_preferences(&loc.id); + let prefs = if let Some(mut os_prefs) = os_prefs { + os_prefs.extend(loc); + os_prefs + } else { + loc.into() + }; + + let dtf = DateTimeFormat::new(prefs, Default::default()); + + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H11 + ); +} + +// In this scenario we showcase retrieval of OS regional preferences. +// The priority is in OS preferences overriding locale unicode extension. +// The result chain is: OS > Locale > Defaults. +#[test] +fn dtf_os_prefs_override_locale() { + let loc = locale!("en-US-u-hc-h11"); + + let os_prefs = get_os_dtf_preferences(&loc.id); + let mut prefs = DateTimeFormatPreferences::from(loc); + if let Some(os_prefs) = os_prefs { + prefs.extend(os_prefs); + } + + let dtf = DateTimeFormat::new(prefs, Default::default()); + + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H23 + ); +} + +// In this scenario we showcase retrieval of OS regional preferences, +// preferences bag, and locale. +// The result chain is: Bag > Locale > OS > Defaults. +#[test] +fn dtf_call_override_locale_override_os_prefs() { + let loc = locale!("en-US-u-hc-h11"); + + let os_prefs = get_os_dtf_preferences(&loc.id); + let mut prefs = if let Some(mut os_prefs) = os_prefs { + os_prefs.extend(loc); + os_prefs + } else { + loc.into() + }; + + let bag = DateTimeFormatPreferences { + hour_cycle: Some(keywords::HourCycle::H24), + ..Default::default() + }; + + prefs.extend(bag); + + let dtf = DateTimeFormat::new(prefs, Default::default()); + + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H24 + ); +} + +#[test] +fn dtf_options_override_os_options() { + use icu_datetime::options::length; + + let loc = locale!("en"); + + let dev_options = DateTimeFormatOptions { + date_length: Some(length::Date::Medium), + ..Default::default() + }; + + let options = if let Some(mut os_options) = get_os_dtf_options() { + os_options.extend(dev_options); + os_options + } else { + dev_options + }; + + let dtf = DateTimeFormat::new(loc.into(), options); + + // This is taken from dev options + assert_eq!(dtf.resolved_options().date_length, length::Date::Medium); + + // Dev didn't specify time zone field presence, so this is taken from os_prefs + assert!(dtf.resolved_options().time_zone); +} diff --git a/utils/preferences/tests/dtf_tests.rs b/utils/preferences/tests/dtf_tests.rs new file mode 100644 index 00000000000..3d2a46f933f --- /dev/null +++ b/utils/preferences/tests/dtf_tests.rs @@ -0,0 +1,156 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +mod dtf; + +use icu_locid::{ + locale, + subtags::{language, region}, + Locale, +}; +use icu_preferences::extensions::unicode::keywords; + +use dtf::*; + +// In this scenario, the locale is the only source of preferences +// and since it's empty, the defaults for the resolved locale will be taken. +// The result chain is: Defaults. +#[test] +fn dtf_default() { + let loc = locale!("en-US"); + + let dtf = DateTimeFormat::new(loc.into(), Default::default()); + + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H12 + ); +} + +// In this scenario, we resolve the locale, and then apply the regional +// preferences from unicode extensions of the Locale on top of it. +// The result chain is: Locale > Defaults. +#[test] +fn dtf_uext() { + let loc: Locale = "en-US-u-hc-h11".parse().unwrap(); + let dtf = DateTimeFormat::new(loc.into(), Default::default()); + + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H11 + ); +} + +// In this scenario, we will take the preferences bag, and extend +// the preferences from the locale with it. +// The result chain is: Bag > Locale > Defaults. +#[test] +fn dtf_prefs() { + let loc: Locale = "en-US-u-hc-h11".parse().unwrap(); + + let bag = DateTimeFormatPreferences { + hour_cycle: Some(keywords::HourCycle::H24), + ..Default::default() + }; + let mut prefs = DateTimeFormatPreferences::from(loc); + prefs.extend(bag); + + let dtf = DateTimeFormat::new(prefs, Default::default()); + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H24 + ); + assert_eq!( + dtf.resolved_preferences().calendar, + keywords::Calendar::Gregory, + ); +} + +// In this scenario we showcase two preferences in locale extensions, +// and one of them overridden in the preferences bag. +// The result chain is: Bag > Locale > Defaults. +#[test] +fn dtf_prefs_with_ca() { + let loc: Locale = "en-US-u-hc-h11-ca-buddhist".parse().unwrap(); + let bag = DateTimeFormatPreferences { + hour_cycle: Some(keywords::HourCycle::H24), + ..Default::default() + }; + let mut prefs = DateTimeFormatPreferences::from(loc); + prefs.extend(bag); + + let dtf = DateTimeFormat::new(prefs, Default::default()); + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H24 + ); + assert_eq!( + dtf.resolved_preferences().calendar, + keywords::Calendar::Buddhist, + ); +} + +// In this scenario we pass `en` but resolve to `en-US`. +#[test] +fn dtf_prefs_default_region() { + let loc: Locale = "en-u-hc-h12".parse().unwrap(); + let dtf = DateTimeFormat::new(loc.into(), Default::default()); + assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); + assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H12 + ); +} + +#[test] +fn dtf_options_default() { + use icu_datetime::options::length; + + let loc: Locale = "en".parse().unwrap(); + + let options = DateTimeFormatOptions { + ..Default::default() + }; + let dtf = DateTimeFormat::new(loc.into(), options); + assert_eq!(dtf.resolved_options().date_length, length::Date::Short); +} + +#[test] +fn dtf_options_manual() { + use icu_datetime::options::length; + + let loc: Locale = "en".parse().unwrap(); + + let options = DateTimeFormatOptions { + date_length: Some(length::Date::Medium), + ..Default::default() + }; + let dtf = DateTimeFormat::new(loc.into(), options); + assert_eq!(dtf.resolved_options().date_length, length::Date::Medium); +} + +#[test] +fn dtf_prefs_unknown_ue_key() { + let loc: Locale = "en-u-aa-bb".parse().unwrap(); + let dtf = DateTimeFormat::new(loc.into(), Default::default()); + assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); + assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H12 + ); +} + +#[test] +fn dtf_prefs_unknown_ue_value() { + let loc: Locale = "en-u-hc-bb".parse().unwrap(); + let dtf = DateTimeFormat::new(loc.into(), Default::default()); + assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); + assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); + assert_eq!( + dtf.resolved_preferences().hour_cycle, + keywords::HourCycle::H12 + ); +}