use crate::{
builtins::intl::date_time_format::DateTimeFormat,
builtins::{Array, BuiltIn, JsArgs},
object::{JsObject, ObjectInitializer},
property::Attribute,
symbol::WellKnownSymbols,
Context, JsResult, JsString, JsValue,
};
pub mod date_time_format;
#[cfg(test)]
mod tests;
use boa_profiler::Profiler;
use icu_locale_canonicalizer::LocaleCanonicalizer;
use icu_locid::{locale, Locale};
use indexmap::IndexSet;
use rustc_hash::FxHashMap;
use tap::{Conv, Pipe, TapOptional};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Intl;
impl BuiltIn for Intl {
const NAME: &'static str = "Intl";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let string_tag = WellKnownSymbols::to_string_tag();
let date_time_format = DateTimeFormat::init(context);
ObjectInitializer::new(context)
.function(Self::get_canonical_locales, "getCanonicalLocales", 1)
.property(
string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"DateTimeFormat",
date_time_format,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Intl {
pub(crate) fn get_canonical_locales(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ll = canonicalize_locale_list(args, context)?;
Ok(JsValue::Object(Array::create_array_from_list(
ll.into_iter().map(|loc| loc.to_string().into()),
context,
)))
}
}
#[derive(Debug)]
struct MatcherRecord {
locale: JsString,
extension: JsString,
}
fn default_locale(canonicalizer: &LocaleCanonicalizer) -> Locale {
sys_locale::get_locale()
.and_then(|loc| loc.parse::<Locale>().ok())
.tap_some_mut(|loc| canonicalize_unicode_locale_id(loc, canonicalizer))
.unwrap_or(locale!("en-US"))
}
fn best_available_locale(available_locales: &[JsString], locale: &JsString) -> Option<JsString> {
let mut candidate = locale.clone();
loop {
if available_locales.contains(&candidate) {
return Some(candidate);
}
let pos = candidate.rfind('-');
match pos {
Some(ind) => {
let tmp_candidate = candidate[..ind].to_string();
let prev_dash = tmp_candidate.rfind('-').unwrap_or(ind);
let trim_ind = if ind >= 2 && prev_dash == ind - 2 {
ind - 2
} else {
ind
};
candidate = JsString::new(&candidate[..trim_ind]);
}
None => return None,
}
}
}
fn lookup_matcher(
available_locales: &[JsString],
requested_locales: &[JsString],
canonicalizer: &LocaleCanonicalizer,
) -> MatcherRecord {
for locale_str in requested_locales {
let parsed_locale =
Locale::from_bytes(locale_str.as_bytes()).expect("Locale parsing failed");
let no_extensions_locale = JsString::new(parsed_locale.id.to_string());
let available_locale = best_available_locale(available_locales, &no_extensions_locale);
if let Some(available_locale) = available_locale {
let maybe_ext = if locale_str.eq(&no_extensions_locale) {
JsString::empty()
} else {
JsString::new(parsed_locale.extensions.to_string())
};
return MatcherRecord {
locale: available_locale,
extension: maybe_ext,
};
}
}
MatcherRecord {
locale: default_locale(canonicalizer).to_string().into(),
extension: JsString::empty(),
}
}
fn best_fit_matcher(
available_locales: &[JsString],
requested_locales: &[JsString],
canonicalizer: &LocaleCanonicalizer,
) -> MatcherRecord {
lookup_matcher(available_locales, requested_locales, canonicalizer)
}
#[derive(Debug)]
struct Keyword {
key: JsString,
value: JsString,
}
#[allow(dead_code)]
#[derive(Debug)]
struct UniExtRecord {
attributes: Vec<JsString>, keywords: Vec<Keyword>,
}
fn unicode_extension_components(extension: &JsString) -> UniExtRecord {
let mut attributes = Vec::<JsString>::new();
let mut keywords = Vec::<Keyword>::new();
let mut keyword: Option<Keyword> = None;
let size = extension.len();
let mut k = 3;
while k < size {
let e = extension.index_of(&JsString::new("-"), k);
let len = match e {
Some(pos) => pos - k,
None => size - k,
};
let subtag = JsString::new(&extension[k..k + len]);
if keyword.is_none() && len != 2 {
if !attributes.contains(&subtag) {
attributes.push(subtag);
}
} else if len == 2 {
if let Some(keyword_val) = keyword {
let has_key = keywords.iter().any(|elem| elem.key == keyword_val.key);
if !has_key {
keywords.push(keyword_val);
}
};
keyword = Some(Keyword {
key: subtag,
value: JsString::empty(),
});
} else {
if let Some(keyword_val) = keyword {
let new_keyword_val = if keyword_val.value.is_empty() {
subtag
} else {
JsString::new(format!("{}-{subtag}", keyword_val.value))
};
keyword = Some(Keyword {
key: keyword_val.key,
value: new_keyword_val,
});
};
}
k = k + len + 1;
}
if let Some(keyword_val) = keyword {
let has_key = keywords.iter().any(|elem| elem.key == keyword_val.key);
if !has_key {
keywords.push(keyword_val);
}
};
UniExtRecord {
attributes,
keywords,
}
}
fn insert_unicode_extension_and_canonicalize(
locale: &str,
extension: &str,
canonicalizer: &LocaleCanonicalizer,
) -> JsString {
let private_index = locale.find("-x-");
let new_locale = match private_index {
None => {
locale.to_owned() + extension
}
Some(idx) => {
let pre_extension = &locale[0..idx];
let post_extension = &locale[idx..];
pre_extension.to_owned() + extension + post_extension
}
};
let mut new_locale = new_locale
.parse()
.expect("Assert: ! IsStructurallyValidLanguageTag(locale) is true.");
canonicalize_unicode_locale_id(&mut new_locale, canonicalizer);
new_locale.to_string().into()
}
fn canonicalize_locale_list(args: &[JsValue], context: &mut Context) -> JsResult<Vec<Locale>> {
let locales = args.get_or_undefined(0);
if locales.is_undefined() {
return Ok(Vec::new());
}
let mut seen = IndexSet::new();
let o = if locales.is_string() {
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 context.throw_type_error("locale should be a String or Object");
}
let tag = k_value.to_string(context)?;
let mut tag = tag.parse().map_err(|_| {
context.construct_range_error("locale is not a structurally valid language tag")
})?;
canonicalize_unicode_locale_id(&mut tag, context.icu().locale_canonicalizer());
seen.insert(tag);
}
}
Ok(seen.into_iter().collect())
}
type LocaleDataRecord = FxHashMap<JsString, FxHashMap<JsString, Vec<JsString>>>;
#[derive(Debug)]
struct DateTimeFormatRecord {
pub(crate) locale_matcher: JsString,
pub(crate) properties: FxHashMap<JsString, JsValue>,
}
#[derive(Debug)]
struct ResolveLocaleRecord {
pub(crate) locale: JsString,
pub(crate) properties: FxHashMap<JsString, JsValue>,
pub(crate) data_locale: JsString,
}
#[allow(dead_code)]
fn resolve_locale(
available_locales: &[JsString],
requested_locales: &[JsString],
options: &DateTimeFormatRecord,
relevant_extension_keys: &[JsString],
locale_data: &LocaleDataRecord,
context: &mut Context,
) -> ResolveLocaleRecord {
let matcher = &options.locale_matcher;
let r = if matcher.eq(&JsString::new("lookup")) {
lookup_matcher(
available_locales,
requested_locales,
context.icu().locale_canonicalizer(),
)
} else {
best_fit_matcher(
available_locales,
requested_locales,
context.icu().locale_canonicalizer(),
)
};
let mut found_locale = r.locale;
let mut result = ResolveLocaleRecord {
locale: JsString::empty(),
properties: FxHashMap::default(),
data_locale: JsString::empty(),
};
result.data_locale = found_locale.clone();
let keywords = if r.extension.is_empty() {
Vec::<Keyword>::new()
} else {
let components = unicode_extension_components(&r.extension);
components.keywords
};
let mut supported_extension = JsString::new("-u");
for key in relevant_extension_keys {
let found_locale_data = match locale_data.get(&found_locale) {
Some(locale_value) => locale_value.clone(),
None => FxHashMap::default(),
};
let key_locale_data = match found_locale_data.get(key) {
Some(locale_vec) => locale_vec.clone(),
None => Vec::new(),
};
let mut value = match key_locale_data.get(0) {
Some(first_elt) => JsValue::String(first_elt.clone()),
None => JsValue::null(),
};
let mut supported_extension_addition = JsString::empty();
if !r.extension.is_empty() {
let maybe_entry = keywords.iter().find(|elem| key.eq(&elem.key));
if let Some(entry) = maybe_entry {
let requested_value = &entry.value;
if !requested_value.is_empty() {
if key_locale_data.contains(requested_value) {
value = JsValue::String(JsString::new(requested_value));
supported_extension_addition =
JsString::concat_array(&["-", key, "-", requested_value]);
}
} else if key_locale_data.contains(&JsString::new("true")) {
value = JsValue::String(JsString::new("true"));
supported_extension_addition = JsString::concat_array(&["-", key]);
}
}
}
if options.properties.contains_key(key) {
let mut options_value = options
.properties
.get(key)
.unwrap_or(&JsValue::undefined())
.clone();
if options_value.is_string() {
if let Some(options_val_str) = options_value.as_string() {
if options_val_str.is_empty() {
options_value = JsValue::String(JsString::new("true"));
}
}
}
let options_val_str = options_value
.to_string(context)
.unwrap_or_else(|_| JsString::empty());
if key_locale_data.contains(&options_val_str) {
if !options_value.eq(&value) {
value = options_value;
supported_extension_addition = JsString::empty();
}
}
}
result.properties.insert(key.clone(), value);
supported_extension = JsString::concat(supported_extension, &supported_extension_addition);
}
if supported_extension.len() > 2 {
found_locale = insert_unicode_extension_and_canonicalize(
&found_locale,
&supported_extension,
context.icu().locale_canonicalizer(),
);
}
result.locale = found_locale;
result
}
#[allow(unused)]
pub(crate) enum GetOptionType {
String,
Boolean,
}
#[allow(unused)]
pub(crate) fn get_option(
options: &JsObject,
property: &str,
r#type: &GetOptionType,
values: &[JsString],
fallback: &JsValue,
context: &mut Context,
) -> JsResult<JsValue> {
let mut value = options.get(property, context)?;
if value.is_undefined() {
return Ok(fallback.clone());
}
value = match r#type {
GetOptionType::Boolean => JsValue::Boolean(value.to_boolean()),
GetOptionType::String => {
let string_value = value.to_string(context)?;
if !values.is_empty() && !values.contains(&string_value) {
return context.throw_range_error("GetOption: values array does not contain value");
}
JsValue::String(string_value)
}
};
Ok(value)
}
#[allow(unused)]
pub(crate) fn get_number_option(
options: &JsObject,
property: &str,
minimum: f64,
maximum: f64,
fallback: Option<f64>,
context: &mut Context,
) -> JsResult<Option<f64>> {
let value = options.get(property, context)?;
default_number_option(&value, minimum, maximum, fallback, context)
}
#[allow(unused)]
pub(crate) fn default_number_option(
value: &JsValue,
minimum: f64,
maximum: f64,
fallback: Option<f64>,
context: &mut Context,
) -> JsResult<Option<f64>> {
if value.is_undefined() {
return Ok(fallback);
}
let value = value.to_number(context)?;
if value.is_nan() || value < minimum || value > maximum {
return context.throw_range_error("DefaultNumberOption: value is out of range.");
}
Ok(Some(value.floor()))
}
fn canonicalize_unicode_locale_id(locale: &mut Locale, canonicalizer: &LocaleCanonicalizer) {
canonicalizer.canonicalize(locale);
}