View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.intl;
16  
17  import static org.htmlunit.BrowserVersionFeatures.JS_INTL_V8_BREAK_ITERATOR;
18  
19  import java.lang.reflect.Method;
20  import java.util.ArrayList;
21  import java.util.IllformedLocaleException;
22  import java.util.LinkedHashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.apache.commons.lang3.LocaleUtils;
28  import org.htmlunit.BrowserVersion;
29  import org.htmlunit.corejs.javascript.Context;
30  import org.htmlunit.corejs.javascript.Function;
31  import org.htmlunit.corejs.javascript.FunctionObject;
32  import org.htmlunit.corejs.javascript.NativeArray;
33  import org.htmlunit.corejs.javascript.Scriptable;
34  import org.htmlunit.corejs.javascript.ScriptableObject;
35  import org.htmlunit.corejs.javascript.SymbolKey;
36  import org.htmlunit.corejs.javascript.TopLevel;
37  import org.htmlunit.corejs.javascript.VarScope;
38  import org.htmlunit.javascript.HtmlUnitScriptable;
39  import org.htmlunit.javascript.JavaScriptEngine;
40  import org.htmlunit.javascript.configuration.AbstractJavaScriptConfiguration;
41  import org.htmlunit.javascript.configuration.ClassConfiguration;
42  import org.htmlunit.javascript.configuration.JsxClass;
43  import org.htmlunit.javascript.configuration.JsxStaticFunction;
44  
45  /**
46   * A JavaScript object for Intl.
47   *
48   * @author Ahmed Ashour
49   * @author Lai Quang Duong
50   * @author Ronald Brill
51   */
52  @JsxClass
53  public class Intl extends HtmlUnitScriptable {
54  
55      /**
56       * Initialize the Intl object and register it on the global scope.
57       * @param scope the top-level scope
58       * @param globalThis the global object
59       * @param browserVersion the browser version
60       */
61      public static void init(final TopLevel scope, final ScriptableObject globalThis,
62              final BrowserVersion browserVersion) {
63          final Intl intl = new Intl();
64          intl.setParentScope(scope);
65          intl.defineProperty(SymbolKey.TO_STRING_TAG, "Intl", ScriptableObject.DONTENUM | ScriptableObject.READONLY);
66          intl.defineProperties(scope, browserVersion);
67  
68          // Configure static functions (getCanonicalLocales)
69          final ClassConfiguration intlConfig =
70                  AbstractJavaScriptConfiguration.getClassConfiguration(Intl.class, browserVersion);
71          if (intlConfig != null) {
72              defineStaticFunctions(intlConfig, scope, intl);
73          }
74  
75          globalThis.defineProperty(intl.getClassName(), intl, ScriptableObject.DONTENUM);
76      }
77  
78      private void defineProperties(final TopLevel scope, final BrowserVersion browserVersion) {
79          define(scope, Collator.class, browserVersion);
80          define(scope, DateTimeFormat.class, browserVersion);
81          define(scope, Locale.class, browserVersion);
82          define(scope, NumberFormat.class, browserVersion);
83          if (browserVersion.hasFeature(JS_INTL_V8_BREAK_ITERATOR)) {
84              define(scope, V8BreakIterator.class, browserVersion);
85          }
86      }
87  
88      private void define(final TopLevel scope, final Class<? extends HtmlUnitScriptable> c,
89              final BrowserVersion browserVersion) {
90          try {
91              final ClassConfiguration config = AbstractJavaScriptConfiguration.getClassConfiguration(c, browserVersion);
92              final HtmlUnitScriptable prototype = JavaScriptEngine.configureClass(config, scope);
93              final FunctionObject constructorFn = new FunctionObject(config.getJsConstructor().getKey(),
94                      config.getJsConstructor().getValue(), scope);
95  
96              JavaScriptEngine.setFunctionProtoAndParent(constructorFn, Context.getCurrentContext(), scope);
97              constructorFn.setImmunePrototypeProperty(prototype);
98              prototype.setParentScope(scope);
99              ScriptableObject.defineProperty(prototype, "constructor", constructorFn, ScriptableObject.DONTENUM);
100             constructorFn.setParentScope(scope);
101 
102             ScriptableObject.defineProperty(this, prototype.getClassName(), constructorFn, ScriptableObject.DONTENUM);
103 
104             defineStaticFunctions(config, scope, constructorFn);
105         }
106         catch (final Exception e) {
107             throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
108         }
109     }
110 
111     private static void defineStaticFunctions(final ClassConfiguration config,
112             final VarScope scope, final ScriptableObject target) {
113         final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
114         if (staticFunctionMap != null) {
115             for (final Map.Entry<String, Method> entry : staticFunctionMap.entrySet()) {
116                 final FunctionObject fn = new FunctionObject(entry.getKey(), entry.getValue(), scope);
117                 target.defineProperty(entry.getKey(), fn, ScriptableObject.DONTENUM);
118             }
119         }
120     }
121 
122     /**
123      * Returns an array containing the canonical locale names.
124      * Duplicates will be omitted and elements will be validated as structurally valid language tags.
125      *
126      * @param cx the current context
127      * @param scope the scope
128      * @param thisObj the scriptable this
129      * @param args the arguments
130      * @param funObj the function object
131      * @return an array of canonical locale names
132      *
133      * @see <a href="https://tc39.es/ecma402/#sec-intl.getcanonicallocales">spec</a>
134      */
135     @JsxStaticFunction
136     public static Object getCanonicalLocales(final Context cx, final VarScope scope,
137             final Scriptable thisObj, final Object[] args, final Function funObj) {
138         if (args.length == 0 || JavaScriptEngine.isUndefined(args[0])) {
139             return cx.newArray(ScriptableObject.getTopLevelScope(scope), new Object[0]);
140         }
141 
142         final Object localesArgument = args[0];
143         if (localesArgument == null) {
144             throw JavaScriptEngine.typeError("Cannot convert null to object");
145         }
146 
147         final List<String> languageTags = new ArrayList<>();
148         if (localesArgument instanceof String s) {
149             languageTags.add(s);
150         }
151         else if (localesArgument instanceof Scriptable scriptable) {
152             if (JavaScriptEngine.isArrayLike(scriptable)) {
153                 JavaScriptEngine.iterateArrayLike(cx, scriptable, elem -> {
154                     if (elem instanceof String s) {
155                         languageTags.add(s);
156                     }
157                     else if (elem instanceof ScriptableObject) {
158                         languageTags.add(JavaScriptEngine.toString(elem));
159                     }
160                     else {
161                         throw JavaScriptEngine.typeError("Invalid element in locales argument");
162                     }
163                 });
164             }
165             else {
166                 languageTags.add(JavaScriptEngine.toString(localesArgument));
167             }
168         }
169 
170         final Set<String> canonicalLocales = new LinkedHashSet<>(languageTags.size());
171         for (final String tag : languageTags) {
172             try {
173                 canonicalLocales.add(
174                         new java.util.Locale.Builder().setLanguageTag(tag).build().toLanguageTag());
175             }
176             catch (final IllformedLocaleException e) {
177                 throw JavaScriptEngine.rangeError("Invalid language tag: '" + tag + "'");
178             }
179         }
180 
181         return cx.newArray(ScriptableObject.getTopLevelScope(scope), canonicalLocales.toArray());
182     }
183 
184     /**
185      * Shared utility for {@code supportedLocalesOf} implementations.
186      * @param localesArgument the locales argument
187      * @return a Scriptable array of supported locale strings
188      */
189     static Scriptable supportedLocalesOf(final Scriptable localesArgument) {
190         final String[] locales;
191         if (localesArgument instanceof NativeArray array) {
192             locales = new String[(int) array.getLength()];
193             for (int i = 0; i < locales.length; i++) {
194                 locales[i] = JavaScriptEngine.toString(array.get(i));
195             }
196         }
197         else {
198             locales = new String[] {JavaScriptEngine.toString(localesArgument)};
199         }
200 
201         final List<String> supportedLocales = new ArrayList<>();
202         for (final String locale : locales) {
203             if (locale.isEmpty()) {
204                 throw JavaScriptEngine.rangeError("Invalid language tag: '" + locale + "'");
205             }
206             final java.util.Locale l = java.util.Locale.forLanguageTag(locale);
207             if (LocaleUtils.isAvailableLocale(l)) {
208                 supportedLocales.add(locale);
209             }
210         }
211 
212         return Context.getCurrentContext().newArray(
213                 ScriptableObject.getTopLevelScope(localesArgument.getParentScope()), supportedLocales.toArray());
214     }
215 }