diff --git a/core/engine/src/builtins/intl/collator/mod.rs b/core/engine/src/builtins/intl/collator/mod.rs index 4cf8696cc83..f782ae817b5 100644 --- a/core/engine/src/builtins/intl/collator/mod.rs +++ b/core/engine/src/builtins/intl/collator/mod.rs @@ -6,11 +6,7 @@ use icu_collator::{ provider::CollationMetadataV1, }; -use icu_locale::{ - Locale, extensions::unicode, extensions_unicode_key as key, preferences::PreferenceKey, - subtags::subtag, -}; -use icu_provider::DataMarkerAttributes; +use icu_locale::{Locale, extensions::unicode}; use crate::{ Context, JsArgs, JsData, JsNativeError, JsResult, JsString, JsValue, @@ -18,10 +14,7 @@ use crate::{ BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, OrdinaryObject, options::get_option, }, - context::{ - icu::IntlProvider, - intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, - }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, native_function::NativeFunction, object::{ @@ -36,7 +29,7 @@ use crate::{ use super::{ Service, - locale::{canonicalize_locale_list, filter_locales, resolve_locale, validate_extension}, + locale::{canonicalize_locale_list, filter_locales, resolve_locale}, options::{IntlOptions, coerce_options_to_object}, }; @@ -72,95 +65,7 @@ impl Collator { impl Service for Collator { type LangMarker = CollationMetadataV1; - type LocaleOptions = CollatorPreferences; - - fn resolve(locale: &mut Locale, options: &mut Self::LocaleOptions, provider: &IntlProvider) { - let mut locale_preferences = CollatorPreferences::from(&*locale); - locale_preferences.collation_type = locale_preferences.collation_type.take().filter(|co| { - let attr = DataMarkerAttributes::from_str_or_panic(co.as_str()); - co != &CollationType::Search - && validate_extension::(locale.id.clone(), attr, provider) - }); - locale.extensions.unicode.clear(); - - options.locale_preferences = (&*locale).into(); - - options.collation_type = options - .collation_type - .take() - .filter(|co| { - let attr = DataMarkerAttributes::from_str_or_panic(co.as_str()); - co != &CollationType::Search - && validate_extension::(locale.id.clone(), attr, provider) - }) - .inspect(|co| { - if Some(co) == locale_preferences.collation_type.as_ref() - && let Some(co) = co.unicode_extension_value() - { - locale.extensions.unicode.keywords.set(key!("co"), co); - } - }) - .or_else(|| { - if let Some(co) = locale_preferences - .collation_type - .as_ref() - .and_then(CollationType::unicode_extension_value) - { - locale.extensions.unicode.keywords.set(key!("co"), co); - } - locale_preferences.collation_type - }); - - options.numeric_ordering = options - .numeric_ordering - .take() - .inspect(|kn| { - if Some(kn) == locale_preferences.numeric_ordering.as_ref() - && let Some(mut kn) = kn.unicode_extension_value() - { - if kn.as_single_subtag() == Some(&subtag!("true")) { - kn = unicode::Value::new_empty(); - } - locale.extensions.unicode.keywords.set(key!("kn"), kn); - } - }) - .or_else(|| { - if let Some(mut kn) = locale_preferences - .numeric_ordering - .as_ref() - .and_then(CollationNumericOrdering::unicode_extension_value) - { - if kn.as_single_subtag() == Some(&subtag!("true")) { - kn = unicode::Value::new_empty(); - } - locale.extensions.unicode.keywords.set(key!("kn"), kn); - } - - locale_preferences.numeric_ordering - }); - - options.case_first = options - .case_first - .take() - .inspect(|kf| { - if Some(kf) == locale_preferences.case_first.as_ref() - && let Some(kn) = kf.unicode_extension_value() - { - locale.extensions.unicode.keywords.set(key!("kf"), kn); - } - }) - .or_else(|| { - if let Some(kf) = locale_preferences - .case_first - .as_ref() - .and_then(CollationCaseFirst::unicode_extension_value) - { - locale.extensions.unicode.keywords.set(key!("kf"), kf); - } - - locale_preferences.case_first - }); - } + type Preferences = CollatorPreferences; } impl IntrinsicObject for Collator { @@ -285,7 +190,7 @@ impl BuiltInConstructor for Collator { let mut intl_options = IntlOptions { matcher, - service_options: { + preferences: { let mut prefs = CollatorPreferences::default(); prefs.collation_type = collation; prefs.numeric_ordering = numeric.map(|kn| { @@ -312,16 +217,16 @@ impl BuiltInConstructor for Collator { // 21. Let collation be r.[[co]]. // 22. If collation is null, let collation be "default". // 23. Set collator.[[Collation]] to collation. - let collation = intl_options.service_options.collation_type; + let collation = intl_options.preferences.collation_type; // 24. If relevantExtensionKeys contains "kn", then // a. Set collator.[[Numeric]] to SameValue(r.[[kn]], "true"). let numeric = - intl_options.service_options.numeric_ordering == Some(CollationNumericOrdering::True); + intl_options.preferences.numeric_ordering == Some(CollationNumericOrdering::True); // 25. If relevantExtensionKeys contains "kf", then // a. Set collator.[[CaseFirst]] to r.[[kf]]. - let case_first = intl_options.service_options.case_first; + let case_first = intl_options.preferences.case_first; // 26. Let sensitivity be ? GetOption(options, "sensitivity", string, « "base", "accent", "case", "variant" », undefined). // 28. Set collator.[[Sensitivity]] to sensitivity. @@ -354,12 +259,12 @@ impl BuiltInConstructor for Collator { options.max_variable = max_variable; if usage == Usage::Search { - intl_options.service_options.collation_type = Some(CollationType::Search); + intl_options.preferences.collation_type = Some(CollationType::Search); } let collator = icu_collator::Collator::try_new_with_buffer_provider( context.intl_provider().erased_provider(), - intl_options.service_options, + intl_options.preferences, options, ) .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; diff --git a/core/engine/src/builtins/intl/collator/options.rs b/core/engine/src/builtins/intl/collator/options.rs index c1c552ce8f8..b725e61ec87 100644 --- a/core/engine/src/builtins/intl/collator/options.rs +++ b/core/engine/src/builtins/intl/collator/options.rs @@ -1,13 +1,24 @@ use std::str::FromStr; use icu_collator::{ + CollatorPreferences, options::{CaseLevel, Strength}, - preferences::CollationCaseFirst, + preferences::{CollationCaseFirst, CollationType}, + provider::CollationMetadataV1, +}; +use icu_locale::{LanguageIdentifier, preferences::PreferenceKey}; +use icu_provider::{ + DataMarkerAttributes, + prelude::icu_locale_core::{extensions::unicode, preferences::LocalePreferences}, }; use crate::{ Context, JsNativeError, JsResult, JsValue, - builtins::options::{OptionType, ParsableOptionType}, + builtins::{ + intl::{ServicePreferences, locale::validate_extension}, + options::{OptionType, ParsableOptionType}, + }, + context::icu::IntlProvider, }; #[derive(Debug, Clone, Copy)] @@ -97,3 +108,60 @@ impl OptionType for CollationCaseFirst { } } } + +impl ServicePreferences for CollatorPreferences { + fn validate(&mut self, id: &LanguageIdentifier, provider: &IntlProvider) { + self.collation_type = self.collation_type.take().filter(|co| { + let attr = DataMarkerAttributes::from_str_or_panic(co.as_str()); + co != &CollationType::Search + && validate_extension::(id, attr, provider) + }); + } + + fn as_unicode(&self) -> unicode::Unicode { + let mut exts = unicode::Unicode::new(); + + if let Some(co) = self.collation_type + && let Some(value) = co.unicode_extension_value() + { + exts.keywords.set(unicode::key!("co"), value); + } + + if let Some(kn) = self.numeric_ordering + && let Some(value) = kn.unicode_extension_value() + { + exts.keywords.set(unicode::key!("kn"), value); + } + + if let Some(kf) = self.case_first + && let Some(value) = kf.unicode_extension_value() + { + exts.keywords.set(unicode::key!("kf"), value); + } + + exts + } + + fn extended(&self, other: &Self) -> Self { + let mut result = *self; + result.extend(*other); + result + } + + fn intersection(&self, other: &Self) -> Self { + let mut inter = *self; + if inter.locale_preferences != other.locale_preferences { + inter.locale_preferences = LocalePreferences::default(); + } + if inter.collation_type != other.collation_type { + inter.collation_type.take(); + } + if inter.case_first != other.case_first { + inter.case_first.take(); + } + if inter.numeric_ordering != other.numeric_ordering { + inter.numeric_ordering.take(); + } + inter + } +} diff --git a/core/engine/src/builtins/intl/date_time_format/mod.rs b/core/engine/src/builtins/intl/date_time_format/mod.rs index 6fdaebe3a3e..733c4bd9011 100644 --- a/core/engine/src/builtins/intl/date_time_format/mod.rs +++ b/core/engine/src/builtins/intl/date_time_format/mod.rs @@ -18,15 +18,12 @@ use crate::{ intl::{ Service, date_time_format::options::{DateStyle, FormatMatcher, FormatOptions, TimeStyle}, - locale::{canonicalize_locale_list, resolve_locale, validate_extension}, + locale::{canonicalize_locale_list, resolve_locale}, options::{IntlOptions, coerce_options_to_object}, }, options::get_option, }, - context::{ - icu::IntlProvider, - intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, - }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, js_error, js_string, object::{ @@ -52,10 +49,7 @@ use icu_datetime::{ }; use icu_decimal::preferences::NumberingSystem; use icu_decimal::provider::DecimalSymbolsV1; -use icu_locale::{ - Locale, extensions::unicode::Value, extensions_unicode_key as key, preferences::PreferenceKey, -}; -use icu_provider::DataMarkerAttributes; +use icu_locale::{Locale, extensions::unicode::Value}; use icu_time::{ TimeZoneInfo, ZonedDateTime, zone::{IanaParser, models::Base}, @@ -96,101 +90,7 @@ pub(crate) struct DateTimeFormat { impl Service for DateTimeFormat { type LangMarker = DecimalSymbolsV1; - type LocaleOptions = DateTimeFormatterPreferences; - - fn resolve(locale: &mut Locale, options: &mut Self::LocaleOptions, provider: &IntlProvider) { - let locale_preferences = DateTimeFormatterPreferences::from(&*locale); - // TODO: Determine if any locale_preferences processing is needed here. - - options.locale_preferences = (&*locale).into(); - - // The below handles the [[RelevantExtensionKeys]] of DateTimeFormatters - // internal slots. - // - // See https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots - - // Handle LDML unicode key "ca", Calendar algorithm - options.calendar_algorithm = options - .calendar_algorithm - .take() - .filter(|ca| { - let attr = DataMarkerAttributes::from_str_or_panic(ca.as_str()); - validate_extension::(locale.id.clone(), attr, provider) - }) - .inspect(|ca| { - if Some(ca) == locale_preferences.calendar_algorithm.as_ref() - && let Some(ca) = ca.unicode_extension_value() - { - locale.extensions.unicode.keywords.set(key!("ca"), ca); - } - }) - .or_else(|| { - if let Some(ca) = locale_preferences - .calendar_algorithm - .as_ref() - .and_then(CalendarAlgorithm::unicode_extension_value) - { - locale.extensions.unicode.keywords.set(key!("ca"), ca); - } - locale_preferences.calendar_algorithm - }); - - // Handle LDML unicode key "nu", Numbering system - options.numbering_system = options - .numbering_system - .take() - .filter(|nu| { - let attr = DataMarkerAttributes::from_str_or_panic(nu.as_str()); - validate_extension::(locale.id.clone(), attr, provider) - }) - .inspect(|nu| { - if Some(nu) == locale_preferences.numbering_system.as_ref() - && let Some(nu) = nu.unicode_extension_value() - { - locale.extensions.unicode.keywords.set(key!("nu"), nu); - } - }) - .or_else(|| { - if let Some(nu) = locale_preferences - .numbering_system - .as_ref() - .and_then(NumberingSystem::unicode_extension_value) - { - locale.extensions.unicode.keywords.set(key!("nu"), nu); - } - locale_preferences.numbering_system - }); - - // NOTE (nekevss): issue: this will not support `H24` as ICU4X does - // not currently support it. - // - // track: https://github.com/unicode-org/icu4x/issues/6597 - // Handle LDML unicode key "hc", Hour cycle - options.hour_cycle = options - .hour_cycle - .take() - .filter(|hc| { - let attr = DataMarkerAttributes::from_str_or_panic(hc.as_str()); - validate_extension::(locale.id.clone(), attr, provider) - }) - .inspect(|hc| { - if Some(hc) == locale_preferences.hour_cycle.as_ref() - && let Some(hc) = hc.unicode_extension_value() - { - locale.extensions.unicode.keywords.set(key!("hc"), hc); - } - }) - .or_else(|| { - if let Some(hc) = locale_preferences - .hour_cycle - .as_ref() - .and_then(IcuHourCycle::unicode_extension_value) - { - locale.extensions.unicode.keywords.set(key!("hc"), hc); - } - locale_preferences.hour_cycle - }); - } + type Preferences = DateTimeFormatterPreferences; } impl IntrinsicObject for DateTimeFormat { @@ -370,7 +270,7 @@ impl DateTimeFormat { ) .map_err(|e| { JsNativeError::range() - .with_message(format!("Failed to load formatter: {e}")) + .with_message(format!("failed to load formatter: {e}")) })?; let dt = fields.to_formattable_datetime(); @@ -508,7 +408,7 @@ fn create_date_time_format( // NOTE: We unroll the below const loop in step 6 using the // ResolutionOptionDescriptors from the internal slots // https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots - let mut service_options = DateTimeFormatterPreferences::default(); + let mut preferences = DateTimeFormatterPreferences::default(); // 6. For each Resolution Option Descriptor desc of constructor.[[ResolutionOptionDescriptors]], do // a. If desc has a [[Type]] field, let type be desc.[[Type]]. Otherwise, let type be string. @@ -521,14 +421,14 @@ fn create_date_time_format( // f. Set opt.[[]] to value. // Handle { [[Key]]: "ca", [[Property]]: "calendar" } - service_options.calendar_algorithm = + preferences.calendar_algorithm = get_option::(&options, js_string!("calendar"), context)? .map(|ca| CalendarAlgorithm::try_from(&ca)) .transpose() .map_err(|_icu4x_error| js_error!(RangeError: "unknown calendar algorithm"))?; // { [[Key]]: "nu", [[Property]]: "numberingSystem" } - service_options.numbering_system = + preferences.numbering_system = get_option::(&options, js_string!("numberingSystem"), context)? .map(NumberingSystem::try_from) .transpose() @@ -538,7 +438,7 @@ fn create_date_time_format( let hour_12 = get_option::(&options, js_string!("hour12"), context)?; // { [[Key]]: "hc", [[Property]]: "hourCycle", [[Values]]: « "h11", "h12", "h23", "h24" » } - service_options.hour_cycle = + preferences.hour_cycle = get_option::(&options, js_string!("hourCycle"), context)? .map(|hc| { // Handle steps 3.a-c here @@ -555,7 +455,7 @@ fn create_date_time_format( let mut intl_options = IntlOptions { matcher, - service_options, + preferences, }; // ResolveOptions 8. Let resolution be ResolveLocale(constructor.[[AvailableLocales]], requestedLocales, @@ -646,8 +546,7 @@ fn create_date_time_format( // d. Set formatOptions.[[]] to value. // e. If value is not undefined, then // i. Set hasExplicitFormatComponents to true. - let mut format_options = - FormatOptions::try_init(&options, service_options.hour_cycle, context)?; + let mut format_options = FormatOptions::try_init(&options, preferences.hour_cycle, context)?; // TODO: how should formatMatcher be used? // 25. Let formatMatcher be ? GetOption(options, "formatMatcher", string, « "basic", "best fit" », "best fit"). @@ -731,7 +630,7 @@ fn create_date_time_format( prototype, DateTimeFormat { locale: resolved_locale, - _calendar_algorithm: intl_options.service_options.calendar_algorithm, + _calendar_algorithm: intl_options.preferences.calendar_algorithm, time_zone, fieldset, bound_format: None, diff --git a/core/engine/src/builtins/intl/date_time_format/options.rs b/core/engine/src/builtins/intl/date_time_format/options.rs index 9394394c209..802917b0f04 100644 --- a/core/engine/src/builtins/intl/date_time_format/options.rs +++ b/core/engine/src/builtins/intl/date_time_format/options.rs @@ -3,18 +3,30 @@ use crate::{ Context, JsError, JsNativeError, JsObject, JsResult, JsValue, builtins::{ - intl::{date_time_format::FormatType, options::get_number_option}, + intl::{ + ServicePreferences, date_time_format::FormatType, locale::validate_extension, + options::get_number_option, + }, options::{OptionType, get_option}, }, + context::icu::IntlProvider, js_error, js_string, }; use icu_datetime::{ + DateTimeFormatterPreferences, fieldsets::builder::{DateFields, ZoneStyle}, options::{Length, SubsecondDigits as IcuSubsecondDigits, TimePrecision}, preferences::{CalendarAlgorithm, HourCycle as IcuHourCycle}, }; -use icu_locale::extensions::unicode::Value; +use icu_decimal::provider::DecimalSymbolsV1; +use icu_locale::{extensions::unicode::Value, preferences::PreferenceKey}; +use icu_provider::{ + DataMarkerAttributes, + prelude::icu_locale_core::{ + LanguageIdentifier, extensions::unicode, preferences::LocalePreferences, + }, +}; pub(crate) enum HourCycle { H11, @@ -567,3 +579,75 @@ impl TimeZoneName { } } } + +// The below handles the [[RelevantExtensionKeys]] of DateTimeFormatters +// internal slots. +// +// See https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots +impl ServicePreferences for DateTimeFormatterPreferences { + fn validate(&mut self, id: &LanguageIdentifier, provider: &IntlProvider) { + // Handle LDML unicode key "nu", Numbering system + self.numbering_system = self.numbering_system.take().filter(|nu| { + let attr = DataMarkerAttributes::from_str_or_panic(nu.as_str()); + validate_extension::(id, attr, provider) + }); + + // Handle LDML unicode key "ca", Calendar algorithm + // TODO: determine the correct way to verify the calendar algorithm data. + + // NOTE (nekevss): issue: this will not support `H24` as ICU4X does + // not currently support it. + // + // track: https://github.com/unicode-org/icu4x/issues/6597 + // Handle LDML unicode key "hc", Hour cycle + // No need to validate hour_cycle since it only affects formatting + // behaviour. + } + + fn as_unicode(&self) -> unicode::Unicode { + let mut exts = unicode::Unicode::new(); + + if let Some(nu) = self.numbering_system + && let Some(value) = nu.unicode_extension_value() + { + exts.keywords.set(unicode::key!("nu"), value); + } + + if let Some(ca) = self.calendar_algorithm + && let Some(value) = ca.unicode_extension_value() + { + exts.keywords.set(unicode::key!("ca"), value); + } + + if let Some(hc) = self.hour_cycle + && let Some(value) = hc.unicode_extension_value() + { + exts.keywords.set(unicode::key!("hc"), value); + } + + exts + } + + fn extended(&self, other: &Self) -> Self { + let mut result = *self; + result.extend(*other); + result + } + + fn intersection(&self, other: &Self) -> Self { + let mut inter = *self; + if inter.locale_preferences != other.locale_preferences { + inter.locale_preferences = LocalePreferences::default(); + } + if inter.numbering_system != other.numbering_system { + inter.numbering_system.take(); + } + if inter.calendar_algorithm != other.calendar_algorithm { + inter.calendar_algorithm.take(); + } + if inter.hour_cycle != other.hour_cycle { + inter.hour_cycle.take(); + } + inter + } +} diff --git a/core/engine/src/builtins/intl/list_format/mod.rs b/core/engine/src/builtins/intl/list_format/mod.rs index 5ac0c952b20..cd32657e56c 100644 --- a/core/engine/src/builtins/intl/list_format/mod.rs +++ b/core/engine/src/builtins/intl/list_format/mod.rs @@ -12,6 +12,7 @@ use crate::{ Context, JsArgs, JsData, JsNativeError, JsResult, JsString, JsValue, builtins::{ Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, OrdinaryObject, + intl::options::EmptyPreferences, iterable::IteratorHint, options::{get_option, get_options_object}, }, @@ -48,7 +49,7 @@ impl Service for ListFormat { const ATTRIBUTES: &'static icu_provider::DataMarkerAttributes = ListFormatterPatterns::WIDE; - type LocaleOptions = (); + type Preferences = EmptyPreferences; } impl IntrinsicObject for ListFormat { diff --git a/core/engine/src/builtins/intl/locale/tests.rs b/core/engine/src/builtins/intl/locale/tests.rs index c9c280f581a..22606661165 100644 --- a/core/engine/src/builtins/intl/locale/tests.rs +++ b/core/engine/src/builtins/intl/locale/tests.rs @@ -1,60 +1,91 @@ use icu_decimal::provider::DecimalSymbolsV1; use icu_locale::{ - Locale, extensions::unicode::Value, extensions_unicode_key as key, - extensions_unicode_value as value, locale, + Locale, extensions_unicode_key as key, extensions_unicode_value as value, locale, preferences::extensions::unicode::keywords::NumberingSystem, }; use icu_plurals::provider::PluralsCardinalV1; use icu_provider::{ DataIdentifierBorrowed, DataLocale, DataProvider, DataRequest, DataRequestMetadata, DryDataProvider, + prelude::icu_locale_core::{LanguageIdentifier, extensions::unicode}, }; use crate::{ builtins::intl::{ - Service, + Service, ServicePreferences, locale::{default_locale, resolve_locale}, options::{IntlOptions, LocaleMatcher}, }, context::icu::IntlProvider, }; -#[derive(Debug)] -struct TestOptions { +#[derive(Debug, Copy, Clone)] +struct TestPreferences { nu: Option, } +impl From<&Locale> for TestPreferences { + fn from(value: &Locale) -> Self { + Self { + nu: value + .extensions + .unicode + .keywords + .get(&unicode::key!("nu")) + .and_then(|nu| NumberingSystem::try_from(nu.clone()).ok()), + } + } +} + +impl ServicePreferences for TestPreferences { + fn validate(&mut self, id: &LanguageIdentifier, provider: &IntlProvider) { + if self.nu.is_some() { + return; + } + + let locale = &DataLocale::from(id); + let req = DataRequest { + id: DataIdentifierBorrowed::for_locale(locale), + metadata: DataRequestMetadata::default(), + }; + let data = DataProvider::::load(provider, req).unwrap(); + let preferred = data.payload.get().numsys(); + self.nu = Some( + NumberingSystem::try_from(unicode::Value::try_from_str(preferred).unwrap()).unwrap(), + ); + } + + fn as_unicode(&self) -> unicode::Unicode { + let mut exts = unicode::Unicode::new(); + if let Some(nu) = self.nu { + exts.keywords.set(unicode::key!("nu"), nu.into()); + } + exts + } + + fn extended(&self, other: &Self) -> Self { + let mut result = *self; + if result.nu.is_none() { + result.nu = other.nu; + } + result + } + + fn intersection(&self, other: &Self) -> Self { + let mut inter = *self; + if inter.nu != other.nu { + inter.nu.take(); + } + inter + } +} + struct TestService; impl Service for TestService { type LangMarker = PluralsCardinalV1; - type LocaleOptions = TestOptions; - - fn resolve(locale: &mut Locale, options: &mut Self::LocaleOptions, provider: &IntlProvider) { - let loc_hc = locale - .extensions - .unicode - .keywords - .get(&key!("nu")) - .and_then(|v| NumberingSystem::try_from(v.clone()).ok()); - let nu = options.nu.or(loc_hc).unwrap_or_else(|| { - let locale = &DataLocale::from(&*locale); - let req = DataRequest { - id: DataIdentifierBorrowed::for_locale(locale), - metadata: DataRequestMetadata::default(), - }; - let data = DataProvider::::load(provider, req).unwrap(); - let preferred = data.payload.get().numsys(); - NumberingSystem::try_from(Value::try_from_str(preferred).unwrap()).unwrap() - }); - locale - .extensions - .unicode - .keywords - .set(key!("nu"), nu.into()); - options.nu = Some(nu); - } + type Preferences = TestPreferences; } #[test] @@ -85,7 +116,7 @@ fn locale_resolution() { // test lookup let mut options = IntlOptions { matcher: LocaleMatcher::Lookup, - service_options: TestOptions { + preferences: TestPreferences { nu: Some(NumberingSystem::try_from(value!("latn")).unwrap()), }, }; @@ -95,7 +126,7 @@ fn locale_resolution() { // test best fit let mut options = IntlOptions { matcher: LocaleMatcher::BestFit, - service_options: TestOptions { + preferences: TestPreferences { nu: Some(NumberingSystem::try_from(value!("latn")).unwrap()), }, }; @@ -106,7 +137,7 @@ fn locale_resolution() { // requested: [es-ES] let mut options = IntlOptions { matcher: LocaleMatcher::Lookup, - service_options: TestOptions { nu: None }, + preferences: TestPreferences { nu: None }, }; let locale = diff --git a/core/engine/src/builtins/intl/locale/utils.rs b/core/engine/src/builtins/intl/locale/utils.rs index a4a3e5f8272..bfd3f0e53aa 100644 --- a/core/engine/src/builtins/intl/locale/utils.rs +++ b/core/engine/src/builtins/intl/locale/utils.rs @@ -3,7 +3,7 @@ use crate::{ builtins::{ Array, intl::{ - Service, + Service, ServicePreferences, options::{IntlOptions, LocaleMatcher, coerce_options_to_object}, }, options::get_option, @@ -311,7 +311,7 @@ where /// [spec]: https://tc39.es/ecma402/#sec-resolvelocale pub(in crate::builtins::intl) fn resolve_locale( requested_locales: impl IntoIterator, - options: &mut IntlOptions, + options: &mut IntlOptions, provider: &IntlProvider, ) -> JsResult where @@ -390,11 +390,29 @@ where // a. Let foundLocale be InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension). // 11. Set result.[[locale]] to foundLocale. - // 12. Return result. - S::resolve(&mut found_locale, &mut options.service_options, provider); + // This is basically an adaptation of the process above, which + // ensures: + // - All options provided by the locale are valid. + // - All options provided by args are valid. + // - Options provided by args are extended (but not overridden) by + // options provided in the locale. + // - Only the locale options that extended the args options are + // added to the final locale. provider .locale_canonicalizer()? .canonicalize(&mut found_locale); + let mut locale_prefs = S::Preferences::from(&found_locale); + + options.preferences.validate(&found_locale.id, provider); + locale_prefs.validate(&found_locale.id, provider); + + // This should not touch the found locale. + let prefs = locale_prefs.extended(&options.preferences); + + found_locale.extensions.unicode = prefs.intersection(&locale_prefs).as_unicode(); + options.preferences = prefs; + + // 12. Return result. Ok(found_locale) } @@ -465,7 +483,7 @@ where /// /// Calling this function with a singleton `DataMarker` will always return `None`. pub(in crate::builtins::intl) fn validate_extension( - language: LanguageIdentifier, + language: &LanguageIdentifier, attributes: &DataMarkerAttributes, provider: &impl DryDataProvider, ) -> bool { @@ -491,13 +509,14 @@ mod tests { impl Service for TestService { type LangMarker = PluralsCardinalV1; - type LocaleOptions = (); + type Preferences = EmptyPreferences; } use crate::{ builtins::intl::{ Service, locale::utils::{lookup_matching_locale_by_best_fit, lookup_matching_locale_by_prefix}, + options::EmptyPreferences, }, context::icu::IntlProvider, }; diff --git a/core/engine/src/builtins/intl/mod.rs b/core/engine/src/builtins/intl/mod.rs index 8d9d4269bce..0d9c4bfc58a 100644 --- a/core/engine/src/builtins/intl/mod.rs +++ b/core/engine/src/builtins/intl/mod.rs @@ -26,6 +26,7 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; +use icu_locale::{LanguageIdentifier, extensions::unicode}; use icu_provider::{DataMarker, DataMarkerAttributes}; use static_assertions::const_assert; @@ -178,37 +179,36 @@ impl Intl { } } +/// A set of preferences that can be provided to a [`Service`] through +/// a locale. +trait ServicePreferences: for<'a> From<&'a icu_locale::Locale> + Clone { + /// Validates that every preference value is available. + /// + /// This usually entails having to query the `IntlProvider` to check + /// if it has the required data to support the requested values. + fn validate(&mut self, id: &LanguageIdentifier, provider: &IntlProvider); + + /// Converts this set of preferences into a Unicode locale extension. + fn as_unicode(&self) -> unicode::Unicode; + + /// Extends all values set in `self` with the values set in `other`. + fn extended(&self, other: &Self) -> Self; + + /// Gets the set of preference values that are the same in `self` and `other`. + fn intersection(&self, other: &Self) -> Self; +} + /// A service component that is part of the `Intl` API. /// /// This needs to be implemented for every `Intl` service in order to use the functions -/// defined in `locale::utils`, such as locale resolution and selection. +/// defined in `locale::utils`, such as [`resolve_locale`][locale::resolve_locale]. trait Service { - /// The data marker used by [`resolve_locale`][locale::resolve_locale] to decide - /// which locales are supported by this service. + /// The data marker used to decide which locales are supported by this service. type LangMarker: DataMarker; /// The attributes used to resolve the locale. const ATTRIBUTES: &'static DataMarkerAttributes = DataMarkerAttributes::empty(); - /// The set of options used in the [`Service::resolve`] method to resolve the provided - /// locale. - type LocaleOptions; - - /// Resolves the final value of `locale` from a set of `options`. - /// - /// The provided `options` will also be modified with the final values, in case there were - /// changes in the resolution algorithm. - /// - /// # Note - /// - /// - A correct implementation must ensure `locale` and `options` are both written with the - /// new final values. - /// - If the implementor service doesn't contain any `[[RelevantExtensionKeys]]`, this can be - /// skipped. - fn resolve( - _locale: &mut icu_locale::Locale, - _options: &mut Self::LocaleOptions, - _provider: &IntlProvider, - ) { - } + /// The set of preferences used to resolve the provided locale. + type Preferences: ServicePreferences; } diff --git a/core/engine/src/builtins/intl/number_format/mod.rs b/core/engine/src/builtins/intl/number_format/mod.rs index e442923c5c3..0449790d7be 100644 --- a/core/engine/src/builtins/intl/number_format/mod.rs +++ b/core/engine/src/builtins/intl/number_format/mod.rs @@ -1,28 +1,26 @@ +use std::cell::Cell; + use boa_gc::{Finalize, Trace}; use fixed_decimal::{Decimal, FloatPrecision, SignDisplay}; use icu_decimal::{ - DecimalFormatter, FormattedDecimal, + DecimalFormatter, DecimalFormatterPreferences, FormattedDecimal, options::{DecimalFormatterOptions, GroupingStrategy}, preferences::NumberingSystem, - provider::DecimalSymbolsV1, + provider::{DecimalDigitsV1, DecimalSymbolsV1}, }; mod options; -use icu_locale::{ - Locale, - extensions::unicode::{Value, key}, -}; -use icu_provider::DataMarkerAttributes; +use icu_locale::{Locale, extensions::unicode::Value}; +use icu_provider::{DataMarker, DataMarkerAttributes, DynamicDataProvider, buf::BufferMarker}; use num_bigint::BigInt; use num_traits::Num; pub(crate) use options::*; use super::{ Service, - locale::{canonicalize_locale_list, filter_locales, resolve_locale, validate_extension}, + locale::{canonicalize_locale_list, filter_locales, resolve_locale}, options::{IntlOptions, coerce_options_to_object}, }; -use crate::value::JsVariant; use crate::{ Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, NativeFunction, @@ -41,6 +39,7 @@ use crate::{ string::StaticJsStrings, value::PreferredType, }; +use crate::{js_error, value::JsVariant}; #[cfg(test)] mod tests; @@ -51,7 +50,7 @@ mod tests; pub(crate) struct NumberFormat { locale: Locale, formatter: DecimalFormatter, - numbering_system: Option, + numbering_system: NumberingSystem, unit_options: UnitFormatOptions, digit_options: DigitFormatOptions, notation: Notation, @@ -79,57 +78,10 @@ impl NumberFormat { } } -#[derive(Debug, Clone)] -pub(super) struct NumberFormatLocaleOptions { - numbering_system: Option, -} - impl Service for NumberFormat { type LangMarker = DecimalSymbolsV1; - type LocaleOptions = NumberFormatLocaleOptions; - - fn resolve( - locale: &mut Locale, - options: &mut Self::LocaleOptions, - provider: &crate::context::icu::IntlProvider, - ) { - let numbering_system = options - .numbering_system - .take() - .filter(|nu| { - NumberingSystem::try_from(nu.clone()).is_ok_and(|nu| { - let attr = DataMarkerAttributes::from_str_or_panic(nu.as_str()); - validate_extension::(locale.id.clone(), attr, provider) - }) - }) - .or_else(|| { - locale - .extensions - .unicode - .keywords - .get(&key!("nu")) - .cloned() - .filter(|nu| { - NumberingSystem::try_from(nu.clone()).is_ok_and(|nu| { - let attr = DataMarkerAttributes::from_str_or_panic(nu.as_str()); - validate_extension::( - locale.id.clone(), - attr, - provider, - ) - }) - }) - }); - - locale.extensions.unicode.clear(); - - if let Some(nu) = numbering_system.clone() { - locale.extensions.unicode.keywords.set(key!("nu"), nu); - } - - options.numbering_system = numbering_system; - } + type Preferences = DecimalFormatterPreferences; } impl IntrinsicObject for NumberFormat { @@ -301,8 +253,10 @@ impl NumberFormat { let mut intl_options = IntlOptions { matcher, - service_options: NumberFormatLocaleOptions { - numbering_system: numbering_system.map(Value::from), + preferences: { + let mut prefs = DecimalFormatterPreferences::default(); + prefs.numbering_system = numbering_system; + prefs }, }; @@ -440,16 +394,53 @@ impl NumberFormat { let mut options = DecimalFormatterOptions::default(); options.grouping_strategy = Some(use_grouping); - let formatter = DecimalFormatter::try_new_with_buffer_provider( - context.intl_provider().erased_provider(), - (&locale).into(), - options, - ) - .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; + let (formatter, numbering_system) = { + struct RequestInspector<'a> { + inner: &'a dyn DynamicDataProvider, + nu: Cell>>, + } + impl DynamicDataProvider for RequestInspector<'_> { + fn load_data( + &self, + marker: icu_provider::DataMarkerInfo, + req: icu_provider::DataRequest<'_>, + ) -> Result, icu_provider::DataError> + { + if marker.id == DecimalDigitsV1::INFO.id { + self.nu.set(Some(req.id.marker_attributes.to_owned())); + } + self.inner.load_data(marker, req) + } + } + + let inspector = RequestInspector { + inner: context.intl_provider().erased_provider(), + nu: Cell::new(None), + }; + let formatter = DecimalFormatter::try_new_with_buffer_provider( + &inspector, + (&locale).into(), + options, + ) + .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; + + let nu = (|| { + let nu = inspector.nu.into_inner()?; + let nu = Value::try_from_str(&nu).ok()?; + NumberingSystem::try_from(nu).ok() + })() + .ok_or_else(|| { + js_error!( + TypeError: "could not obtain resolved numbering system from Intl provider" + ) + })?; + + (formatter, nu) + }; Ok(NumberFormat { locale, - numbering_system: intl_options.service_options.numbering_system, + numbering_system, formatter, unit_options, digit_options, @@ -573,13 +564,11 @@ impl NumberFormat { js_string!(nf.locale.to_string()), Attribute::all(), ); - if let Some(nu) = &nf.numbering_system { - options.property( - js_string!("numberingSystem"), - js_string!(nu.to_string()), - Attribute::all(), - ); - } + options.property( + js_string!("numberingSystem"), + js_string!(nf.numbering_system.as_str()), + Attribute::all(), + ); options.property( js_string!("style"), diff --git a/core/engine/src/builtins/intl/number_format/options.rs b/core/engine/src/builtins/intl/number_format/options.rs index 89cf0b8b223..0b58124e6bd 100644 --- a/core/engine/src/builtins/intl/number_format/options.rs +++ b/core/engine/src/builtins/intl/number_format/options.rs @@ -6,16 +6,29 @@ use fixed_decimal::{ }; use boa_macros::js_str; -use icu_decimal::preferences::NumberingSystem; -use icu_locale::extensions::unicode::Value; +use icu_decimal::{ + DecimalFormatterPreferences, preferences::NumberingSystem, provider::DecimalSymbolsV1, +}; +use icu_locale::{extensions::unicode::Value, preferences::PreferenceKey}; +use icu_provider::{ + DataMarkerAttributes, + prelude::icu_locale_core::{ + LanguageIdentifier, extensions::unicode, preferences::LocalePreferences, + }, +}; use tinystr::TinyAsciiStr; use crate::{ Context, JsNativeError, JsObject, JsResult, JsStr, JsString, JsValue, builtins::{ - intl::options::{default_number_option, get_number_option}, + intl::{ + ServicePreferences, + locale::validate_extension, + options::{default_number_option, get_number_option}, + }, options::{OptionType, ParsableOptionType, get_option}, }, + context::icu::IntlProvider, js_string, }; @@ -1253,3 +1266,40 @@ impl RoundingType { } } } + +impl ServicePreferences for DecimalFormatterPreferences { + fn validate(&mut self, id: &LanguageIdentifier, provider: &IntlProvider) { + self.numbering_system = self.numbering_system.take().filter(|nu| { + let attr = DataMarkerAttributes::from_str_or_panic(nu.as_str()); + validate_extension::(id, attr, provider) + }); + } + + fn as_unicode(&self) -> unicode::Unicode { + let mut exts = unicode::Unicode::new(); + + if let Some(nu) = self.numbering_system + && let Some(value) = nu.unicode_extension_value() + { + exts.keywords.set(unicode::key!("nu"), value); + } + exts + } + + fn extended(&self, other: &Self) -> Self { + let mut result = *self; + result.extend(*other); + result + } + + fn intersection(&self, other: &Self) -> Self { + let mut inter = *self; + if inter.locale_preferences != other.locale_preferences { + inter.locale_preferences = LocalePreferences::default(); + } + if inter.numbering_system != other.numbering_system { + inter.numbering_system.take(); + } + inter + } +} diff --git a/core/engine/src/builtins/intl/options.rs b/core/engine/src/builtins/intl/options.rs index 17fbde775d4..52543429017 100644 --- a/core/engine/src/builtins/intl/options.rs +++ b/core/engine/src/builtins/intl/options.rs @@ -1,10 +1,12 @@ use std::{fmt, str::FromStr}; +use icu_locale::{LanguageIdentifier, extensions::unicode}; use num_traits::FromPrimitive; use crate::{ Context, JsNativeError, JsResult, JsString, JsValue, - builtins::{OrdinaryObject, options::ParsableOptionType}, + builtins::{OrdinaryObject, intl::ServicePreferences, options::ParsableOptionType}, + context::icu::IntlProvider, object::JsObject, }; @@ -15,7 +17,7 @@ use crate::{ #[derive(Debug, Default)] pub(super) struct IntlOptions { pub(super) matcher: LocaleMatcher, - pub(super) service_options: O, + pub(super) preferences: O, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] @@ -48,6 +50,28 @@ impl FromStr for LocaleMatcher { impl ParsableOptionType for LocaleMatcher {} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub(super) struct EmptyPreferences; + +impl From<&icu_locale::Locale> for EmptyPreferences { + fn from(_: &icu_locale::Locale) -> Self { + Self + } +} + +impl ServicePreferences for EmptyPreferences { + fn validate(&mut self, _: &LanguageIdentifier, _: &IntlProvider) {} + fn as_unicode(&self) -> unicode::Unicode { + unicode::Unicode::new() + } + fn extended(&self, _: &Self) -> Self { + Self + } + fn intersection(&self, _: &Self) -> Self { + Self + } +} + /// Abstract operation `GetNumberOption ( options, property, minimum, maximum, fallback )` /// /// Extracts the value of the property named `property` from the provided `options` diff --git a/core/engine/src/builtins/intl/plural_rules/mod.rs b/core/engine/src/builtins/intl/plural_rules/mod.rs index 68400788b93..01e3b5fd0fc 100644 --- a/core/engine/src/builtins/intl/plural_rules/mod.rs +++ b/core/engine/src/builtins/intl/plural_rules/mod.rs @@ -12,7 +12,7 @@ use crate::{ Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, builtins::{ Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, - options::get_option, + intl::options::EmptyPreferences, options::get_option, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, @@ -43,7 +43,7 @@ pub(crate) struct PluralRules { impl Service for PluralRules { type LangMarker = PluralsCardinalV1; - type LocaleOptions = (); + type Preferences = EmptyPreferences; } impl IntrinsicObject for PluralRules { diff --git a/core/engine/src/builtins/intl/segmenter/mod.rs b/core/engine/src/builtins/intl/segmenter/mod.rs index 2846506ec48..459a385f6ed 100644 --- a/core/engine/src/builtins/intl/segmenter/mod.rs +++ b/core/engine/src/builtins/intl/segmenter/mod.rs @@ -4,6 +4,7 @@ use crate::{ Context, JsArgs, JsData, JsNativeError, JsResult, JsString, JsSymbol, JsValue, builtins::{ BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + intl::options::EmptyPreferences, options::{get_option, get_options_object}, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -98,7 +99,7 @@ impl Service for Segmenter { // and replace when segmenters are locale-aware. type LangMarker = CollationDiacriticsV1; - type LocaleOptions = (); + type Preferences = EmptyPreferences; } impl IntrinsicObject for Segmenter {