Skip to main content

boa_engine/builtins/intl/
mod.rs

1//! Boa's implementation of ECMAScript's global `Intl` object.
2//!
3//! The `Intl` namespace object contains several constructors as well as functionality common to the
4//! internationalization constructors and other language sensitive functions. Collectively, they
5//! comprise the ECMAScript Internationalization API, which provides language sensitive string
6//! comparison, number formatting, date and time formatting, and more.
7//!
8//! More information:
9//!  - [ECMAScript reference][spec]
10//!  - [MDN documentation][mdn]
11//!
12//!
13//! [spec]: https://tc39.es/ecma402/#intl-object
14//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl
15
16use crate::{
17    Context, JsArgs, JsData, JsResult, JsString, JsValue,
18    builtins::{Array, BuiltInBuilder, BuiltInObject, IntrinsicObject},
19    context::{icu::IntlProvider, intrinsics::Intrinsics},
20    js_string,
21    object::JsObject,
22    property::Attribute,
23    realm::Realm,
24    string::StaticJsStrings,
25    symbol::JsSymbol,
26};
27
28use boa_gc::{Finalize, Trace};
29use icu_locale::{LanguageIdentifier, extensions::unicode};
30use icu_provider::{DataMarker, DataMarkerAttributes};
31use static_assertions::const_assert;
32
33pub(crate) use self::{
34    collator::Collator, date_time_format::DateTimeFormat, list_format::ListFormat, locale::Locale,
35    number_format::NumberFormat, plural_rules::PluralRules, segmenter::Segmenter,
36};
37
38/// Macro to easily implement `ServicePreferences`.
39///
40/// This macro receives a list of fields, and adds the methods to
41/// correctly implement `ServicePreferences` from the provided fields.
42macro_rules! impl_service_preferences {
43    ($($field:ident),*) => {
44        fn extended(&self, other: &Self) -> Self {
45            let mut result = *self;
46            result.extend(*other);
47            result
48        }
49
50        fn as_unicode(&self) -> unicode::Unicode {
51            let mut exts = unicode::Unicode::new();
52
53            $(
54                if let Some(key) = &self.$field
55                    && let Some((key, value)) = $crate::builtins::intl::get_kv_from_pref(key)
56                {
57                    exts.keywords.set(key, value);
58                }
59            )*
60
61            exts
62        }
63
64        fn intersection(&self, other: &Self) -> Self {
65            let mut inter = *self;
66            if inter.locale_preferences != other.locale_preferences {
67                inter.locale_preferences = LocalePreferences::default();
68            }
69
70            $(
71                if inter.$field != other.$field {
72                    inter.$field.take();
73                }
74            )*
75
76            inter
77        }
78    };
79}
80
81pub(crate) mod collator;
82pub(crate) mod date_time_format;
83pub(crate) mod list_format;
84pub(crate) mod locale;
85pub(crate) mod number_format;
86pub(crate) mod plural_rules;
87pub(crate) mod segmenter;
88
89mod options;
90
91// No singletons are allowed as lang markers.
92// Hopefully, we'll be able to migrate this to the definition of `Service` in the future
93// (https://github.com/rust-lang/rust/issues/76560)
94const_assert! {!<Collator as Service>::LangMarker::INFO.is_singleton}
95const_assert! {!<ListFormat as Service>::LangMarker::INFO.is_singleton}
96const_assert! {!<NumberFormat as Service>::LangMarker::INFO.is_singleton}
97const_assert! {!<PluralRules as Service>::LangMarker::INFO.is_singleton}
98const_assert! {!<Segmenter as Service>::LangMarker::INFO.is_singleton}
99const_assert! {!<DateTimeFormat as Service>::LangMarker::INFO.is_singleton}
100
101/// JavaScript `Intl` object.
102#[derive(Debug, Clone, Trace, Finalize, JsData)]
103#[boa_gc(unsafe_empty_trace)]
104pub struct Intl {
105    fallback_symbol: JsSymbol,
106}
107
108impl Intl {
109    /// Gets this realm's `Intl` object's `[[FallbackSymbol]]` slot.
110    #[must_use]
111    pub fn fallback_symbol(&self) -> JsSymbol {
112        self.fallback_symbol.clone()
113    }
114
115    pub(crate) fn new() -> Option<Self> {
116        let fallback_symbol = JsSymbol::new(Some(js_string!("IntlLegacyConstructedSymbol")))?;
117        Some(Self { fallback_symbol })
118    }
119}
120
121impl IntrinsicObject for Intl {
122    fn init(realm: &Realm) {
123        BuiltInBuilder::with_intrinsic::<Self>(realm)
124            .static_property(
125                JsSymbol::to_string_tag(),
126                Self::NAME,
127                Attribute::CONFIGURABLE,
128            )
129            .static_property(
130                Collator::NAME,
131                realm.intrinsics().constructors().collator().constructor(),
132                Collator::ATTRIBUTE,
133            )
134            .static_property(
135                ListFormat::NAME,
136                realm
137                    .intrinsics()
138                    .constructors()
139                    .list_format()
140                    .constructor(),
141                ListFormat::ATTRIBUTE,
142            )
143            .static_property(
144                Locale::NAME,
145                realm.intrinsics().constructors().locale().constructor(),
146                Locale::ATTRIBUTE,
147            )
148            .static_property(
149                Segmenter::NAME,
150                realm.intrinsics().constructors().segmenter().constructor(),
151                Segmenter::ATTRIBUTE,
152            )
153            .static_property(
154                PluralRules::NAME,
155                realm
156                    .intrinsics()
157                    .constructors()
158                    .plural_rules()
159                    .constructor(),
160                PluralRules::ATTRIBUTE,
161            )
162            .static_property(
163                DateTimeFormat::NAME,
164                realm
165                    .intrinsics()
166                    .constructors()
167                    .date_time_format()
168                    .constructor(),
169                DateTimeFormat::ATTRIBUTE,
170            )
171            .static_property(
172                NumberFormat::NAME,
173                realm
174                    .intrinsics()
175                    .constructors()
176                    .number_format()
177                    .constructor(),
178                NumberFormat::ATTRIBUTE,
179            )
180            .static_method(
181                Self::get_canonical_locales,
182                js_string!("getCanonicalLocales"),
183                1,
184            )
185            .build();
186    }
187
188    fn get(intrinsics: &Intrinsics) -> JsObject {
189        intrinsics.objects().intl().upcast()
190    }
191}
192
193impl BuiltInObject for Intl {
194    const NAME: JsString = StaticJsStrings::INTL;
195}
196
197impl Intl {
198    /// `Intl.getCanonicalLocales ( locales )`
199    ///
200    /// Returns an array containing the canonical locale names.
201    ///
202    /// More information:
203    ///  - [ECMAScript reference][spec]
204    ///  - [MDN docs][mdn]
205    ///
206    /// [spec]: https://tc39.es/ecma402/#sec-intl.getcanonicallocales
207    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/getCanonicalLocales
208    pub(crate) fn get_canonical_locales(
209        _: &JsValue,
210        args: &[JsValue],
211        context: &mut Context,
212    ) -> JsResult<JsValue> {
213        let locales = args.get_or_undefined(0);
214
215        // 1. Let ll be ? CanonicalizeLocaleList(locales).
216        let ll = locale::canonicalize_locale_list(locales, context)?;
217
218        // 2. Return CreateArrayFromList(ll).
219        Ok(JsValue::new(Array::create_array_from_list(
220            ll.into_iter().map(|loc| js_string!(loc.to_string()).into()),
221            context,
222        )))
223    }
224}
225
226fn get_kv_from_pref<T: icu_locale::preferences::PreferenceKey>(
227    pref: &T,
228) -> Option<(unicode::Key, unicode::Value)> {
229    T::unicode_extension_key().zip(pref.unicode_extension_value())
230}
231
232/// A set of preferences that can be provided to a [`Service`] through
233/// a locale.
234trait ServicePreferences: for<'a> From<&'a icu_locale::Locale> + Clone {
235    /// Validates that every preference value is available.
236    ///
237    /// This usually entails having to query the `IntlProvider` to check
238    /// if it has the required data to support the requested values.
239    fn validate(&mut self, id: &LanguageIdentifier, provider: &IntlProvider);
240
241    /// Converts this set of preferences into a Unicode locale extension.
242    fn as_unicode(&self) -> unicode::Unicode;
243
244    /// Extends all values set in `self` with the values set in `other`.
245    fn extended(&self, other: &Self) -> Self;
246
247    /// Gets the set of preference values that are the same in `self` and `other`.
248    fn intersection(&self, other: &Self) -> Self;
249}
250
251/// A service component that is part of the `Intl` API.
252///
253/// This needs to be implemented for every `Intl` service in order to use the functions
254/// defined in `locale::utils`, such as [`resolve_locale`][locale::resolve_locale].
255trait Service {
256    /// The data marker used to decide which locales are supported by this service.
257    type LangMarker: DataMarker;
258
259    /// The attributes used to resolve the locale.
260    const ATTRIBUTES: &'static DataMarkerAttributes = DataMarkerAttributes::empty();
261
262    /// The set of preferences used to resolve the provided locale.
263    type Preferences: ServicePreferences;
264}