use crate::{
builtins::{
intl::{
options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher},
Service,
},
Array,
},
context::{icu::Icu, BoaProvider},
object::JsObject,
string::utf16,
Context, JsNativeError, JsResult, JsValue,
};
use icu_collator::provider::CollationMetadataV1Marker;
use icu_locid::{
extensions::unicode::{Key, Value},
subtags::Variants,
LanguageIdentifier, Locale,
};
use icu_locid_transform::LocaleCanonicalizer;
use icu_provider::{DataLocale, DataProvider, DataRequest, DataRequestMetadata, KeyedDataMarker};
use icu_segmenter::provider::WordBreakDataV1Marker;
use indexmap::IndexSet;
use tap::TapOptional;
pub(crate) fn default_locale(canonicalizer: &LocaleCanonicalizer) -> Locale {
sys_locale::get_locale()
.and_then(|loc| loc.parse::<Locale>().ok())
.tap_some_mut(|loc| {
canonicalizer.canonicalize(loc);
})
.unwrap_or_default()
}
pub(crate) fn canonicalize_locale_list(
locales: &JsValue,
context: &mut Context<'_>,
) -> JsResult<Vec<Locale>> {
if locales.is_undefined() {
return Ok(Vec::default());
}
let mut seen = IndexSet::new();
let o = if locales.is_string()
|| locales
.as_object()
.map_or(false, |o| o.borrow().is_locale())
{
Array::create_array_from_list([locales.clone()], context)
} else {
locales.to_object(context)?
};
let len = o.length_of_array_like(context)?;
for k in 0..len {
let k_present = o.has_property(k, context)?;
if k_present {
let k_value = o.get(k, context)?;
if !(k_value.is_object() || k_value.is_string()) {
return Err(JsNativeError::typ()
.with_message("locale should be a String or Object")
.into());
}
let mut tag = if let Some(tag) = k_value
.as_object()
.and_then(|obj| obj.borrow().as_locale().cloned())
{
tag
}
else {
let k_value = k_value.to_string(context)?.to_std_string_escaped();
if k_value.contains('_') {
return Err(JsNativeError::range()
.with_message("locale is not a structurally valid language tag")
.into());
}
k_value
.parse()
.map_err(|_| {
JsNativeError::range()
.with_message("locale is not a structurally valid language tag")
})?
};
context.icu().locale_canonicalizer().canonicalize(&mut tag);
seen.insert(tag);
}
}
Ok(seen.into_iter().collect())
}
pub(crate) fn best_available_locale<M: KeyedDataMarker>(
candidate: LanguageIdentifier,
provider: &(impl DataProvider<M> + ?Sized),
) -> Option<LanguageIdentifier> {
let mut candidate = candidate.into();
loop {
let response = DataProvider::<M>::load(
provider,
DataRequest {
locale: &candidate,
metadata: {
let mut metadata = DataRequestMetadata::default();
metadata.silent = true;
metadata
},
},
);
if let Ok(req) = response {
match req.metadata.locale {
Some(loc)
if loc == candidate
|| (loc.is_empty()
&& [
CollationMetadataV1Marker::KEY.path(),
WordBreakDataV1Marker::KEY.path(),
]
.contains(&M::KEY.path())) =>
{
return Some(candidate.into_locale().id)
}
None => return Some(candidate.into_locale().id),
_ => {}
}
}
if candidate.has_variants() {
let mut variants = candidate
.clear_variants()
.iter()
.copied()
.collect::<Vec<_>>();
variants.pop();
candidate.set_variants(Variants::from_vec_unchecked(variants));
} else if candidate.region().is_some() {
candidate.set_region(None);
} else if candidate.script().is_some() {
candidate.set_script(None);
} else {
return None;
}
}
}
pub(crate) fn best_locale_for_provider<M: KeyedDataMarker>(
candidate: LanguageIdentifier,
provider: &(impl DataProvider<M> + ?Sized),
) -> Option<LanguageIdentifier> {
let response = DataProvider::<M>::load(
provider,
DataRequest {
locale: &DataLocale::from(&candidate),
metadata: DataRequestMetadata::default(),
},
)
.ok()?;
if candidate == LanguageIdentifier::UND {
return Some(LanguageIdentifier::UND);
}
response
.metadata
.locale
.map(|dl| {
if [
CollationMetadataV1Marker::KEY.path(),
WordBreakDataV1Marker::KEY.path(),
]
.contains(&M::KEY.path())
&& dl.is_empty()
{
candidate.clone()
} else {
dl.into_locale().id
}
})
.or(Some(candidate))
.filter(|loc| loc != &LanguageIdentifier::UND)
}
fn lookup_matcher<'provider, M: KeyedDataMarker>(
requested_locales: &[Locale],
icu: &Icu<'provider>,
) -> Locale
where
BoaProvider<'provider>: DataProvider<M>,
{
for locale in requested_locales {
let mut locale = locale.clone();
let id = std::mem::take(&mut locale.id);
let available_locale = best_available_locale::<M>(id, &icu.provider());
if let Some(available_locale) = available_locale {
locale.id = available_locale;
return locale;
}
}
default_locale(icu.locale_canonicalizer())
}
fn best_fit_matcher<'provider, M: KeyedDataMarker>(
requested_locales: &[Locale],
icu: &Icu<'provider>,
) -> Locale
where
BoaProvider<'provider>: DataProvider<M>,
{
for mut locale in requested_locales
.iter()
.cloned()
.chain(std::iter::once_with(|| {
default_locale(icu.locale_canonicalizer())
}))
{
let id = std::mem::take(&mut locale.id);
if let Some(available) = best_locale_for_provider(id, &icu.provider()) {
locale.id = available;
return locale;
}
}
Locale::default()
}
pub(in crate::builtins::intl) fn resolve_locale<'provider, S>(
requested_locales: &[Locale],
options: &mut IntlOptions<S::LocaleOptions>,
icu: &Icu<'provider>,
) -> Locale
where
S: Service,
BoaProvider<'provider>: DataProvider<S::LangMarker>,
{
let mut found_locale = if options.matcher == LocaleMatcher::Lookup {
lookup_matcher::<S::LangMarker>(requested_locales, icu)
} else {
best_fit_matcher::<S::LangMarker>(requested_locales, icu)
};
S::resolve(
&mut found_locale,
&mut options.service_options,
icu.provider(),
);
icu.locale_canonicalizer().canonicalize(&mut found_locale);
found_locale
}
fn lookup_supported_locales<M: KeyedDataMarker>(
requested_locales: &[Locale],
provider: &impl DataProvider<M>,
) -> Vec<Locale> {
requested_locales
.iter()
.cloned()
.filter(|loc| best_available_locale(loc.id.clone(), provider).is_some())
.collect()
}
fn best_fit_supported_locales<M: KeyedDataMarker>(
requested_locales: &[Locale],
provider: &impl DataProvider<M>,
) -> Vec<Locale> {
requested_locales
.iter()
.cloned()
.filter(|loc| best_locale_for_provider(loc.id.clone(), provider).is_some())
.collect()
}
pub(in crate::builtins::intl) fn supported_locales<'ctx, 'icu: 'ctx, M: KeyedDataMarker>(
requested_locales: &[Locale],
options: &JsValue,
context: &'ctx mut Context<'icu>,
) -> JsResult<JsObject>
where
BoaProvider<'icu>: DataProvider<M>,
{
let options = coerce_options_to_object(options, context)?;
let matcher = get_option::<LocaleMatcher>(&options, utf16!("localeMatcher"), false, context)?
.unwrap_or_default();
let elements = match matcher {
LocaleMatcher::Lookup => {
lookup_supported_locales(requested_locales, &context.icu().provider())
}
LocaleMatcher::BestFit => {
best_fit_supported_locales(requested_locales, &context.icu().provider())
}
};
Ok(Array::create_array_from_list(
elements.into_iter().map(|loc| loc.to_string().into()),
context,
))
}
pub(in crate::builtins::intl) fn validate_extension<M: KeyedDataMarker>(
language: LanguageIdentifier,
key: Key,
value: &Value,
provider: &impl DataProvider<M>,
) -> bool {
let mut locale = DataLocale::from(language);
locale.set_unicode_ext(key, value.clone());
let request = DataRequest {
locale: &locale,
metadata: DataRequestMetadata::default(),
};
DataProvider::load(provider, request)
.ok()
.map(|res| res.metadata.locale.unwrap_or_else(|| locale.clone()))
.filter(|loc| loc == &locale)
.is_some()
}
#[cfg(test)]
mod tests {
use icu_locid::{langid, locale, Locale};
use icu_plurals::provider::CardinalV1Marker;
use icu_provider::{AsDeserializingBufferProvider, BufferProvider};
use crate::{
builtins::intl::locale::utils::{
best_available_locale, best_fit_matcher, default_locale, lookup_matcher,
},
context::icu::{BoaProvider, Icu},
};
#[test]
fn best_avail_loc() {
let provider = boa_icu_provider::buffer();
let provider = provider.as_deserializing();
assert_eq!(
best_available_locale::<CardinalV1Marker>(langid!("en"), &provider),
Some(langid!("en"))
);
assert_eq!(
best_available_locale::<CardinalV1Marker>(langid!("es-ES"), &provider),
Some(langid!("es"))
);
assert_eq!(
best_available_locale::<CardinalV1Marker>(langid!("kr"), &provider),
None
);
}
#[test]
fn lookup_match() {
let provider: &dyn BufferProvider = boa_icu_provider::buffer();
let icu = Icu::new(BoaProvider::Buffer(provider)).unwrap();
let res = lookup_matcher::<CardinalV1Marker>(&[], &icu);
assert_eq!(res, default_locale(icu.locale_canonicalizer()));
assert!(res.extensions.is_empty());
let requested: Locale = "fr-FR-u-hc-h12".parse().unwrap();
let result = lookup_matcher::<CardinalV1Marker>(&[requested.clone()], &icu);
assert_eq!(result.id, langid!("fr"));
assert_eq!(result.extensions, requested.extensions);
let kr = "kr-KR-u-hc-h12".parse().unwrap();
let gr = "gr-GR-u-hc-h24-x-4a".parse().unwrap();
let es: Locale = "es-ES-valencia-u-ca-gregory".parse().unwrap();
let uz = locale!("uz-Cyrl");
let requested = vec![kr, gr, es.clone(), uz];
let res = best_fit_matcher::<CardinalV1Marker>(&requested, &icu);
assert_eq!(res.id, langid!("es"));
assert_eq!(res.extensions, es.extensions);
}
}