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