mirror of https://github.com/boa-dev/boa.git
Browse Source
This Pull Request fixes/closes #1180. (I'll open a tracking issue for the progress) It changes the following: - Redesigns the internal API of Intl to (hopefully!) make it easier to implement a service. - Implements the `Intl.Locale` service. - Implements the `Intl.Collator` service. - Implements the `Intl.ListFormat` service. On the subject of the failing tests. Some of them are caused by missing locale data in the `icu_testdata` crate; we would need to regenerate that with the missing locales, or vendor a custom default data. On the other hand, there are some tests that are bugs from the ICU4X crate. The repo https://github.com/jedel1043/icu4x-test262 currently tracks the found bugs when running test262. I'll sync with the ICU4X team to try to fix those. cc @sffcpull/2509/head
José Julián Espina
2 years ago
42 changed files with 6470 additions and 1698 deletions
@ -0,0 +1,80 @@ |
|||||||
|
use std::str::FromStr; |
||||||
|
|
||||||
|
use icu_collator::{CaseLevel, Strength}; |
||||||
|
|
||||||
|
use crate::builtins::intl::options::OptionTypeParsable; |
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub(crate) enum Sensitivity { |
||||||
|
Base, |
||||||
|
Accent, |
||||||
|
Case, |
||||||
|
Variant, |
||||||
|
} |
||||||
|
|
||||||
|
impl Sensitivity { |
||||||
|
/// Converts the sensitivity option to the equivalent ICU4X collator options.
|
||||||
|
pub(crate) const fn to_collator_options(self) -> (Strength, CaseLevel) { |
||||||
|
match self { |
||||||
|
Sensitivity::Base => (Strength::Primary, CaseLevel::Off), |
||||||
|
Sensitivity::Accent => (Strength::Secondary, CaseLevel::Off), |
||||||
|
Sensitivity::Case => (Strength::Primary, CaseLevel::On), |
||||||
|
Sensitivity::Variant => (Strength::Tertiary, CaseLevel::On), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub(crate) struct ParseSensitivityError; |
||||||
|
|
||||||
|
impl std::fmt::Display for ParseSensitivityError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not `base`, `accent`, `case` or `variant`") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for Sensitivity { |
||||||
|
type Err = ParseSensitivityError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"base" => Ok(Self::Base), |
||||||
|
"accent" => Ok(Self::Accent), |
||||||
|
"case" => Ok(Self::Case), |
||||||
|
"variant" => Ok(Self::Variant), |
||||||
|
_ => Err(ParseSensitivityError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OptionTypeParsable for Sensitivity {} |
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] |
||||||
|
pub(crate) enum Usage { |
||||||
|
#[default] |
||||||
|
Sort, |
||||||
|
Search, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub(crate) struct ParseUsageError; |
||||||
|
|
||||||
|
impl std::fmt::Display for ParseUsageError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not `sort` or `search`") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for Usage { |
||||||
|
type Err = ParseUsageError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"sort" => Ok(Self::Sort), |
||||||
|
"search" => Ok(Self::Search), |
||||||
|
_ => Err(ParseUsageError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OptionTypeParsable for Usage {} |
@ -0,0 +1,53 @@ |
|||||||
|
use std::str::FromStr; |
||||||
|
|
||||||
|
use icu_list::ListLength; |
||||||
|
|
||||||
|
use crate::{ |
||||||
|
builtins::intl::options::{OptionType, OptionTypeParsable}, |
||||||
|
Context, JsNativeError, JsResult, JsValue, |
||||||
|
}; |
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)] |
||||||
|
pub(crate) enum ListFormatType { |
||||||
|
#[default] |
||||||
|
Conjunction, |
||||||
|
Disjunction, |
||||||
|
Unit, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub(crate) struct ParseListFormatTypeError; |
||||||
|
|
||||||
|
impl std::fmt::Display for ParseListFormatTypeError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not `conjunction`, `disjunction` or `unit`") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for ListFormatType { |
||||||
|
type Err = ParseListFormatTypeError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"conjunction" => Ok(Self::Conjunction), |
||||||
|
"disjunction" => Ok(Self::Disjunction), |
||||||
|
"unit" => Ok(Self::Unit), |
||||||
|
_ => Err(ParseListFormatTypeError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OptionTypeParsable for ListFormatType {} |
||||||
|
|
||||||
|
impl OptionType for ListLength { |
||||||
|
fn from_value(value: JsValue, context: &mut Context) -> JsResult<Self> { |
||||||
|
match value.to_string(context)?.to_std_string_escaped().as_str() { |
||||||
|
"long" => Ok(Self::Wide), |
||||||
|
"short" => Ok(Self::Short), |
||||||
|
"narrow" => Ok(Self::Narrow), |
||||||
|
_ => Err(JsNativeError::range() |
||||||
|
.with_message("provided string was not `long`, `short` or `narrow`") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
use icu_locid::extensions::unicode::Value; |
||||||
|
|
||||||
|
use crate::{builtins::intl::options::OptionType, JsNativeError}; |
||||||
|
|
||||||
|
impl OptionType for Value { |
||||||
|
fn from_value(value: crate::JsValue, context: &mut crate::Context) -> crate::JsResult<Self> { |
||||||
|
let val = value |
||||||
|
.to_string(context)? |
||||||
|
.to_std_string_escaped() |
||||||
|
.parse::<Value>() |
||||||
|
.map_err(|e| JsNativeError::range().with_message(e.to_string()))?; |
||||||
|
|
||||||
|
if val.as_tinystr_slice().is_empty() { |
||||||
|
return Err(JsNativeError::range() |
||||||
|
.with_message("Unicode Locale Identifier `type` cannot be empty") |
||||||
|
.into()); |
||||||
|
} |
||||||
|
|
||||||
|
Ok(val) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
use icu_datetime::{ |
||||||
|
options::preferences::HourCycle, pattern::CoarseHourCycle, |
||||||
|
provider::calendar::TimeLengthsV1Marker, |
||||||
|
}; |
||||||
|
use icu_locid::{ |
||||||
|
extensions::unicode::Value, extensions_unicode_key as key, extensions_unicode_value as value, |
||||||
|
locale, Locale, |
||||||
|
}; |
||||||
|
use icu_plurals::provider::CardinalV1Marker; |
||||||
|
use icu_provider::{DataLocale, DataProvider, DataRequest, DataRequestMetadata}; |
||||||
|
use icu_provider_adapters::fallback::LocaleFallbackProvider; |
||||||
|
|
||||||
|
use crate::{ |
||||||
|
builtins::intl::{ |
||||||
|
locale::{best_locale_for_provider, default_locale, resolve_locale}, |
||||||
|
options::{IntlOptions, LocaleMatcher}, |
||||||
|
Service, |
||||||
|
}, |
||||||
|
context::icu::{BoaProvider, Icu}, |
||||||
|
}; |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
struct TestOptions { |
||||||
|
hc: Option<HourCycle>, |
||||||
|
} |
||||||
|
|
||||||
|
struct TestService; |
||||||
|
|
||||||
|
impl<P> Service<P> for TestService |
||||||
|
where |
||||||
|
P: DataProvider<TimeLengthsV1Marker>, |
||||||
|
{ |
||||||
|
type LangMarker = CardinalV1Marker; |
||||||
|
|
||||||
|
type LocaleOptions = TestOptions; |
||||||
|
|
||||||
|
fn resolve(locale: &mut Locale, options: &mut Self::LocaleOptions, provider: &P) { |
||||||
|
let loc_hc = locale |
||||||
|
.extensions |
||||||
|
.unicode |
||||||
|
.keywords |
||||||
|
.get(&key!("hc")) |
||||||
|
.and_then(Value::as_single_subtag) |
||||||
|
.and_then(|s| match &**s { |
||||||
|
"h11" => Some(HourCycle::H11), |
||||||
|
"h12" => Some(HourCycle::H12), |
||||||
|
"h23" => Some(HourCycle::H23), |
||||||
|
"h24" => Some(HourCycle::H24), |
||||||
|
_ => None, |
||||||
|
}); |
||||||
|
let hc = options.hc.or(loc_hc).unwrap_or_else(|| { |
||||||
|
let req = DataRequest { |
||||||
|
locale: &DataLocale::from(&*locale), |
||||||
|
metadata: DataRequestMetadata::default(), |
||||||
|
}; |
||||||
|
let preferred = DataProvider::<TimeLengthsV1Marker>::load(provider, req) |
||||||
|
.unwrap() |
||||||
|
.take_payload() |
||||||
|
.unwrap() |
||||||
|
.get() |
||||||
|
.preferred_hour_cycle; |
||||||
|
match preferred { |
||||||
|
CoarseHourCycle::H11H12 => HourCycle::H11, |
||||||
|
CoarseHourCycle::H23H24 => HourCycle::H23, |
||||||
|
} |
||||||
|
}); |
||||||
|
let hc_value = match hc { |
||||||
|
HourCycle::H11 => value!("h11"), |
||||||
|
HourCycle::H12 => value!("h12"), |
||||||
|
HourCycle::H23 => value!("h23"), |
||||||
|
HourCycle::H24 => value!("h24"), |
||||||
|
}; |
||||||
|
locale.extensions.unicode.keywords.set(key!("hc"), hc_value); |
||||||
|
options.hc = Some(hc); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn locale_resolution() { |
||||||
|
let provider = |
||||||
|
LocaleFallbackProvider::try_new_with_buffer_provider(boa_icu_provider::blob()).unwrap(); |
||||||
|
let icu = Icu::new(BoaProvider::Buffer(Box::new(provider))).unwrap(); |
||||||
|
let mut default = default_locale(icu.locale_canonicalizer()); |
||||||
|
default |
||||||
|
.extensions |
||||||
|
.unicode |
||||||
|
.keywords |
||||||
|
.set(key!("hc"), value!("h11")); |
||||||
|
|
||||||
|
// test lookup
|
||||||
|
let mut options = IntlOptions { |
||||||
|
matcher: LocaleMatcher::Lookup, |
||||||
|
service_options: TestOptions { |
||||||
|
hc: Some(HourCycle::H11), |
||||||
|
}, |
||||||
|
}; |
||||||
|
let locale = resolve_locale::<TestService, _>(&[], &mut options, &icu); |
||||||
|
assert_eq!(locale, default); |
||||||
|
|
||||||
|
// test best fit
|
||||||
|
let mut options = IntlOptions { |
||||||
|
matcher: LocaleMatcher::BestFit, |
||||||
|
service_options: TestOptions { |
||||||
|
hc: Some(HourCycle::H11), |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
let locale = resolve_locale::<TestService, _>(&[], &mut options, &icu); |
||||||
|
let best = best_locale_for_provider::<<TestService as Service<BoaProvider>>::LangMarker>( |
||||||
|
default.id.clone(), |
||||||
|
icu.provider(), |
||||||
|
) |
||||||
|
.unwrap(); |
||||||
|
let mut best = Locale::from(best); |
||||||
|
best.extensions = locale.extensions.clone(); |
||||||
|
assert_eq!(locale, best); |
||||||
|
|
||||||
|
// requested: [es-ES]
|
||||||
|
let mut options = IntlOptions { |
||||||
|
matcher: LocaleMatcher::Lookup, |
||||||
|
service_options: TestOptions { hc: None }, |
||||||
|
}; |
||||||
|
|
||||||
|
let locale = resolve_locale::<TestService, _>(&[locale!("es-AR")], &mut options, &icu); |
||||||
|
assert_eq!(locale, "es-u-hc-h23".parse().unwrap()); |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
// TODO: implement `Segmenter` when https://github.com/unicode-org/icu4x/issues/2259 closes.
|
||||||
|
|
||||||
|
use boa_profiler::Profiler; |
||||||
|
use tap::{Conv, Pipe}; |
||||||
|
|
||||||
|
use crate::{builtins::BuiltIn, object::ConstructorBuilder, Context, JsResult, JsValue}; |
||||||
|
|
||||||
|
mod options; |
||||||
|
#[allow(unused)] |
||||||
|
pub(crate) use options::*; |
||||||
|
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub(crate) struct Segmenter; |
||||||
|
|
||||||
|
impl BuiltIn for Segmenter { |
||||||
|
const NAME: &'static str = "Segmenter"; |
||||||
|
|
||||||
|
fn init(context: &mut Context) -> Option<JsValue> { |
||||||
|
let _timer = Profiler::global().start_event(Self::NAME, "init"); |
||||||
|
|
||||||
|
ConstructorBuilder::with_standard_constructor( |
||||||
|
context, |
||||||
|
Self::constructor, |
||||||
|
context.intrinsics().constructors().segmenter().clone(), |
||||||
|
) |
||||||
|
.name(Self::NAME) |
||||||
|
.length(Self::LENGTH) |
||||||
|
.build() |
||||||
|
.conv::<JsValue>() |
||||||
|
.pipe(Some) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Segmenter { |
||||||
|
pub(crate) const LENGTH: usize = 0; |
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)] |
||||||
|
pub(crate) fn constructor(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> { |
||||||
|
Ok(JsValue::Undefined) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
#[derive(Debug, Clone, Copy, Default)] |
||||||
|
pub(crate) enum Granularity { |
||||||
|
#[default] |
||||||
|
Grapheme, |
||||||
|
Word, |
||||||
|
Sentence, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub(crate) struct ParseGranularityError; |
||||||
|
|
||||||
|
impl std::fmt::Display for ParseGranularityError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not `grapheme`, `word` or `sentence`") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl std::str::FromStr for Granularity { |
||||||
|
type Err = ParseGranularityError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"grapheme" => Ok(Self::Grapheme), |
||||||
|
"word" => Ok(Self::Word), |
||||||
|
"sentence" => Ok(Self::Sentence), |
||||||
|
_ => Err(ParseGranularityError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,547 +0,0 @@ |
|||||||
use crate::{ |
|
||||||
builtins::intl::date_time_format::{to_date_time_options, DateTimeReqs}, |
|
||||||
builtins::intl::{ |
|
||||||
best_available_locale, best_fit_matcher, default_locale, default_number_option, |
|
||||||
get_number_option, get_option, insert_unicode_extension_and_canonicalize, lookup_matcher, |
|
||||||
resolve_locale, unicode_extension_components, DateTimeFormatRecord, GetOptionType, |
|
||||||
}, |
|
||||||
object::JsObject, |
|
||||||
Context, JsValue, |
|
||||||
}; |
|
||||||
|
|
||||||
use icu_locale_canonicalizer::LocaleCanonicalizer; |
|
||||||
use rustc_hash::FxHashMap; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn best_avail_loc() { |
|
||||||
let no_extensions_locale = "en-US"; |
|
||||||
let available_locales = Vec::new(); |
|
||||||
assert_eq!( |
|
||||||
best_available_locale(&available_locales, no_extensions_locale), |
|
||||||
None |
|
||||||
); |
|
||||||
|
|
||||||
let no_extensions_locale = "de-DE"; |
|
||||||
let available_locales = vec![no_extensions_locale]; |
|
||||||
assert_eq!( |
|
||||||
best_available_locale(&available_locales, no_extensions_locale), |
|
||||||
Some(no_extensions_locale) |
|
||||||
); |
|
||||||
|
|
||||||
let locale_part = "fr"; |
|
||||||
let no_extensions_locale = locale_part.to_string() + "-CA"; |
|
||||||
let available_locales = vec![locale_part]; |
|
||||||
assert_eq!( |
|
||||||
best_available_locale(&available_locales, &no_extensions_locale), |
|
||||||
Some(locale_part) |
|
||||||
); |
|
||||||
|
|
||||||
let ja_kana_t = "ja-Kana-JP-t"; |
|
||||||
let ja_kana = "ja-Kana-JP"; |
|
||||||
let no_extensions_locale = "ja-Kana-JP-t-it-latn-it"; |
|
||||||
let available_locales = vec![ja_kana_t, ja_kana]; |
|
||||||
assert_eq!( |
|
||||||
best_available_locale(&available_locales, no_extensions_locale), |
|
||||||
Some(ja_kana) |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn lookup_match() { |
|
||||||
let provider = icu_testdata::get_provider(); |
|
||||||
let canonicalizer = |
|
||||||
LocaleCanonicalizer::new(&provider).expect("Could not create canonicalizer"); |
|
||||||
// available: [], requested: []
|
|
||||||
let available_locales = Vec::new(); |
|
||||||
let requested_locales = Vec::new(); |
|
||||||
|
|
||||||
let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer); |
|
||||||
assert_eq!( |
|
||||||
matcher.locale, |
|
||||||
default_locale(&canonicalizer).to_string().as_str() |
|
||||||
); |
|
||||||
assert_eq!(matcher.extension, ""); |
|
||||||
|
|
||||||
// available: [de-DE], requested: []
|
|
||||||
let available_locales = vec!["de-DE"]; |
|
||||||
let requested_locales = Vec::new(); |
|
||||||
|
|
||||||
let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer); |
|
||||||
assert_eq!( |
|
||||||
matcher.locale, |
|
||||||
default_locale(&canonicalizer).to_string().as_str() |
|
||||||
); |
|
||||||
assert_eq!(matcher.extension, ""); |
|
||||||
|
|
||||||
// available: [fr-FR], requested: [fr-FR-u-hc-h12]
|
|
||||||
let available_locales = vec!["fr-FR"]; |
|
||||||
let requested_locales = vec!["fr-FR-u-hc-h12"]; |
|
||||||
|
|
||||||
let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer); |
|
||||||
assert_eq!(matcher.locale, "fr-FR"); |
|
||||||
assert_eq!(matcher.extension, "u-hc-h12"); |
|
||||||
|
|
||||||
// available: [es-ES], requested: [es-ES]
|
|
||||||
let available_locales = vec!["es-ES"]; |
|
||||||
let requested_locales = vec!["es-ES"]; |
|
||||||
|
|
||||||
let matcher = best_fit_matcher(&available_locales, &requested_locales, &canonicalizer); |
|
||||||
assert_eq!(matcher.locale, "es-ES"); |
|
||||||
assert_eq!(matcher.extension, ""); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn insert_unicode_ext() { |
|
||||||
let provider = icu_testdata::get_provider(); |
|
||||||
let canonicalizer = |
|
||||||
LocaleCanonicalizer::new(&provider).expect("Could not create canonicalizer"); |
|
||||||
let locale = "hu-HU"; |
|
||||||
let ext = ""; |
|
||||||
assert_eq!( |
|
||||||
insert_unicode_extension_and_canonicalize(locale, ext, &canonicalizer), |
|
||||||
locale |
|
||||||
); |
|
||||||
|
|
||||||
let locale = "hu-HU"; |
|
||||||
let ext = "-u-hc-h12"; |
|
||||||
assert_eq!( |
|
||||||
insert_unicode_extension_and_canonicalize(locale, ext, &canonicalizer), |
|
||||||
"hu-HU-u-hc-h12" |
|
||||||
); |
|
||||||
|
|
||||||
let locale = "hu-HU-x-PRIVATE"; |
|
||||||
let ext = "-u-hc-h12"; |
|
||||||
assert_eq!( |
|
||||||
insert_unicode_extension_and_canonicalize(locale, ext, &canonicalizer), |
|
||||||
"hu-HU-u-hc-h12-x-private" |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn uni_ext_comp() { |
|
||||||
let ext = "-u-ca-japanese-hc-h12"; |
|
||||||
let components = unicode_extension_components(ext); |
|
||||||
assert!(components.attributes.is_empty()); |
|
||||||
assert_eq!(components.keywords.len(), 2); |
|
||||||
assert_eq!(components.keywords[0].key, "ca"); |
|
||||||
assert_eq!(components.keywords[0].value, "japanese"); |
|
||||||
assert_eq!(components.keywords[1].key, "hc"); |
|
||||||
assert_eq!(components.keywords[1].value, "h12"); |
|
||||||
|
|
||||||
let ext = "-u-alias-co-phonebk-ka-shifted"; |
|
||||||
let components = unicode_extension_components(ext); |
|
||||||
assert_eq!(components.attributes, vec![String::from("alias")]); |
|
||||||
assert_eq!(components.keywords.len(), 2); |
|
||||||
assert_eq!(components.keywords[0].key, "co"); |
|
||||||
assert_eq!(components.keywords[0].value, "phonebk"); |
|
||||||
assert_eq!(components.keywords[1].key, "ka"); |
|
||||||
assert_eq!(components.keywords[1].value, "shifted"); |
|
||||||
|
|
||||||
let ext = "-u-ca-buddhist-kk-nu-thai"; |
|
||||||
let components = unicode_extension_components(ext); |
|
||||||
assert!(components.attributes.is_empty()); |
|
||||||
assert_eq!(components.keywords.len(), 3); |
|
||||||
assert_eq!(components.keywords[0].key, "ca"); |
|
||||||
assert_eq!(components.keywords[0].value, "buddhist"); |
|
||||||
assert_eq!(components.keywords[1].key, "kk"); |
|
||||||
assert_eq!(components.keywords[1].value, ""); |
|
||||||
assert_eq!(components.keywords[2].key, "nu"); |
|
||||||
assert_eq!(components.keywords[2].value, "thai"); |
|
||||||
|
|
||||||
let ext = "-u-ca-islamic-civil"; |
|
||||||
let components = unicode_extension_components(ext); |
|
||||||
assert!(components.attributes.is_empty()); |
|
||||||
assert_eq!(components.keywords.len(), 1); |
|
||||||
assert_eq!(components.keywords[0].key, "ca"); |
|
||||||
assert_eq!(components.keywords[0].value, "islamic-civil"); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn locale_resolution() { |
|
||||||
let mut context = Context::default(); |
|
||||||
|
|
||||||
// test lookup
|
|
||||||
let available_locales = Vec::new(); |
|
||||||
let requested_locales = Vec::new(); |
|
||||||
let relevant_extension_keys = Vec::new(); |
|
||||||
let locale_data = FxHashMap::default(); |
|
||||||
let options = DateTimeFormatRecord { |
|
||||||
locale_matcher: "lookup".into(), |
|
||||||
properties: FxHashMap::default(), |
|
||||||
}; |
|
||||||
|
|
||||||
let locale_record = resolve_locale( |
|
||||||
&available_locales, |
|
||||||
&requested_locales, |
|
||||||
&options, |
|
||||||
&relevant_extension_keys, |
|
||||||
&locale_data, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
locale_record.locale, |
|
||||||
default_locale(context.icu().locale_canonicalizer()) |
|
||||||
.to_string() |
|
||||||
.as_str() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
locale_record.data_locale, |
|
||||||
default_locale(context.icu().locale_canonicalizer()) |
|
||||||
.to_string() |
|
||||||
.as_str() |
|
||||||
); |
|
||||||
assert!(locale_record.properties.is_empty()); |
|
||||||
|
|
||||||
// test best fit
|
|
||||||
let available_locales = Vec::new(); |
|
||||||
let requested_locales = Vec::new(); |
|
||||||
let relevant_extension_keys = Vec::new(); |
|
||||||
let locale_data = FxHashMap::default(); |
|
||||||
let options = DateTimeFormatRecord { |
|
||||||
locale_matcher: "best-fit".into(), |
|
||||||
properties: FxHashMap::default(), |
|
||||||
}; |
|
||||||
|
|
||||||
let locale_record = resolve_locale( |
|
||||||
&available_locales, |
|
||||||
&requested_locales, |
|
||||||
&options, |
|
||||||
&relevant_extension_keys, |
|
||||||
&locale_data, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
locale_record.locale, |
|
||||||
default_locale(context.icu().locale_canonicalizer()) |
|
||||||
.to_string() |
|
||||||
.as_str() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
locale_record.data_locale, |
|
||||||
default_locale(context.icu().locale_canonicalizer()) |
|
||||||
.to_string() |
|
||||||
.as_str() |
|
||||||
); |
|
||||||
assert!(locale_record.properties.is_empty()); |
|
||||||
|
|
||||||
// available: [es-ES], requested: [es-ES]
|
|
||||||
let available_locales = vec!["es-ES"]; |
|
||||||
let requested_locales = vec!["es-ES"]; |
|
||||||
let relevant_extension_keys = Vec::new(); |
|
||||||
let locale_data = FxHashMap::default(); |
|
||||||
let options = DateTimeFormatRecord { |
|
||||||
locale_matcher: "lookup".into(), |
|
||||||
properties: FxHashMap::default(), |
|
||||||
}; |
|
||||||
|
|
||||||
let locale_record = resolve_locale( |
|
||||||
&available_locales, |
|
||||||
&requested_locales, |
|
||||||
&options, |
|
||||||
&relevant_extension_keys, |
|
||||||
&locale_data, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert_eq!(locale_record.locale, "es-ES"); |
|
||||||
assert_eq!(locale_record.data_locale, "es-ES"); |
|
||||||
assert!(locale_record.properties.is_empty()); |
|
||||||
|
|
||||||
// available: [zh-CN], requested: []
|
|
||||||
let available_locales = vec!["zh-CN"]; |
|
||||||
let requested_locales = Vec::new(); |
|
||||||
let relevant_extension_keys = Vec::new(); |
|
||||||
let locale_data = FxHashMap::default(); |
|
||||||
let options = DateTimeFormatRecord { |
|
||||||
locale_matcher: "lookup".into(), |
|
||||||
properties: FxHashMap::default(), |
|
||||||
}; |
|
||||||
|
|
||||||
let locale_record = resolve_locale( |
|
||||||
&available_locales, |
|
||||||
&requested_locales, |
|
||||||
&options, |
|
||||||
&relevant_extension_keys, |
|
||||||
&locale_data, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
locale_record.locale, |
|
||||||
default_locale(context.icu().locale_canonicalizer()) |
|
||||||
.to_string() |
|
||||||
.as_str() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
locale_record.data_locale, |
|
||||||
default_locale(context.icu().locale_canonicalizer()) |
|
||||||
.to_string() |
|
||||||
.as_str() |
|
||||||
); |
|
||||||
assert!(locale_record.properties.is_empty()); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn get_opt() { |
|
||||||
let mut context = Context::default(); |
|
||||||
|
|
||||||
let values = Vec::new(); |
|
||||||
let fallback = JsValue::String("fallback".into()); |
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
let option_type = GetOptionType::String; |
|
||||||
let get_option_result = get_option( |
|
||||||
&options_obj, |
|
||||||
"", |
|
||||||
&option_type, |
|
||||||
&values, |
|
||||||
&fallback, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("GetOption should not fail on fallback test"); |
|
||||||
assert_eq!(get_option_result, fallback); |
|
||||||
|
|
||||||
let values = Vec::new(); |
|
||||||
let fallback = JsValue::String("fallback".into()); |
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
let locale_value = JsValue::String("en-US".into()); |
|
||||||
options_obj |
|
||||||
.set("Locale", locale_value.clone(), true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let option_type = GetOptionType::String; |
|
||||||
let get_option_result = get_option( |
|
||||||
&options_obj, |
|
||||||
"Locale", |
|
||||||
&option_type, |
|
||||||
&values, |
|
||||||
&fallback, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("GetOption should not fail on string test"); |
|
||||||
assert_eq!(get_option_result, locale_value); |
|
||||||
|
|
||||||
let fallback = JsValue::String("fallback".into()); |
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
let locale_string = "en-US"; |
|
||||||
let locale_value = JsValue::String(locale_string.into()); |
|
||||||
let values = vec![locale_string]; |
|
||||||
options_obj |
|
||||||
.set("Locale", locale_value.clone(), true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let option_type = GetOptionType::String; |
|
||||||
let get_option_result = get_option( |
|
||||||
&options_obj, |
|
||||||
"Locale", |
|
||||||
&option_type, |
|
||||||
&values, |
|
||||||
&fallback, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("GetOption should not fail on values test"); |
|
||||||
assert_eq!(get_option_result, locale_value); |
|
||||||
|
|
||||||
let fallback = JsValue::new(false); |
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
let boolean_value = JsValue::new(true); |
|
||||||
let values = Vec::new(); |
|
||||||
options_obj |
|
||||||
.set("boolean_val", boolean_value.clone(), true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let option_type = GetOptionType::Boolean; |
|
||||||
let get_option_result = get_option( |
|
||||||
&options_obj, |
|
||||||
"boolean_val", |
|
||||||
&option_type, |
|
||||||
&values, |
|
||||||
&fallback, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("GetOption should not fail on boolean test"); |
|
||||||
assert_eq!(get_option_result, boolean_value); |
|
||||||
|
|
||||||
let fallback = JsValue::String("fallback".into()); |
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
let locale_value = JsValue::String("en-US".into()); |
|
||||||
let other_locale_str = "de-DE"; |
|
||||||
let values = vec![other_locale_str]; |
|
||||||
options_obj |
|
||||||
.set("Locale", locale_value, true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let option_type = GetOptionType::String; |
|
||||||
let get_option_result = get_option( |
|
||||||
&options_obj, |
|
||||||
"Locale", |
|
||||||
&option_type, |
|
||||||
&values, |
|
||||||
&fallback, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert!(get_option_result.is_err()); |
|
||||||
|
|
||||||
let value = JsValue::undefined(); |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback_val = 5.0; |
|
||||||
let fallback = Some(fallback_val); |
|
||||||
let get_option_result = |
|
||||||
default_number_option(&value, minimum, maximum, fallback, &mut context).unwrap(); |
|
||||||
assert_eq!(get_option_result, fallback); |
|
||||||
|
|
||||||
let value = JsValue::nan(); |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback = Some(5.0); |
|
||||||
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); |
|
||||||
assert!(get_option_result.is_err()); |
|
||||||
|
|
||||||
let value = JsValue::new(0); |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback = Some(5.0); |
|
||||||
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); |
|
||||||
assert!(get_option_result.is_err()); |
|
||||||
|
|
||||||
let value = JsValue::new(11); |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback = Some(5.0); |
|
||||||
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); |
|
||||||
assert!(get_option_result.is_err()); |
|
||||||
|
|
||||||
let value_f64 = 7.0; |
|
||||||
let value = JsValue::new(value_f64); |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback = Some(5.0); |
|
||||||
let get_option_result = |
|
||||||
default_number_option(&value, minimum, maximum, fallback, &mut context).unwrap(); |
|
||||||
assert_eq!(get_option_result, Some(value_f64)); |
|
||||||
|
|
||||||
let options = JsObject::empty(); |
|
||||||
let property = "fractionalSecondDigits"; |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback_val = 5.0; |
|
||||||
let fallback = Some(fallback_val); |
|
||||||
let get_option_result = |
|
||||||
get_number_option(&options, property, minimum, maximum, fallback, &mut context).unwrap(); |
|
||||||
assert_eq!(get_option_result, fallback); |
|
||||||
|
|
||||||
let options = JsObject::empty(); |
|
||||||
let value_f64 = 8.0; |
|
||||||
let value = JsValue::new(value_f64); |
|
||||||
let property = "fractionalSecondDigits"; |
|
||||||
options |
|
||||||
.set(property, value, true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let minimum = 1.0; |
|
||||||
let maximum = 10.0; |
|
||||||
let fallback = Some(5.0); |
|
||||||
let get_option_result = |
|
||||||
get_number_option(&options, property, minimum, maximum, fallback, &mut context).unwrap(); |
|
||||||
assert_eq!(get_option_result, Some(value_f64)); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn to_date_time_opts() { |
|
||||||
let mut context = Context::default(); |
|
||||||
|
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
options_obj |
|
||||||
.set("timeStyle", JsObject::empty(), true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let date_time_opts = to_date_time_options( |
|
||||||
&JsValue::new(options_obj), |
|
||||||
&DateTimeReqs::Date, |
|
||||||
&DateTimeReqs::Date, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert!(date_time_opts.is_err()); |
|
||||||
|
|
||||||
let options_obj = JsObject::empty(); |
|
||||||
options_obj |
|
||||||
.set("dateStyle", JsObject::empty(), true, &mut context) |
|
||||||
.expect("Setting a property should not fail"); |
|
||||||
let date_time_opts = to_date_time_options( |
|
||||||
&JsValue::new(options_obj), |
|
||||||
&DateTimeReqs::Time, |
|
||||||
&DateTimeReqs::Time, |
|
||||||
&mut context, |
|
||||||
); |
|
||||||
assert!(date_time_opts.is_err()); |
|
||||||
|
|
||||||
let date_time_opts = to_date_time_options( |
|
||||||
&JsValue::undefined(), |
|
||||||
&DateTimeReqs::Date, |
|
||||||
&DateTimeReqs::Date, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("toDateTimeOptions should not fail in date test"); |
|
||||||
|
|
||||||
let numeric_jsstring = JsValue::String("numeric".into()); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("year", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("month", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("day", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
|
|
||||||
let date_time_opts = to_date_time_options( |
|
||||||
&JsValue::undefined(), |
|
||||||
&DateTimeReqs::Time, |
|
||||||
&DateTimeReqs::Time, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("toDateTimeOptions should not fail in time test"); |
|
||||||
|
|
||||||
let numeric_jsstring = JsValue::String("numeric".into()); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("hour", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("minute", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("second", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
|
|
||||||
let date_time_opts = to_date_time_options( |
|
||||||
&JsValue::undefined(), |
|
||||||
&DateTimeReqs::AnyAll, |
|
||||||
&DateTimeReqs::AnyAll, |
|
||||||
&mut context, |
|
||||||
) |
|
||||||
.expect("toDateTimeOptions should not fail when testing required = 'any'"); |
|
||||||
|
|
||||||
let numeric_jsstring = JsValue::String("numeric".into()); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("year", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("month", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("day", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("hour", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("minute", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
date_time_opts.get("second", &mut context).unwrap(), |
|
||||||
numeric_jsstring |
|
||||||
); |
|
||||||
} |
|
@ -1,79 +1,175 @@ |
|||||||
use icu_datetime::provider::{ |
use std::fmt::Debug; |
||||||
calendar::{DatePatternsV1Marker, DateSkeletonPatternsV1Marker, DateSymbolsV1Marker}, |
|
||||||
week_data::WeekDataV1Marker, |
use icu_collator::{Collator, CollatorError, CollatorOptions}; |
||||||
}; |
use icu_list::{ListError, ListFormatter, ListLength}; |
||||||
use icu_locale_canonicalizer::{ |
use icu_locid_transform::{LocaleCanonicalizer, LocaleExpander, LocaleTransformError}; |
||||||
provider::{AliasesV1Marker, LikelySubtagsV1Marker}, |
use icu_provider::{ |
||||||
LocaleCanonicalizer, |
yoke::{trait_hack::YokeTraitHack, Yokeable}, |
||||||
|
zerofrom::ZeroFrom, |
||||||
|
AnyProvider, AsDeserializingBufferProvider, AsDowncastingAnyProvider, BufferProvider, |
||||||
|
DataError, DataLocale, DataProvider, DataRequest, DataResponse, KeyedDataMarker, MaybeSendSync, |
||||||
}; |
}; |
||||||
use icu_plurals::provider::OrdinalV1Marker; |
use serde::Deserialize; |
||||||
use icu_provider::prelude::*; |
|
||||||
|
|
||||||
/// Trait encompassing all the required implementations that define
|
use crate::builtins::intl::list_format::ListFormatType; |
||||||
/// a valid icu data provider.
|
|
||||||
pub trait BoaProvider: |
/// ICU4X data provider used in boa.
|
||||||
ResourceProvider<AliasesV1Marker> |
///
|
||||||
+ ResourceProvider<LikelySubtagsV1Marker> |
/// Providers can be either [`BufferProvider`]s or [`AnyProvider`]s.
|
||||||
+ ResourceProvider<DateSymbolsV1Marker> |
///
|
||||||
+ ResourceProvider<DatePatternsV1Marker> |
/// The [`icu_provider`] documentation has more information about data providers.
|
||||||
+ ResourceProvider<DateSkeletonPatternsV1Marker> |
pub enum BoaProvider { |
||||||
+ ResourceProvider<OrdinalV1Marker> |
/// A [`BufferProvider`] data provider.
|
||||||
+ ResourceProvider<WeekDataV1Marker> |
Buffer(Box<dyn BufferProvider>), |
||||||
{ |
/// An [`AnyProvider] data provider.
|
||||||
|
Any(Box<dyn AnyProvider>), |
||||||
} |
} |
||||||
|
|
||||||
impl<T> BoaProvider for T where |
impl Debug for BoaProvider { |
||||||
T: ResourceProvider<AliasesV1Marker> |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
+ ResourceProvider<LikelySubtagsV1Marker> |
match self { |
||||||
+ ResourceProvider<DateSymbolsV1Marker> |
Self::Buffer(_) => f.debug_tuple("Buffer").field(&"_").finish(), |
||||||
+ ResourceProvider<DatePatternsV1Marker> |
Self::Any(_) => f.debug_tuple("Any").field(&"_").finish(), |
||||||
+ ResourceProvider<DateSkeletonPatternsV1Marker> |
} |
||||||
+ ResourceProvider<OrdinalV1Marker> |
} |
||||||
+ ResourceProvider<WeekDataV1Marker> |
} |
||||||
+ ?Sized |
|
||||||
|
impl<M> DataProvider<M> for BoaProvider |
||||||
|
where |
||||||
|
M: KeyedDataMarker + 'static, |
||||||
|
for<'de> YokeTraitHack<<M::Yokeable as Yokeable<'de>>::Output>: Deserialize<'de>, |
||||||
|
for<'a> YokeTraitHack<<M::Yokeable as Yokeable<'a>>::Output>: Clone, |
||||||
|
M::Yokeable: ZeroFrom<'static, M::Yokeable> + MaybeSendSync, |
||||||
{ |
{ |
||||||
|
fn load(&self, req: DataRequest<'_>) -> Result<DataResponse<M>, DataError> { |
||||||
|
match self { |
||||||
|
BoaProvider::Buffer(provider) => provider.as_deserializing().load(req), |
||||||
|
BoaProvider::Any(provider) => provider.as_downcasting().load(req), |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
/// Collection of tools initialized from a [`BoaProvider`] that are used
|
impl BoaProvider { |
||||||
|
/// Creates a new [`LocaleCanonicalizer`] from the provided [`DataProvider`].
|
||||||
|
pub(crate) fn try_new_locale_canonicalizer( |
||||||
|
&self, |
||||||
|
) -> Result<LocaleCanonicalizer, LocaleTransformError> { |
||||||
|
match self { |
||||||
|
BoaProvider::Buffer(buffer) => { |
||||||
|
LocaleCanonicalizer::try_new_with_buffer_provider(&**buffer) |
||||||
|
} |
||||||
|
BoaProvider::Any(any) => LocaleCanonicalizer::try_new_with_any_provider(&**any), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Creates a new [`LocaleExpander`] from the provided [`DataProvider`].
|
||||||
|
pub(crate) fn try_new_locale_expander(&self) -> Result<LocaleExpander, LocaleTransformError> { |
||||||
|
match self { |
||||||
|
BoaProvider::Buffer(buffer) => LocaleExpander::try_new_with_buffer_provider(&**buffer), |
||||||
|
BoaProvider::Any(any) => LocaleExpander::try_new_with_any_provider(&**any), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Creates a new [`ListFormatter`] from the provided [`DataProvider`] and options.
|
||||||
|
pub(crate) fn try_new_list_formatter( |
||||||
|
&self, |
||||||
|
locale: &DataLocale, |
||||||
|
typ: ListFormatType, |
||||||
|
style: ListLength, |
||||||
|
) -> Result<ListFormatter, ListError> { |
||||||
|
match self { |
||||||
|
BoaProvider::Buffer(buf) => match typ { |
||||||
|
ListFormatType::Conjunction => { |
||||||
|
ListFormatter::try_new_and_with_length_with_buffer_provider( |
||||||
|
&**buf, locale, style, |
||||||
|
) |
||||||
|
} |
||||||
|
ListFormatType::Disjunction => { |
||||||
|
ListFormatter::try_new_or_with_length_with_buffer_provider( |
||||||
|
&**buf, locale, style, |
||||||
|
) |
||||||
|
} |
||||||
|
ListFormatType::Unit => { |
||||||
|
ListFormatter::try_new_unit_with_length_with_buffer_provider( |
||||||
|
&**buf, locale, style, |
||||||
|
) |
||||||
|
} |
||||||
|
}, |
||||||
|
BoaProvider::Any(any) => match typ { |
||||||
|
ListFormatType::Conjunction => { |
||||||
|
ListFormatter::try_new_and_with_length_with_any_provider(&**any, locale, style) |
||||||
|
} |
||||||
|
ListFormatType::Disjunction => { |
||||||
|
ListFormatter::try_new_or_with_length_with_any_provider(&**any, locale, style) |
||||||
|
} |
||||||
|
ListFormatType::Unit => { |
||||||
|
ListFormatter::try_new_unit_with_length_with_any_provider(&**any, locale, style) |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Creates a new [`Collator`] from the provided [`DataProvider`] and options.
|
||||||
|
pub(crate) fn try_new_collator( |
||||||
|
&self, |
||||||
|
locale: &DataLocale, |
||||||
|
options: CollatorOptions, |
||||||
|
) -> Result<Collator, CollatorError> { |
||||||
|
match self { |
||||||
|
BoaProvider::Buffer(buf) => { |
||||||
|
Collator::try_new_with_buffer_provider(&**buf, locale, options) |
||||||
|
} |
||||||
|
BoaProvider::Any(any) => Collator::try_new_with_any_provider(&**any, locale, options), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Collection of tools initialized from a [`DataProvider`] that are used
|
||||||
/// for the functionality of `Intl`.
|
/// for the functionality of `Intl`.
|
||||||
#[allow(unused)] |
pub(crate) struct Icu<P> { |
||||||
pub(crate) struct Icu { |
provider: P, |
||||||
provider: Box<dyn BoaProvider>, |
|
||||||
locale_canonicalizer: LocaleCanonicalizer, |
locale_canonicalizer: LocaleCanonicalizer, |
||||||
|
locale_expander: LocaleExpander, |
||||||
} |
} |
||||||
|
|
||||||
impl std::fmt::Debug for Icu { |
impl<P: Debug> Debug for Icu<P> { |
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
#[derive(Debug)] |
|
||||||
struct Canonicalizer; |
|
||||||
f.debug_struct("Icu") |
f.debug_struct("Icu") |
||||||
.field("locale_canonicalizer", &Canonicalizer) |
.field("provider", &self.provider) |
||||||
|
.field("locale_canonicalizer", &"LocaleCanonicalizer") |
||||||
.finish() |
.finish() |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
impl Icu { |
impl<P> Icu<P> { |
||||||
/// Create a new [`Icu`] from a valid [`BoaProvider`]
|
/// Gets the [`LocaleCanonicalizer`] tool.
|
||||||
|
pub(crate) const fn locale_canonicalizer(&self) -> &LocaleCanonicalizer { |
||||||
|
&self.locale_canonicalizer |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the inner icu data provider
|
||||||
|
pub(crate) const fn provider(&self) -> &P { |
||||||
|
&self.provider |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the [`LocaleExpander`] tool.
|
||||||
|
pub(crate) const fn locale_expander(&self) -> &LocaleExpander { |
||||||
|
&self.locale_expander |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Icu<BoaProvider> { |
||||||
|
/// Creates a new [`Icu`] from a valid [`BoaProvider`]
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This method will return an error if any of the tools
|
/// This method will return an error if any of the tools
|
||||||
/// required cannot be constructed.
|
/// required cannot be constructed.
|
||||||
pub(crate) fn new(provider: Box<dyn BoaProvider>) -> Result<Self, DataError> { |
pub(crate) fn new(provider: BoaProvider) -> Result<Self, LocaleTransformError> { |
||||||
Ok(Self { |
Ok(Self { |
||||||
locale_canonicalizer: LocaleCanonicalizer::new(&*provider)?, |
locale_canonicalizer: provider.try_new_locale_canonicalizer()?, |
||||||
|
locale_expander: provider.try_new_locale_expander()?, |
||||||
provider, |
provider, |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
/// Get the [`LocaleCanonicalizer`] tool.
|
|
||||||
pub(crate) const fn locale_canonicalizer(&self) -> &LocaleCanonicalizer { |
|
||||||
&self.locale_canonicalizer |
|
||||||
} |
|
||||||
|
|
||||||
/// Get the inner icu data provider
|
|
||||||
#[allow(unused)] |
|
||||||
pub(crate) fn provider(&self) -> &dyn BoaProvider { |
|
||||||
self.provider.as_ref() |
|
||||||
} |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,28 @@ |
|||||||
|
[package] |
||||||
|
name = "boa_icu_provider" |
||||||
|
description = "ICU4X data provider for the Boa JavaScript engine." |
||||||
|
keywords = ["javascript", "cldr", "unicode"] |
||||||
|
categories = ["internationalization"] |
||||||
|
version.workspace = true |
||||||
|
edition.workspace = true |
||||||
|
authors.workspace = true |
||||||
|
license.workspace = true |
||||||
|
repository.workspace = true |
||||||
|
rust-version.workspace = true |
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
icu_provider = { version = "1.0.1", features = ["serde", "sync"] } |
||||||
|
icu_provider_blob = "1.0.0" |
||||||
|
icu_datagen = { version = "1.0.2", optional = true } |
||||||
|
log = { version = "0.4.17", optional = true } |
||||||
|
simple_logger = { version = "4.0.0", optional = true } |
||||||
|
|
||||||
|
[features] |
||||||
|
bin = ["dep:icu_datagen", "dep:simple_logger", "dep:log"] |
||||||
|
|
||||||
|
[[bin]] |
||||||
|
name = "boa-datagen" |
||||||
|
path = "src/bin/datagen.rs" |
||||||
|
required-features = ["bin"] |
@ -0,0 +1,12 @@ |
|||||||
|
# boa_icu_provider |
||||||
|
|
||||||
|
`boa_icu_provider` generates and defines the [ICU4X](https://github.com/unicode-org/icu4x) data provider |
||||||
|
used in the Boa engine to enable internationalization functionality. |
||||||
|
|
||||||
|
## Datagen |
||||||
|
|
||||||
|
To regenerate the data: |
||||||
|
|
||||||
|
```bash |
||||||
|
$ cargo run --bin boa-datagen --features bin |
||||||
|
``` |
Binary file not shown.
@ -0,0 +1,21 @@ |
|||||||
|
use std::{error::Error, fs::File}; |
||||||
|
|
||||||
|
use boa_icu_provider::data_root; |
||||||
|
use icu_datagen::{all_keys, datagen, CldrLocaleSubset, Out, SourceData}; |
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> { |
||||||
|
simple_logger::SimpleLogger::new() |
||||||
|
.env() |
||||||
|
.with_level(log::LevelFilter::Info) |
||||||
|
.init()?; |
||||||
|
|
||||||
|
let source_data = SourceData::default() |
||||||
|
.with_cldr_latest(CldrLocaleSubset::Modern)? |
||||||
|
.with_icuexport_latest()?; |
||||||
|
|
||||||
|
let blob_out = Out::Blob(Box::new(File::create( |
||||||
|
data_root().join("icudata.postcard"), |
||||||
|
)?)); |
||||||
|
|
||||||
|
datagen(None, &all_keys(), &source_data, [blob_out].into()).map_err(Into::into) |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
//! Boa's **`boa_icu_provider`** exports the default data provider used by its `Intl` implementation.
|
||||||
|
//!
|
||||||
|
//! # Crate Overview
|
||||||
|
//! This crate exports the function [`blob`], which contains an extensive dataset of locale data to
|
||||||
|
//! enable `Intl` functionality in the engine. The set of locales included is precisely the ["modern"]
|
||||||
|
//! subset of locales in the [Unicode Common Locale Data Repository][cldr].
|
||||||
|
//!
|
||||||
|
//! If you need to support the full set of locales, you can check out the [ICU4X guide] about
|
||||||
|
//! generating custom data providers. Boa supports plugging both [`BufferProvider`]s or [`AnyProvider`]s
|
||||||
|
//! generated by the tool.
|
||||||
|
//!
|
||||||
|
//! ["modern"]: https://github.com/unicode-org/cldr-json/tree/main/cldr-json/cldr-localenames-modern/main
|
||||||
|
//! [cldr]: https://github.com/unicode-org/
|
||||||
|
//! [ICU4X guide]: https://github.com/unicode-org/icu4x/blob/main/docs/tutorials/data_management.md
|
||||||
|
//! [`BufferProvider`]: icu_provider::BufferProvider
|
||||||
|
//! [`AnyProvider`]: icu_provider::AnyProvider
|
||||||
|
|
||||||
|
#![deny(
|
||||||
|
// rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html
|
||||||
|
warnings, |
||||||
|
future_incompatible, |
||||||
|
let_underscore, |
||||||
|
nonstandard_style, |
||||||
|
rust_2018_compatibility, |
||||||
|
rust_2018_idioms, |
||||||
|
rust_2021_compatibility, |
||||||
|
unused, |
||||||
|
|
||||||
|
// rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
|
||||||
|
macro_use_extern_crate, |
||||||
|
meta_variable_misuse, |
||||||
|
missing_abi, |
||||||
|
missing_copy_implementations, |
||||||
|
missing_debug_implementations, |
||||||
|
non_ascii_idents, |
||||||
|
noop_method_call, |
||||||
|
single_use_lifetimes, |
||||||
|
trivial_casts, |
||||||
|
trivial_numeric_casts, |
||||||
|
unreachable_pub, |
||||||
|
unsafe_op_in_unsafe_fn, |
||||||
|
unused_import_braces, |
||||||
|
unused_lifetimes, |
||||||
|
unused_qualifications, |
||||||
|
unused_tuple_struct_fields, |
||||||
|
variant_size_differences, |
||||||
|
|
||||||
|
// rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html
|
||||||
|
rustdoc::broken_intra_doc_links, |
||||||
|
rustdoc::private_intra_doc_links, |
||||||
|
rustdoc::missing_crate_level_docs, |
||||||
|
rustdoc::private_doc_tests, |
||||||
|
rustdoc::invalid_codeblock_attributes, |
||||||
|
rustdoc::invalid_rust_codeblocks, |
||||||
|
rustdoc::bare_urls, |
||||||
|
|
||||||
|
// clippy categories https://doc.rust-lang.org/clippy/
|
||||||
|
clippy::all, |
||||||
|
clippy::correctness, |
||||||
|
clippy::suspicious, |
||||||
|
clippy::style, |
||||||
|
clippy::complexity, |
||||||
|
clippy::perf, |
||||||
|
clippy::pedantic, |
||||||
|
clippy::nursery, |
||||||
|
)] |
||||||
|
|
||||||
|
/// Gets the path to the directory where the generated data is stored.
|
||||||
|
#[must_use] |
||||||
|
#[doc(hidden)] |
||||||
|
pub fn data_root() -> std::path::PathBuf { |
||||||
|
std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR")).join("data") |
||||||
|
} |
||||||
|
|
||||||
|
use icu_provider_blob::BlobDataProvider; |
||||||
|
|
||||||
|
/// Gets a data provider that is stored as a Postcard blob.
|
||||||
|
///
|
||||||
|
/// This provider does NOT execute locale fallback. Use `LocaleFallbackProvider` from
|
||||||
|
/// the `icu_provider_adapters` crate for this functionality.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// The returned provider internally uses [`Arc`][std::sync::Arc] to share the data between instances,
|
||||||
|
/// so it is preferrable to clone instead of calling `buffer()` multiple times.
|
||||||
|
#[must_use] |
||||||
|
pub fn blob() -> BlobDataProvider { |
||||||
|
BlobDataProvider::try_new_from_static_blob(include_bytes!(concat!( |
||||||
|
env!("CARGO_MANIFEST_DIR"), |
||||||
|
"/data/icudata.postcard" |
||||||
|
))) |
||||||
|
.expect("The statically compiled data file should be valid.") |
||||||
|
} |
Loading…
Reference in new issue