use crate::{
builtins::{
intl::{
options::{coerce_options_to_object, IntlOptions, LocaleMatcher},
Service,
},
options::get_option,
Array,
},
context::icu::IntlProvider,
js_string,
object::JsObject,
Context, JsNativeError, JsResult, JsValue,
};
use boa_macros::js_str;
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 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 {
if let Some(k_value) = o.try_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().downcast_ref::<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
.intl_provider()
.locale_canonicalizer()
.canonicalize(&mut tag);
seen.insert(tag);
}
}
Ok(seen.into_iter().collect())
}
pub(crate) fn lookup_matching_locale_by_prefix<M: KeyedDataMarker>(
requested_locales: impl IntoIterator<Item = Locale>,
provider: &IntlProvider,
) -> Option<Locale>
where
IntlProvider: DataProvider<M>,
{
for locale in requested_locales {
let mut locale = locale.clone();
let id = std::mem::take(&mut locale.id);
locale.extensions.transform.clear();
locale.extensions.private.clear();
let mut prefix = id.into();
loop {
let response = DataProvider::<M>::load(
provider,
DataRequest {
locale: &prefix,
metadata: {
let mut metadata = DataRequestMetadata::default();
metadata.silent = true;
metadata
},
},
);
if let Ok(req) = response {
match req.metadata.locale {
Some(loc) if loc.get_langid() == prefix.get_langid() => {
locale.id = loc.into_locale().id;
return Some(locale);
}
None => {
locale.id = prefix.into_locale().id;
return Some(locale);
}
_ => {}
}
}
if prefix.has_variants() {
let mut variants = prefix.clear_variants().iter().copied().collect::<Vec<_>>();
variants.pop();
prefix.set_variants(Variants::from_vec_unchecked(variants));
} else if prefix.region().is_some() {
prefix.set_region(None);
} else if prefix.script().is_some() {
prefix.set_script(None);
} else {
break;
}
}
}
None
}
fn lookup_matching_locale_by_best_fit<M: KeyedDataMarker>(
requested_locales: impl IntoIterator<Item = Locale>,
provider: &IntlProvider,
) -> Option<Locale>
where
IntlProvider: DataProvider<M>,
{
for mut locale in requested_locales {
let id = std::mem::take(&mut locale.id);
locale.extensions.transform.clear();
locale.extensions.private.clear();
let Ok(response) = DataProvider::<M>::load(
provider,
DataRequest {
locale: &DataLocale::from(&id),
metadata: {
let mut md = DataRequestMetadata::default();
md.silent = true;
md
},
},
) else {
continue;
};
if id == LanguageIdentifier::UND {
return Some(locale);
}
if let Some(id) = response
.metadata
.locale
.map(|dl| dl.into_locale().id)
.or(Some(id))
.filter(|loc| loc != &LanguageIdentifier::UND)
{
locale.id = id;
return Some(locale);
}
}
None
}
pub(in crate::builtins::intl) fn resolve_locale<S>(
requested_locales: impl IntoIterator<Item = Locale>,
options: &mut IntlOptions<S::LocaleOptions>,
provider: &IntlProvider,
) -> Locale
where
S: Service,
IntlProvider: DataProvider<S::LangMarker>,
{
let mut found_locale = if options.matcher == LocaleMatcher::Lookup {
lookup_matching_locale_by_prefix::<S::LangMarker>(requested_locales, provider)
} else {
lookup_matching_locale_by_best_fit::<S::LangMarker>(requested_locales, provider)
}
.unwrap_or_else(|| default_locale(provider.locale_canonicalizer()));
S::resolve(&mut found_locale, &mut options.service_options, provider);
provider
.locale_canonicalizer()
.canonicalize(&mut found_locale);
found_locale
}
pub(in crate::builtins::intl) fn filter_locales<M: KeyedDataMarker>(
requested_locales: Vec<Locale>,
options: &JsValue,
context: &mut Context,
) -> JsResult<JsObject>
where
IntlProvider: DataProvider<M>,
{
let options = coerce_options_to_object(options, context)?;
let matcher = get_option(&options, js_str!("localeMatcher"), context)?.unwrap_or_default();
let mut subset = Vec::with_capacity(requested_locales.len());
for locale in requested_locales {
let mut no_ext_loc = locale.clone();
no_ext_loc.extensions.unicode.clear();
let loc_match = match matcher {
LocaleMatcher::Lookup => {
lookup_matching_locale_by_prefix([no_ext_loc], context.intl_provider())
}
LocaleMatcher::BestFit => {
lookup_matching_locale_by_best_fit([no_ext_loc], context.intl_provider())
}
};
if loc_match.is_some() {
subset.push(locale);
}
}
Ok(Array::create_array_from_list(
subset
.into_iter()
.map(|loc| js_string!(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(all(test, feature = "intl_bundled"))]
mod tests {
use icu_locid::{langid, locale, Locale};
use icu_plurals::provider::CardinalV1Marker;
use crate::{
builtins::intl::locale::utils::{
lookup_matching_locale_by_best_fit, lookup_matching_locale_by_prefix,
},
context::icu::IntlProvider,
};
#[test]
fn best_fit() {
let icu = &IntlProvider::try_new_with_buffer_provider(boa_icu_provider::buffer()).unwrap();
assert_eq!(
lookup_matching_locale_by_best_fit::<CardinalV1Marker>([locale!("en")], icu),
Some(locale!("en"))
);
assert_eq!(
lookup_matching_locale_by_best_fit::<CardinalV1Marker>([locale!("es-ES")], icu),
Some(locale!("es"))
);
assert_eq!(
lookup_matching_locale_by_best_fit::<CardinalV1Marker>([locale!("kr")], icu),
None
);
}
#[test]
fn lookup_match() {
let icu = &IntlProvider::try_new_with_buffer_provider(boa_icu_provider::buffer()).unwrap();
let requested: Locale = "fr-FR-u-hc-h12".parse().unwrap();
let result =
lookup_matching_locale_by_prefix::<CardinalV1Marker>([requested.clone()], icu).unwrap();
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 = lookup_matching_locale_by_best_fit::<CardinalV1Marker>(requested, icu).unwrap();
assert_eq!(res.id, langid!("es"));
assert_eq!(res.extensions, es.extensions);
}
}