use crate::{
Context, JsNativeError, JsResult, JsValue,
builtins::{
Array,
intl::{
Service,
options::{IntlOptions, LocaleMatcher, coerce_options_to_object},
},
options::get_option,
},
context::icu::IntlProvider,
js_string,
object::JsObject,
};
use icu_locale::{LanguageIdentifier, Locale, LocaleCanonicalizer};
use icu_provider::{
DataIdentifierBorrowed, DataLocale, DataMarker, DataMarkerAttributes, DataRequest,
DataRequestMetadata, DryDataProvider,
};
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(Locale::UNKNOWN)
}
pub(crate) fn locale_from_value(tag: &JsValue, context: &mut Context) -> JsResult<Locale> {
if !(tag.is_object() || tag.is_string()) {
return Err(JsNativeError::typ()
.with_message("locale should be a String or Object")
.into());
}
let object = tag.as_object();
if let Some(tag) = object.as_ref().and_then(|obj| obj.downcast_ref::<Locale>()) {
return Ok(tag.clone());
}
let tag = tag.to_string(context)?.to_std_string_escaped();
if tag.contains('_') {
return Err(JsNativeError::range()
.with_message("locale is not a structurally valid language tag")
.into());
}
let mut tag = tag
.parse()
.map_err(|_| {
JsNativeError::range().with_message("locale is not a structurally valid language tag")
})?;
context
.intl_provider()
.locale_canonicalizer()?
.canonicalize(&mut tag);
Ok(tag)
}
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().is_some_and(|o| o.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)? {
let tag = locale_from_value(&k_value, context)?;
seen.insert(tag);
}
}
Ok(seen.into_iter().collect())
}
fn lookup_matching_locale_by_prefix<S: Service>(
requested_locales: impl IntoIterator<Item = Locale>,
provider: &IntlProvider,
) -> Option<Locale>
where
IntlProvider: DryDataProvider<S::LangMarker>,
{
for locale in requested_locales {
let mut locale = locale.clone();
let id = std::mem::replace(&mut locale.id, LanguageIdentifier::UNKNOWN);
locale.extensions.transform.clear();
locale.extensions.private.clear();
let mut prefix = id.into();
loop {
let response = DryDataProvider::dry_load(
provider,
DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
S::ATTRIBUTES,
&prefix,
),
metadata: {
let mut metadata = DataRequestMetadata::default();
metadata.silent = true;
metadata
},
},
);
if let Ok(metadata) = response {
match metadata.locale {
Some(loc) if loc.into_locale().id == prefix.into_locale().id => {
locale.id = prefix.into_locale().id;
return Some(locale);
}
None => {
locale.id = prefix.into_locale().id;
return Some(locale);
}
_ => {}
}
}
if prefix.variant.take().is_none()
&& prefix.region.take().is_none()
&& prefix.script.take().is_none()
{
break;
}
}
}
None
}
fn lookup_matching_locale_by_best_fit<S: Service>(
requested_locales: impl IntoIterator<Item = Locale>,
provider: &IntlProvider,
) -> Option<Locale>
where
IntlProvider: DryDataProvider<S::LangMarker>,
{
for mut locale in requested_locales {
let id = std::mem::replace(&mut locale.id, LanguageIdentifier::UNKNOWN);
locale.extensions.transform.clear();
locale.extensions.private.clear();
let dl = &DataLocale::from(&id);
let Ok(response) = DryDataProvider::dry_load(
provider,
DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(S::ATTRIBUTES, dl),
metadata: {
let mut md = DataRequestMetadata::default();
md.silent = true;
md
},
},
) else {
continue;
};
if id == LanguageIdentifier::UNKNOWN {
return Some(locale);
}
if let Some(id) = response
.locale
.map(|dl| dl.into_locale().id)
.or(Some(id))
.filter(|loc| loc != &LanguageIdentifier::UNKNOWN)
{
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,
) -> JsResult<Locale>
where
S: Service,
IntlProvider: DryDataProvider<S::LangMarker>,
{
let found_locale = if options.matcher == LocaleMatcher::Lookup {
lookup_matching_locale_by_prefix::<S>(requested_locales, provider)
} else {
lookup_matching_locale_by_best_fit::<S>(requested_locales, provider)
};
let mut found_locale = if let Some(loc) = found_locale {
loc
} else {
let default = default_locale(provider.locale_canonicalizer()?);
lookup_matching_locale_by_best_fit::<S>([default], provider).ok_or_else(|| {
JsNativeError::typ().with_message("could not find i18n data for Intl service")
})?
};
S::resolve(&mut found_locale, &mut options.service_options, provider);
provider
.locale_canonicalizer()?
.canonicalize(&mut found_locale);
Ok(found_locale)
}
pub(in crate::builtins::intl) fn filter_locales<S: Service>(
requested_locales: Vec<Locale>,
options: &JsValue,
context: &mut Context,
) -> JsResult<JsObject>
where
IntlProvider: DryDataProvider<S::LangMarker>,
{
let options = coerce_options_to_object(options, context)?;
let matcher = get_option(&options, js_string!("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::<S>([no_ext_loc], context.intl_provider())
}
LocaleMatcher::BestFit => {
lookup_matching_locale_by_best_fit::<S>([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: DataMarker>(
language: LanguageIdentifier,
attributes: &DataMarkerAttributes,
provider: &impl DryDataProvider<M>,
) -> bool {
let locale = DataLocale::from(language);
let req = DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(attributes, &locale),
metadata: {
let mut metadata = DataRequestMetadata::default();
metadata.silent = true;
metadata
},
};
provider.dry_load(req).is_ok()
}
#[cfg(all(test, feature = "intl_bundled"))]
mod tests {
use icu_locale::{Locale, langid, locale};
use icu_plurals::provider::PluralsCardinalV1;
struct TestService;
impl Service for TestService {
type LangMarker = PluralsCardinalV1;
type LocaleOptions = ();
}
use crate::{
builtins::intl::{
Service,
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_buffer(boa_icu_provider::buffer());
assert_eq!(
lookup_matching_locale_by_best_fit::<TestService>([locale!("en")], icu),
Some(locale!("en"))
);
assert_eq!(
lookup_matching_locale_by_best_fit::<TestService>([locale!("es-ES")], icu),
Some(locale!("es"))
);
assert_eq!(
lookup_matching_locale_by_best_fit::<TestService>([locale!("kr")], icu),
None
);
}
#[test]
fn lookup_match() {
let icu = &IntlProvider::try_new_buffer(boa_icu_provider::buffer());
let requested: Locale = "fr-FR-u-hc-h12".parse().unwrap();
let result =
lookup_matching_locale_by_prefix::<TestService>([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::<TestService>(requested, icu).unwrap();
assert_eq!(res.id, langid!("es"));
assert_eq!(res.extensions, es.extensions);
}
}