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 java.util.IllformedLocaleException;
18  import java.util.List;
19  
20  import org.apache.commons.lang3.LocaleUtils;
21  import org.htmlunit.corejs.javascript.Context;
22  import org.htmlunit.corejs.javascript.Function;
23  import org.htmlunit.corejs.javascript.FunctionObject;
24  import org.htmlunit.corejs.javascript.Scriptable;
25  import org.htmlunit.corejs.javascript.ScriptableObject;
26  import org.htmlunit.corejs.javascript.VarScope;
27  import org.htmlunit.javascript.HtmlUnitScriptable;
28  import org.htmlunit.javascript.JavaScriptEngine;
29  import org.htmlunit.javascript.configuration.JsxClass;
30  import org.htmlunit.javascript.configuration.JsxConstructor;
31  import org.htmlunit.javascript.configuration.JsxFunction;
32  import org.htmlunit.javascript.configuration.JsxGetter;
33  import org.htmlunit.javascript.configuration.JsxSymbolConstant;
34  
35  /**
36   * A JavaScript object for Intl.Locale.
37   *
38   * @author Lai Quang Duong
39   * @author Ronald Brill
40   *
41   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale">MDN Documentation</a>
42   */
43  @JsxClass
44  public class Locale extends HtmlUnitScriptable {
45  
46      /** Symbol.toStringTag support. */
47      @JsxSymbolConstant
48      public static final String TO_STRING_TAG = "Intl.Locale";
49  
50      private static final List<String> ALLOWED_HOUR_CYCLES = List.of("h11", "h12", "h23", "h24");
51      private static final List<String> ALLOWED_CASE_FIRSTS = List.of("upper", "lower", "false");
52  
53      private java.util.Locale locale_;
54      private String language_;
55      private String script_;
56      private String region_;
57      private String calendar_;
58      private String collation_;
59      private String numberingSystem_;
60      private String caseFirst_;
61      private String hourCycle_;
62      private boolean numeric_;
63  
64      /**
65       * Default constructor.
66       */
67      public Locale() {
68          super();
69      }
70  
71      private Locale(final java.util.Locale locale) {
72          super();
73          locale_ = locale;
74          language_ = locale.getLanguage();
75          if (!locale.getScript().isEmpty()) {
76              script_ = locale.getScript();
77          }
78          if (!locale.getCountry().isEmpty()) {
79              region_ = locale.getCountry();
80          }
81          if (locale.hasExtensions()) {
82              calendar_ = locale.getUnicodeLocaleType("ca");
83              collation_ = locale.getUnicodeLocaleType("co");
84              numberingSystem_ = locale.getUnicodeLocaleType("nu");
85              caseFirst_ = locale.getUnicodeLocaleType("kf");
86              hourCycle_ = locale.getUnicodeLocaleType("hc");
87              numeric_ = Boolean.parseBoolean(locale.getUnicodeLocaleType("kn"));
88          }
89      }
90  
91      /**
92       * JavaScript constructor.
93       * @param cx the current context
94       * @param scope the scope
95       * @param args the arguments
96       * @param ctorObj the constructor function
97       * @param inNewExpr whether called via new
98       * @return the new Locale instance
99       */
100     @JsxConstructor
101     public static Scriptable jsConstructor(final Context cx, final VarScope scope,
102             final Object[] args, final Function ctorObj, final boolean inNewExpr) {
103         if (args.length == 0 || JavaScriptEngine.isUndefined(args[0])) {
104             throw JavaScriptEngine.typeError("Invalid element in locales argument");
105         }
106 
107         final String languageTag = JavaScriptEngine.toString(args[0]);
108         if (languageTag.isEmpty()) {
109             throw JavaScriptEngine.rangeError("Invalid language tag: ");
110         }
111 
112         java.util.Locale locale;
113         try {
114             locale = new java.util.Locale.Builder()
115                     .setLanguageTag(languageTag)
116                     .build();
117         }
118         catch (final IllformedLocaleException e) {
119             throw JavaScriptEngine.rangeError("Invalid language tag: " + languageTag);
120         }
121 
122         // Override by options if present
123         if (args.length > 1 && !JavaScriptEngine.isUndefined(args[1])) {
124             locale = overrideExistingWithOptions(locale, ScriptableObject.ensureScriptableObject(args[1]));
125         }
126 
127         final Locale l = new Locale(locale);
128         l.setParentScope(getTopLevelScope(scope));
129         l.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
130         return l;
131     }
132 
133     private static java.util.Locale overrideExistingWithOptions(
134             final java.util.Locale existing, final ScriptableObject options) {
135         final java.util.Locale.Builder builder = new java.util.Locale.Builder().setLocale(existing);
136 
137         setStringOption(builder, options, "language");
138         setStringOption(builder, options, "script");
139         setStringOption(builder, options, "region");
140         setUnicodeKeyword(builder, options, "calendar", "ca", null);
141         setUnicodeKeyword(builder, options, "collation", "co", null);
142         setUnicodeKeyword(builder, options, "numberingSystem", "nu", null);
143         setUnicodeKeyword(builder, options, "caseFirst", "kf", ALLOWED_CASE_FIRSTS);
144         setUnicodeKeyword(builder, options, "hourCycle", "hc", ALLOWED_HOUR_CYCLES);
145 
146         final Object numeric = ScriptableObject.getProperty(options, "numeric");
147         if (numeric != Scriptable.NOT_FOUND && !JavaScriptEngine.isUndefined(numeric)) {
148             final boolean isNumeric = numeric instanceof Boolean ? (Boolean) numeric : true;
149             builder.setUnicodeLocaleKeyword("kn", Boolean.toString(isNumeric));
150         }
151 
152         return builder.build();
153     }
154 
155     private static void setStringOption(final java.util.Locale.Builder builder,
156             final ScriptableObject options, final String optionName) {
157         final Object value = ScriptableObject.getProperty(options, optionName);
158         if (value == Scriptable.NOT_FOUND || JavaScriptEngine.isUndefined(value)) {
159             return;
160         }
161         try {
162             final String s = JavaScriptEngine.toString(value);
163             switch (optionName) {
164                 case "language":
165                     builder.setLanguage(s);
166                     break;
167                 case "script":
168                     builder.setScript(s);
169                     break;
170                 case "region":
171                     builder.setRegion(s);
172                     break;
173                 default:
174                     break;
175             }
176         }
177         catch (final Exception e) {
178             throw JavaScriptEngine.rangeError("Invalid value for option \"" + optionName + "\"");
179         }
180     }
181 
182     private static void setUnicodeKeyword(final java.util.Locale.Builder builder,
183             final ScriptableObject options, final String optionName, final String unicodeKey,
184             final List<String> allowedValues) {
185         final Object value = ScriptableObject.getProperty(options, optionName);
186         if (value == Scriptable.NOT_FOUND || JavaScriptEngine.isUndefined(value)) {
187             return;
188         }
189         final String s;
190         try {
191             s = JavaScriptEngine.toString(value);
192         }
193         catch (final Exception e) {
194             throw JavaScriptEngine.rangeError("Invalid value for option \"" + optionName + "\"");
195         }
196         if (allowedValues != null && !allowedValues.contains(s)) {
197             throw JavaScriptEngine.rangeError("Invalid value for option \"" + optionName + "\"");
198         }
199         builder.setUnicodeLocaleKeyword(unicodeKey, s);
200     }
201 
202     /**
203      * Returns the language subtag.
204      * @return the language
205      */
206     @JsxGetter
207     public Object getLanguage() {
208         return language_ != null ? language_ : JavaScriptEngine.UNDEFINED;
209     }
210 
211     /**
212      * Returns the script subtag.
213      * @return the script
214      */
215     @JsxGetter
216     public Object getScript() {
217         return script_ != null ? script_ : JavaScriptEngine.UNDEFINED;
218     }
219 
220     /**
221      * Returns the region subtag.
222      * @return the region
223      */
224     @JsxGetter
225     public Object getRegion() {
226         return region_ != null ? region_ : JavaScriptEngine.UNDEFINED;
227     }
228 
229     /**
230      * Returns the calendar type.
231      * @return the calendar type
232      */
233     @JsxGetter
234     public Object getCalendar() {
235         return calendar_ != null ? calendar_ : JavaScriptEngine.UNDEFINED;
236     }
237 
238     /**
239      * Returns the collation type.
240      * @return the collation type
241      */
242     @JsxGetter
243     public Object getCollation() {
244         return collation_ != null ? collation_ : JavaScriptEngine.UNDEFINED;
245     }
246 
247     /**
248      * Returns the numbering system.
249      * @return the numbering system
250      */
251     @JsxGetter
252     public Object getNumberingSystem() {
253         return numberingSystem_ != null ? numberingSystem_ : JavaScriptEngine.UNDEFINED;
254     }
255 
256     /**
257      * Returns the case first setting.
258      * @return the case first setting
259      */
260     @JsxGetter
261     public Object getCaseFirst() {
262         return caseFirst_ != null ? caseFirst_ : JavaScriptEngine.UNDEFINED;
263     }
264 
265     /**
266      * Returns the hour cycle.
267      * @return the hour cycle
268      */
269     @JsxGetter
270     public Object getHourCycle() {
271         return hourCycle_ != null ? hourCycle_ : JavaScriptEngine.UNDEFINED;
272     }
273 
274     /**
275      * Returns whether numeric sorting is used.
276      * @return whether numeric sorting is used
277      */
278     @JsxGetter
279     public boolean isNumeric() {
280         return numeric_;
281     }
282 
283     /**
284      * Returns the base name (without Unicode extensions).
285      * @return the base name (without Unicode extensions)
286      */
287     @JsxGetter
288     public Object getBaseName() {
289         final String variant = locale_.getVariant().replace("_", "-");
290         return language_
291                 + (script_ != null ? "-" + script_ : "")
292                 + (region_ != null ? "-" + region_ : "")
293                 + (!variant.isEmpty() ? "-" + variant : "");
294     }
295 
296     /**
297      * Returns a Locale with maximized subtags.
298      * @return a new Locale instance with maximized subtags
299      */
300     @JsxFunction
301     public Locale maximize() {
302         final String region;
303         if (region_ != null) {
304             region = region_;
305         }
306         else {
307             final java.util.List<java.util.Locale> locales =
308                     LocaleUtils.countriesByLanguage(language_);
309             if (!locales.isEmpty()) {
310                 region = locales.get(0).getCountry();
311             }
312             else {
313                 region = null;
314             }
315         }
316 
317         final java.util.Locale locale = new java.util.Locale.Builder()
318                 .setLanguage(language_)
319                 .setScript(script_)
320                 .setRegion(region)
321                 .setExtension('u', locale_.getExtension('u'))
322                 .build();
323 
324         final Locale l = new Locale(locale);
325         l.setParentScope(getTopLevelScope(getParentScope()));
326         l.setPrototype(this.getPrototype());
327         return l;
328     }
329 
330     /**
331      * Returns a Locale with minimized subtags.
332      * @return a new Locale instance with minimized subtags
333      */
334     @JsxFunction
335     public Locale minimize() {
336         final java.util.Locale locale = new java.util.Locale.Builder()
337                 .setLanguage(language_)
338                 .setExtension('u', locale_.getExtension('u'))
339                 .build();
340 
341         final Locale l = new Locale(locale);
342         l.setParentScope(getTopLevelScope(getParentScope()));
343         l.setPrototype(this.getPrototype());
344         return l;
345     }
346 
347     /**
348      * Returns the locale's Unicode locale identifier string.
349      * @return the locale's Unicode locale identifier string
350      */
351     @JsxFunction(functionName = "toString")
352     public String jsToString() {
353         if (locale_ == null) {
354             return super.toString();
355         }
356         return locale_.toLanguageTag();
357     }
358 
359     /**
360      * {@inheritDoc}
361      */
362     @Override
363     public Object getDefaultValue(final Class<?> hint) {
364         if (getPrototype() != null && (String.class.equals(hint) || hint == null)) {
365             return jsToString();
366         }
367         return super.getDefaultValue(hint);
368     }
369 }