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   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl">MDN Documentation</a>
53   */
54  @JsxClass
55  public class Intl extends HtmlUnitScriptable {
56  
57      /**
58       * Initialize the Intl object and register it on the global scope.
59       * @param scope the top-level scope
60       * @param globalThis the global object
61       * @param browserVersion the browser version
62       */
63      public static void init(final TopLevel scope, final ScriptableObject globalThis,
64              final BrowserVersion browserVersion) {
65          final Intl intl = new Intl();
66          intl.setParentScope(scope);
67          intl.defineProperty(SymbolKey.TO_STRING_TAG, "Intl", ScriptableObject.DONTENUM | ScriptableObject.READONLY);
68          intl.defineProperties(scope, browserVersion);
69  
70          // Configure static functions (getCanonicalLocales)
71          final ClassConfiguration intlConfig =
72                  AbstractJavaScriptConfiguration.getClassConfiguration(Intl.class, browserVersion);
73          if (intlConfig != null) {
74              defineStaticFunctions(intlConfig, scope, intl);
75          }
76  
77          globalThis.defineProperty(intl.getClassName(), intl, ScriptableObject.DONTENUM);
78      }
79  
80      private void defineProperties(final TopLevel scope, final BrowserVersion browserVersion) {
81          define(scope, Collator.class, browserVersion);
82          define(scope, DateTimeFormat.class, browserVersion);
83          define(scope, Locale.class, browserVersion);
84          define(scope, NumberFormat.class, browserVersion);
85          if (browserVersion.hasFeature(JS_INTL_V8_BREAK_ITERATOR)) {
86              define(scope, V8BreakIterator.class, browserVersion);
87          }
88      }
89  
90      private void define(final TopLevel scope, final Class<? extends HtmlUnitScriptable> c,
91              final BrowserVersion browserVersion) {
92          try {
93              final ClassConfiguration config = AbstractJavaScriptConfiguration.getClassConfiguration(c, browserVersion);
94              final HtmlUnitScriptable prototype = JavaScriptEngine.configureClass(config, scope);
95              final FunctionObject constructorFn = new FunctionObject(config.getJsConstructor().getKey(),
96                      config.getJsConstructor().getValue(), scope);
97  
98              JavaScriptEngine.setFunctionProtoAndParent(constructorFn, Context.getCurrentContext(), scope);
99              constructorFn.setImmunePrototypeProperty(prototype);
100             prototype.setParentScope(scope);
101             ScriptableObject.defineProperty(prototype, "constructor", constructorFn, ScriptableObject.DONTENUM);
102             constructorFn.setParentScope(scope);
103 
104             ScriptableObject.defineProperty(this, prototype.getClassName(), constructorFn, ScriptableObject.DONTENUM);
105 
106             defineStaticFunctions(config, scope, constructorFn);
107         }
108         catch (final Exception e) {
109             throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
110         }
111     }
112 
113     private static void defineStaticFunctions(final ClassConfiguration config,
114             final VarScope scope, final ScriptableObject target) {
115         final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
116         if (staticFunctionMap != null) {
117             for (final Map.Entry<String, Method> entry : staticFunctionMap.entrySet()) {
118                 final FunctionObject fn = new FunctionObject(entry.getKey(), entry.getValue(), scope);
119                 target.defineProperty(entry.getKey(), fn, ScriptableObject.DONTENUM);
120             }
121         }
122     }
123 
124     /**
125      * Returns an array containing the canonical locale names.
126      * Duplicates will be omitted and elements will be validated as structurally valid language tags.
127      *
128      * @param cx the current context
129      * @param scope the scope
130      * @param thisObj the scriptable this
131      * @param args the arguments
132      * @param funObj the function object
133      * @return an array of canonical locale names
134      *
135      * @see <a href="https://tc39.es/ecma402/#sec-intl.getcanonicallocales">spec</a>
136      */
137     @JsxStaticFunction
138     public static Object getCanonicalLocales(final Context cx, final VarScope scope,
139             final Scriptable thisObj, final Object[] args, final Function funObj) {
140         if (args.length == 0 || JavaScriptEngine.isUndefined(args[0])) {
141             return cx.newArray(ScriptableObject.getTopLevelScope(scope), new Object[0]);
142         }
143 
144         final Object localesArgument = args[0];
145         if (localesArgument == null) {
146             throw JavaScriptEngine.typeError("Cannot convert null to object");
147         }
148 
149         final List<String> languageTags = new ArrayList<>();
150         if (localesArgument instanceof String s) {
151             languageTags.add(s);
152         }
153         else if (localesArgument instanceof Scriptable scriptable) {
154             if (JavaScriptEngine.isArrayLike(scriptable)) {
155                 JavaScriptEngine.iterateArrayLike(cx, scriptable, elem -> {
156                     if (elem instanceof String s) {
157                         languageTags.add(s);
158                     }
159                     else if (elem instanceof ScriptableObject) {
160                         languageTags.add(JavaScriptEngine.toString(elem));
161                     }
162                     else {
163                         throw JavaScriptEngine.typeError("Invalid element in locales argument");
164                     }
165                 });
166             }
167             else {
168                 languageTags.add(JavaScriptEngine.toString(localesArgument));
169             }
170         }
171 
172         final Set<String> canonicalLocales = new LinkedHashSet<>(languageTags.size());
173         for (final String tag : languageTags) {
174             try {
175                 canonicalLocales.add(
176                         new java.util.Locale.Builder().setLanguageTag(tag).build().toLanguageTag());
177             }
178             catch (final IllformedLocaleException e) {
179                 throw JavaScriptEngine.rangeError("Invalid language tag: '" + tag + "'");
180             }
181         }
182 
183         return cx.newArray(ScriptableObject.getTopLevelScope(scope), canonicalLocales.toArray());
184     }
185 
186     /**
187      * Shared utility for {@code supportedLocalesOf} implementations.
188      * @param localesArgument the locales argument
189      * @return a Scriptable array of supported locale strings
190      */
191     static Scriptable supportedLocalesOf(final Scriptable localesArgument) {
192         final String[] locales;
193         if (localesArgument instanceof NativeArray array) {
194             locales = new String[(int) array.getLength()];
195             for (int i = 0; i < locales.length; i++) {
196                 locales[i] = JavaScriptEngine.toString(array.get(i));
197             }
198         }
199         else {
200             locales = new String[] {JavaScriptEngine.toString(localesArgument)};
201         }
202 
203         final List<String> supportedLocales = new ArrayList<>();
204         for (final String locale : locales) {
205             if (locale.isEmpty()) {
206                 throw JavaScriptEngine.rangeError("Invalid language tag: '" + locale + "'");
207             }
208             final java.util.Locale l = java.util.Locale.forLanguageTag(locale);
209             if (LocaleUtils.isAvailableLocale(l)) {
210                 supportedLocales.add(locale);
211             }
212         }
213 
214         return Context.getCurrentContext().newArray(
215                 ScriptableObject.getTopLevelScope(localesArgument.getParentScope()), supportedLocales.toArray());
216     }
217 }