diff --git a/src/main/java/org/htmlunit/javascript/JavaScriptEngine.java b/src/main/java/org/htmlunit/javascript/JavaScriptEngine.java
index 0046f93c57..a95e54480c 100644
--- a/src/main/java/org/htmlunit/javascript/JavaScriptEngine.java
+++ b/src/main/java/org/htmlunit/javascript/JavaScriptEngine.java
@@ -454,10 +454,7 @@ public static void configureRhino(final WebClient webClient, final BrowserVersio
}
// add Intl
- final Intl intl = new Intl();
- intl.setParentScope(scope);
- globalThis.defineProperty(intl.getClassName(), intl, ScriptableObject.DONTENUM);
- intl.defineProperties(scope, browserVersion);
+ Intl.init(scope, globalThis, browserVersion);
}
/**
diff --git a/src/main/java/org/htmlunit/javascript/host/intl/DateTimeFormat.java b/src/main/java/org/htmlunit/javascript/host/intl/DateTimeFormat.java
index c6ea1cd226..83b258e47e 100644
--- a/src/main/java/org/htmlunit/javascript/host/intl/DateTimeFormat.java
+++ b/src/main/java/org/htmlunit/javascript/host/intl/DateTimeFormat.java
@@ -37,6 +37,7 @@
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxConstructor;
import org.htmlunit.javascript.configuration.JsxFunction;
+import org.htmlunit.javascript.configuration.JsxStaticFunction;
import org.htmlunit.javascript.host.Window;
import org.htmlunit.util.StringUtils;
@@ -305,6 +306,18 @@ public Scriptable resolvedOptions() {
return options;
}
+ /**
+ * Returns an array containing those of the provided locales that are supported
+ * without having to fall back to the default locale.
+ * @param localesArgument A string with a BCP 47 language tag, or an array of such strings
+ * @param options unused
+ * @return an array containing supported locales
+ */
+ @JsxStaticFunction
+ public static Scriptable supportedLocalesOf(final Scriptable localesArgument, final Scriptable options) {
+ return Intl.supportedLocalesOf(localesArgument);
+ }
+
/**
* Helper.
*/
diff --git a/src/main/java/org/htmlunit/javascript/host/intl/Intl.java b/src/main/java/org/htmlunit/javascript/host/intl/Intl.java
index 72964eb813..f9b286efce 100644
--- a/src/main/java/org/htmlunit/javascript/host/intl/Intl.java
+++ b/src/main/java/org/htmlunit/javascript/host/intl/Intl.java
@@ -16,30 +16,64 @@
import static org.htmlunit.BrowserVersionFeatures.JS_INTL_V8_BREAK_ITERATOR;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.IllformedLocaleException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.LocaleUtils;
import org.htmlunit.BrowserVersion;
+import org.htmlunit.corejs.javascript.Context;
+import org.htmlunit.corejs.javascript.Function;
import org.htmlunit.corejs.javascript.FunctionObject;
+import org.htmlunit.corejs.javascript.NativeArray;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.corejs.javascript.TopLevel;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.configuration.AbstractJavaScriptConfiguration;
import org.htmlunit.javascript.configuration.ClassConfiguration;
+import org.htmlunit.javascript.configuration.JsxClass;
+import org.htmlunit.javascript.configuration.JsxStaticFunction;
/**
* A JavaScript object for {@code Intl}.
*
* @author Ahmed Ashour
+ * @author Lai Quang Duong
*/
+@JsxClass
public class Intl extends HtmlUnitScriptable {
/**
- * Define needed properties.
- * @param scope the scope
+ * Initialize the Intl object and register it on the global scope.
+ * @param scope the top-level scope
+ * @param globalThis the global object
* @param browserVersion the browser version
*/
- public void defineProperties(final Scriptable scope, final BrowserVersion browserVersion) {
+ public static void init(final Scriptable scope, final ScriptableObject globalThis,
+ final BrowserVersion browserVersion) {
+ final Intl intl = new Intl();
+ intl.setParentScope(scope);
+ intl.defineProperties(scope, browserVersion);
+
+ // Configure static functions (getCanonicalLocales)
+ final ClassConfiguration intlConfig = AbstractJavaScriptConfiguration.getClassConfiguration(Intl.class, browserVersion);
+ if (intlConfig != null) {
+ defineStaticFunctions(intlConfig, intl, intl);
+ }
+
+ globalThis.defineProperty(intl.getClassName(), intl, ScriptableObject.DONTENUM);
+ }
+
+ private void defineProperties(final Scriptable scope, final BrowserVersion browserVersion) {
define(scope, Collator.class, browserVersion);
define(scope, DateTimeFormat.class, browserVersion);
+ define(scope, Locale.class, browserVersion);
define(scope, NumberFormat.class, browserVersion);
if (browserVersion.hasFeature(JS_INTL_V8_BREAK_ITERATOR)) {
define(scope, V8BreakIterator.class, browserVersion);
@@ -51,13 +85,125 @@ private void define(final Scriptable scope, final Class extends HtmlUnitScript
try {
final ClassConfiguration config = AbstractJavaScriptConfiguration.getClassConfiguration(c, browserVersion);
final HtmlUnitScriptable prototype = JavaScriptEngine.configureClass(config, scope);
- final FunctionObject functionObject =
- new FunctionObject(config.getJsConstructor().getKey(),
- config.getJsConstructor().getValue(), this);
- functionObject.addAsConstructor(this, prototype, ScriptableObject.DONTENUM);
+ final FunctionObject constructorFn = new FunctionObject(config.getJsConstructor().getKey(),
+ config.getJsConstructor().getValue(), this);
+ constructorFn.addAsConstructor(this, prototype, ScriptableObject.DONTENUM);
+
+ defineStaticFunctions(config, this, constructorFn);
}
catch (final Exception e) {
throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
}
}
+
+ private static void defineStaticFunctions(final ClassConfiguration config,
+ final Scriptable scope, final ScriptableObject target) {
+ final Map staticFunctionMap = config.getStaticFunctionMap();
+ if (staticFunctionMap != null) {
+ for (final Map.Entry entry : staticFunctionMap.entrySet()) {
+ final FunctionObject fn = new FunctionObject(entry.getKey(), entry.getValue(), scope);
+ target.defineProperty(entry.getKey(), fn, ScriptableObject.EMPTY);
+ }
+ }
+ }
+
+ /**
+ * Returns an array containing the canonical locale names.
+ * Duplicates will be omitted and elements will be validated as structurally valid language tags.
+ *
+ * @param cx the current context
+ * @param thisObj the scriptable this
+ * @param args the arguments
+ * @param funObj the function object
+ * @return an array of canonical locale names
+ *
+ * @see spec
+ */
+ @JsxStaticFunction
+ public static Object getCanonicalLocales(final Context cx, final Scriptable thisObj,
+ final Object[] args, final Function funObj) {
+ if (args.length == 0 || JavaScriptEngine.isUndefined(args[0])) {
+ return cx.newArray(TopLevel.getTopLevelScope(thisObj), new Object[0]);
+ }
+
+ final Object localesArgument = args[0];
+ if (localesArgument == null) {
+ throw JavaScriptEngine.typeError("Cannot convert null to object");
+ }
+
+ final List languageTags = new ArrayList<>();
+ if (localesArgument instanceof String s) {
+ languageTags.add(s);
+ }
+ else if (localesArgument instanceof Scriptable scriptable) {
+ if ("String".equals(scriptable.getClassName()) || scriptable instanceof Locale) {
+ languageTags.add(scriptable.toString());
+ }
+ else if (scriptable instanceof NativeArray array) {
+ for (int i = 0; i < array.getLength(); i++) {
+ final Object elem = array.get(i);
+ if (elem instanceof String s) {
+ languageTags.add(s);
+ }
+ else if (elem instanceof Locale) {
+ languageTags.add(elem.toString());
+ }
+ else if (elem instanceof ScriptableObject) {
+ languageTags.add(JavaScriptEngine.toString(elem));
+ }
+ else {
+ throw JavaScriptEngine.typeError("Invalid element in locales argument");
+ }
+ }
+ }
+ else {
+ languageTags.add(JavaScriptEngine.toString(localesArgument));
+ }
+ }
+
+ final Set canonicalLocales = new LinkedHashSet<>(languageTags.size());
+ for (final String tag : languageTags) {
+ try {
+ canonicalLocales.add(
+ new java.util.Locale.Builder().setLanguageTag(tag).build().toLanguageTag());
+ }
+ catch (final IllformedLocaleException e) {
+ throw JavaScriptEngine.rangeError("Invalid language tag: " + tag);
+ }
+ }
+
+ return cx.newArray(TopLevel.getTopLevelScope(thisObj), canonicalLocales.toArray());
+ }
+
+ /**
+ * Shared utility for {@code supportedLocalesOf} implementations.
+ * @param localesArgument the locales argument
+ * @return a Scriptable array of supported locale strings
+ */
+ static Scriptable supportedLocalesOf(final Scriptable localesArgument) {
+ final String[] locales;
+ if (localesArgument instanceof NativeArray array) {
+ locales = new String[(int) array.getLength()];
+ for (int i = 0; i < locales.length; i++) {
+ locales[i] = JavaScriptEngine.toString(array.get(i));
+ }
+ }
+ else {
+ locales = new String[] {JavaScriptEngine.toString(localesArgument)};
+ }
+
+ final List supportedLocales = new ArrayList<>();
+ for (final String locale : locales) {
+ if (locale.isEmpty()) {
+ throw JavaScriptEngine.rangeError("Invalid language tag: " + locale);
+ }
+ final java.util.Locale l = java.util.Locale.forLanguageTag(locale);
+ if (LocaleUtils.isAvailableLocale(l)) {
+ supportedLocales.add(locale);
+ }
+ }
+
+ return Context.getCurrentContext().newArray(
+ TopLevel.getTopLevelScope(localesArgument), supportedLocales.toArray());
+ }
}
diff --git a/src/main/java/org/htmlunit/javascript/host/intl/Locale.java b/src/main/java/org/htmlunit/javascript/host/intl/Locale.java
new file mode 100644
index 0000000000..d3c414b320
--- /dev/null
+++ b/src/main/java/org/htmlunit/javascript/host/intl/Locale.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (c) 2002-2026 Gargoyle Software Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.javascript.host.intl;
+
+import java.util.IllformedLocaleException;
+import java.util.List;
+
+import org.apache.commons.lang3.LocaleUtils;
+import org.htmlunit.corejs.javascript.Context;
+import org.htmlunit.corejs.javascript.Function;
+import org.htmlunit.corejs.javascript.FunctionObject;
+import org.htmlunit.corejs.javascript.Scriptable;
+import org.htmlunit.corejs.javascript.ScriptableObject;
+import org.htmlunit.javascript.HtmlUnitScriptable;
+import org.htmlunit.javascript.JavaScriptEngine;
+import org.htmlunit.javascript.configuration.JsxClass;
+import org.htmlunit.javascript.configuration.JsxConstructor;
+import org.htmlunit.javascript.configuration.JsxFunction;
+import org.htmlunit.javascript.configuration.JsxGetter;
+
+/**
+ * A JavaScript object for {@code Intl.Locale}.
+ *
+ * @author Lai Quang Duong
+ */
+@JsxClass
+public class Locale extends HtmlUnitScriptable {
+
+ private static final List ALLOWED_HOUR_CYCLES = List.of("h11", "h12", "h23", "h24");
+ private static final List ALLOWED_CASE_FIRSTS = List.of("upper", "lower", "false");
+
+ private java.util.Locale locale_;
+ private String language_;
+ private String script_;
+ private String region_;
+ private String calendar_;
+ private String collation_;
+ private String numberingSystem_;
+ private String caseFirst_;
+ private String hourCycle_;
+ private boolean numeric_;
+
+ /**
+ * Default constructor.
+ */
+ public Locale() {
+ super();
+ }
+
+ private Locale(final java.util.Locale locale) {
+ super();
+ locale_ = locale;
+ language_ = locale.getLanguage();
+ if (!locale.getScript().isEmpty()) {
+ script_ = locale.getScript();
+ }
+ if (!locale.getCountry().isEmpty()) {
+ region_ = locale.getCountry();
+ }
+ if (locale.hasExtensions()) {
+ calendar_ = locale.getUnicodeLocaleType("ca");
+ collation_ = locale.getUnicodeLocaleType("co");
+ numberingSystem_ = locale.getUnicodeLocaleType("nu");
+ caseFirst_ = locale.getUnicodeLocaleType("kf");
+ hourCycle_ = locale.getUnicodeLocaleType("hc");
+ numeric_ = Boolean.parseBoolean(locale.getUnicodeLocaleType("kn"));
+ }
+ }
+
+ /**
+ * JavaScript constructor.
+ * @param cx the current context
+ * @param scope the scope
+ * @param args the arguments
+ * @param ctorObj the constructor function
+ * @param inNewExpr whether called via new
+ * @return the new Locale instance
+ */
+ @JsxConstructor
+ public static Scriptable jsConstructor(final Context cx, final Scriptable scope,
+ final Object[] args, final Function ctorObj, final boolean inNewExpr) {
+ if (args.length == 0 || JavaScriptEngine.isUndefined(args[0])) {
+ throw JavaScriptEngine.typeError("Invalid element in locales argument");
+ }
+
+ final String languageTag;
+ if (args[0] instanceof Locale loc) {
+ languageTag = loc.toString();
+ }
+ else {
+ languageTag = JavaScriptEngine.toString(args[0]);
+ }
+ if (languageTag.isEmpty()) {
+ throw JavaScriptEngine.rangeError("Invalid language tag: ");
+ }
+
+ java.util.Locale locale;
+ try {
+ locale = new java.util.Locale.Builder()
+ .setLanguageTag(languageTag)
+ .build();
+ }
+ catch (final IllformedLocaleException e) {
+ throw JavaScriptEngine.rangeError("Invalid language tag: " + languageTag);
+ }
+
+ // Override by options if present
+ if (args.length > 1 && !JavaScriptEngine.isUndefined(args[1])) {
+ locale = overrideExistingWithOptions(locale,
+ ScriptableObject.ensureScriptableObject(args[1]));
+ }
+
+ final Locale l = new Locale(locale);
+ l.setParentScope(getTopLevelScope(ctorObj));
+ l.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
+ return l;
+ }
+
+ private static java.util.Locale overrideExistingWithOptions(
+ final java.util.Locale existing, final ScriptableObject options) {
+ final java.util.Locale.Builder builder = new java.util.Locale.Builder().setLocale(existing);
+
+ setStringOption(builder, options, "language");
+ setStringOption(builder, options, "script");
+ setStringOption(builder, options, "region");
+ setUnicodeKeyword(builder, options, "calendar", "ca", null);
+ setUnicodeKeyword(builder, options, "collation", "co", null);
+ setUnicodeKeyword(builder, options, "numberingSystem", "nu", null);
+ setUnicodeKeyword(builder, options, "caseFirst", "kf", ALLOWED_CASE_FIRSTS);
+ setUnicodeKeyword(builder, options, "hourCycle", "hc", ALLOWED_HOUR_CYCLES);
+
+ final Object numeric = ScriptableObject.getProperty(options, "numeric");
+ if (numeric != Scriptable.NOT_FOUND && !JavaScriptEngine.isUndefined(numeric)) {
+ final boolean isNumeric = numeric instanceof Boolean ? (Boolean) numeric : true;
+ builder.setUnicodeLocaleKeyword("kn", Boolean.toString(isNumeric));
+ }
+
+ return builder.build();
+ }
+
+ private static void setStringOption(final java.util.Locale.Builder builder,
+ final ScriptableObject options, final String optionName) {
+ final Object value = ScriptableObject.getProperty(options, optionName);
+ if (value == Scriptable.NOT_FOUND || JavaScriptEngine.isUndefined(value)) {
+ return;
+ }
+ try {
+ final String s = JavaScriptEngine.toString(value);
+ switch (optionName) {
+ case "language":
+ builder.setLanguage(s);
+ break;
+ case "script":
+ builder.setScript(s);
+ break;
+ case "region":
+ builder.setRegion(s);
+ break;
+ default:
+ break;
+ }
+ }
+ catch (final Exception e) {
+ throw JavaScriptEngine.rangeError("Invalid value for option \"" + optionName + "\"");
+ }
+ }
+
+ private static void setUnicodeKeyword(final java.util.Locale.Builder builder,
+ final ScriptableObject options, final String optionName, final String unicodeKey,
+ final List allowedValues) {
+ final Object value = ScriptableObject.getProperty(options, optionName);
+ if (value == Scriptable.NOT_FOUND || JavaScriptEngine.isUndefined(value)) {
+ return;
+ }
+ final String s;
+ try {
+ s = JavaScriptEngine.toString(value);
+ }
+ catch (final Exception e) {
+ throw JavaScriptEngine.rangeError("Invalid value for option \"" + optionName + "\"");
+ }
+ if (allowedValues != null && !allowedValues.contains(s)) {
+ throw JavaScriptEngine.rangeError("Invalid value for option \"" + optionName + "\"");
+ }
+ builder.setUnicodeLocaleKeyword(unicodeKey, s);
+ }
+
+ /** @return the language */
+ @JsxGetter
+ public Object getLanguage() {
+ return language_ != null ? language_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the script */
+ @JsxGetter
+ public Object getScript() {
+ return script_ != null ? script_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the region */
+ @JsxGetter
+ public Object getRegion() {
+ return region_ != null ? region_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the calendar type */
+ @JsxGetter
+ public Object getCalendar() {
+ return calendar_ != null ? calendar_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the collation type */
+ @JsxGetter
+ public Object getCollation() {
+ return collation_ != null ? collation_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the numbering system */
+ @JsxGetter
+ public Object getNumberingSystem() {
+ return numberingSystem_ != null ? numberingSystem_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the case first setting */
+ @JsxGetter
+ public Object getCaseFirst() {
+ return caseFirst_ != null ? caseFirst_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return the hour cycle */
+ @JsxGetter
+ public Object getHourCycle() {
+ return hourCycle_ != null ? hourCycle_ : JavaScriptEngine.UNDEFINED;
+ }
+
+ /** @return whether numeric sorting is used */
+ @JsxGetter
+ public boolean isNumeric() {
+ return numeric_;
+ }
+
+ /** @return the base name (without Unicode extensions) */
+ @JsxGetter
+ public Object getBaseName() {
+ final String variant = locale_.getVariant().replace("_", "-");
+ return language_
+ + (script_ != null ? "-" + script_ : "")
+ + (region_ != null ? "-" + region_ : "")
+ + (!variant.isEmpty() ? "-" + variant : "");
+ }
+
+ /**
+ * Returns a Locale with maximized subtags.
+ * @return a new Locale instance with maximized subtags
+ */
+ @JsxFunction
+ public Object maximize() {
+ final String region;
+ if (region_ != null) {
+ region = region_;
+ }
+ else {
+ final java.util.List locales =
+ LocaleUtils.countriesByLanguage(language_);
+ if (!locales.isEmpty()) {
+ region = locales.get(0).getCountry();
+ }
+ else {
+ region = null;
+ }
+ }
+
+ final java.util.Locale locale = new java.util.Locale.Builder()
+ .setLanguage(language_)
+ .setScript(script_)
+ .setRegion(region)
+ .setExtension('u', locale_.getExtension('u'))
+ .build();
+
+ final Locale l = new Locale(locale);
+ l.setParentScope(getWindow(this));
+ l.setPrototype(this.getPrototype());
+ return l;
+ }
+
+ /**
+ * Returns a Locale with minimized subtags.
+ * @return a new Locale instance with minimized subtags
+ */
+ @JsxFunction
+ public Object minimize() {
+ final java.util.Locale locale = new java.util.Locale.Builder()
+ .setLanguage(language_)
+ .setExtension('u', locale_.getExtension('u'))
+ .build();
+
+ final Locale l = new Locale(locale);
+ l.setParentScope(getWindow(this));
+ l.setPrototype(this.getPrototype());
+ return l;
+ }
+
+ /**
+ * @return the locale's Unicode locale identifier string
+ */
+ @Override
+ @JsxFunction
+ public String toString() {
+ if (locale_ == null) {
+ return super.toString();
+ }
+ return locale_.toLanguageTag();
+ }
+}
diff --git a/src/main/java/org/htmlunit/javascript/host/intl/NumberFormat.java b/src/main/java/org/htmlunit/javascript/host/intl/NumberFormat.java
index c1a3129fe2..039a329d7d 100644
--- a/src/main/java/org/htmlunit/javascript/host/intl/NumberFormat.java
+++ b/src/main/java/org/htmlunit/javascript/host/intl/NumberFormat.java
@@ -32,6 +32,7 @@
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxConstructor;
import org.htmlunit.javascript.configuration.JsxFunction;
+import org.htmlunit.javascript.configuration.JsxStaticFunction;
import org.htmlunit.javascript.host.Window;
import org.htmlunit.util.StringUtils;
@@ -209,6 +210,18 @@ public Scriptable resolvedOptions() {
return JavaScriptEngine.newObject(getParentScope());
}
+ /**
+ * Returns an array containing those of the provided locales that are supported
+ * without having to fall back to the default locale.
+ * @param localesArgument A string with a BCP 47 language tag, or an array of such strings
+ * @param options unused
+ * @return an array containing supported locales
+ */
+ @JsxStaticFunction
+ public static Scriptable supportedLocalesOf(final Scriptable localesArgument, final Scriptable options) {
+ return Intl.supportedLocalesOf(localesArgument);
+ }
+
/**
* Helper.
*/
diff --git a/src/test/java/org/htmlunit/javascript/host/intl/DateTimeFormatTest.java b/src/test/java/org/htmlunit/javascript/host/intl/DateTimeFormatTest.java
index 78467d2df5..abc2e50627 100644
--- a/src/test/java/org/htmlunit/javascript/host/intl/DateTimeFormatTest.java
+++ b/src/test/java/org/htmlunit/javascript/host/intl/DateTimeFormatTest.java
@@ -434,4 +434,27 @@ private void locale(final String language) throws Exception {
shutDownAll();
}
}
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"en", "en,ja", ""})
+ public void supportedLocalesOf() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
}
diff --git a/src/test/java/org/htmlunit/javascript/host/intl/IntlTest.java b/src/test/java/org/htmlunit/javascript/host/intl/IntlTest.java
index 67564be292..b521f96c35 100644
--- a/src/test/java/org/htmlunit/javascript/host/intl/IntlTest.java
+++ b/src/test/java/org/htmlunit/javascript/host/intl/IntlTest.java
@@ -23,6 +23,7 @@
*
* @author Ahmed Ashour
* @author Ronald Brill
+ * @author Lai Quang Duong
*/
public class IntlTest extends WebDriverTestCase {
@@ -79,6 +80,15 @@ public void numberFormat() throws Exception {
test("Intl.NumberFormat");
}
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts("function Locale() { [native code] }")
+ public void locale() throws Exception {
+ test("Intl.Locale");
+ }
+
/**
* @throws Exception if the test fails
*/
@@ -90,4 +100,39 @@ public void v8BreakIterator() throws Exception {
test("Intl.v8BreakIterator");
}
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"en-US", "en-US,fr-FR,ja-JP",
+ "zh-Hant-TW", "en-Latn-US",
+ "ja-JP", "en-US,fr", "en-US", "ja-JP",
+ "TypeError", "", "", "RangeError", "RangeError", ""})
+ public void getCanonicalLocales() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
}
diff --git a/src/test/java/org/htmlunit/javascript/host/intl/LocaleTest.java b/src/test/java/org/htmlunit/javascript/host/intl/LocaleTest.java
new file mode 100644
index 0000000000..78fb36b486
--- /dev/null
+++ b/src/test/java/org/htmlunit/javascript/host/intl/LocaleTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2002-2026 Gargoyle Software Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.javascript.host.intl;
+
+import org.htmlunit.WebDriverTestCase;
+import org.htmlunit.junit.annotation.Alerts;
+import org.htmlunit.junit.annotation.HtmlUnitNYI;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link Locale}.
+ *
+ * @author Lai Quang Duong
+ */
+public class LocaleTest extends WebDriverTestCase {
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"en", "undefined", "undefined", "en", "en",
+ "zh", "undefined", "Hant", "zh-Hant", "zh-Hant",
+ "zh", "TW", "Hant", "zh-Hant-TW", "zh-Hant-TW",
+ "ja", "JP", "undefined", "ja-JP", "ja-JP",
+ "en", "US", "undefined", "en-US", "en-US",
+ "ja-JP"})
+ public void construction() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"TypeError", "RangeError", "RangeError", "RangeError",
+ "invalid", "en-US", "RangeError", "RangeError"})
+ public void constructorErrors() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"undefined", "undefined", "undefined", "undefined", "undefined", "false",
+ "buddhist", "emoji", "latn", "h23", "lower", "true",
+ "ja", "JP", "Jpan", "japanese", "h12", "ja-Jpan-JP",
+ "de", "phonebk", "de-u-co-phonebk",
+ "th", "TH", "thai", "th-TH-u-nu-thai",
+ "en", "upper", "en-u-kf-upper"})
+ public void extensions() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"en", "true", "en-u-kn",
+ "false", "en-u-kn-false",
+ "false", "en-u-kn-false"})
+ @HtmlUnitNYI(CHROME = {"en", "true", "en-u-kn-true",
+ "false", "en-u-kn-false",
+ "false", "en-u-kn-false"},
+ EDGE = {"en", "true", "en-u-kn-true",
+ "false", "en-u-kn-false",
+ "false", "en-u-kn-false"},
+ FF = {"en", "true", "en-u-kn-true",
+ "false", "en-u-kn-false",
+ "false", "en-u-kn-false"},
+ FF_ESR = {"en", "true", "en-u-kn-true",
+ "false", "en-u-kn-false",
+ "false", "en-u-kn-false"})
+ public void numeric() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"en", "RU", "Cyrl", "buddhist", "zhuyin", "thai", "h11", "true",
+ "en", "CA", "en-CA",
+ "en", "GB", "en-GB",
+ "islamic", "en-US-u-ca-islamic",
+ "buddhist", "en-US-u-ca-buddhist"})
+ public void optionsOverride() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"RangeError", "RangeError", "RangeError", "RangeError", "RangeError"})
+ public void invalidOptions() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"ja-Jpan-JP-u-ca-japanese-hc-h12",
+ "ja-u-ca-japanese-hc-h12",
+ "ja-Jpan-JP-u-ca-japanese-hc-h12",
+ "ja-Jpan-JP"})
+ @HtmlUnitNYI(CHROME = {"ja-Jpan-JP-u-ca-japanese-hc-h12",
+ "ja-u-ca-japanese-hc-h12",
+ "ja-JP-u-ca-japanese-hc-h12",
+ "ja-Jpan-JP"},
+ EDGE = {"ja-Jpan-JP-u-ca-japanese-hc-h12",
+ "ja-u-ca-japanese-hc-h12",
+ "ja-JP-u-ca-japanese-hc-h12",
+ "ja-Jpan-JP"},
+ FF = {"ja-Jpan-JP-u-ca-japanese-hc-h12",
+ "ja-u-ca-japanese-hc-h12",
+ "ja-JP-u-ca-japanese-hc-h12",
+ "ja-Jpan-JP"},
+ FF_ESR = {"ja-Jpan-JP-u-ca-japanese-hc-h12",
+ "ja-u-ca-japanese-hc-h12",
+ "ja-JP-u-ca-japanese-hc-h12",
+ "ja-Jpan-JP"})
+ public void minimizeMaximize() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
+}
diff --git a/src/test/java/org/htmlunit/javascript/host/intl/NumberFormatTest.java b/src/test/java/org/htmlunit/javascript/host/intl/NumberFormatTest.java
index 5d659bcab8..88ee0e674e 100644
--- a/src/test/java/org/htmlunit/javascript/host/intl/NumberFormatTest.java
+++ b/src/test/java/org/htmlunit/javascript/host/intl/NumberFormatTest.java
@@ -111,4 +111,27 @@ public void numberFormat() throws Exception {
loadPageVerifyTitle2(html);
}
+
+ /**
+ * @throws Exception if the test fails
+ */
+ @Test
+ @Alerts({"en", "en,ja", ""})
+ public void supportedLocalesOf() throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ loadPageVerifyTitle2(html);
+ }
}