1 /*
2  * Copyright (C) 2008 The Android Open Source Project
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  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package libcore.icu;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 import android.icu.text.DateTimePatternGenerator;
21 import android.icu.util.Currency;
22 import android.icu.util.IllformedLocaleException;
23 import android.icu.util.ULocale;
24 
25 import com.android.icu.util.ExtendedCalendar;
26 import com.android.icu.util.LocaleNative;
27 
28 import java.util.Collections;
29 import java.util.Date;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.LinkedHashSet;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.Set;
37 import libcore.util.BasicLruCache;
38 
39 /**
40  * Makes ICU data accessible to Java.
41  * @hide
42  */
43 public final class ICU {
44 
45   @UnsupportedAppUsage
46   private static final BasicLruCache<String, String> CACHED_PATTERNS =
47       new BasicLruCache<String, String>(8);
48 
49   private static Locale[] availableLocalesCache;
50 
51   private static String[] isoCountries;
52   private static Set<String> isoCountriesSet;
53 
54   private static String[] isoLanguages;
55 
56   /**
57    * Avoid initialization with many dependencies here, because when this is called,
58    * lower-level classes, e.g. java.lang.System, are not initialized and java.lang.System
59    * relies on getIcuVersion().
60    */
61   static {
62 
63   }
64 
ICU()65   private ICU() {
66   }
67 
initializeCacheInZygote()68   public static void initializeCacheInZygote() {
69     // Fill CACHED_PATTERNS with the patterns from default locale and en-US initially.
70     // This should be called in Zygote pre-fork process and the initial values in the cache
71     // can be shared among app. The cache was filled by LocaleData in the older Android platform,
72     // but moved here, due to an performance issue http://b/161846393.
73     // It initializes 2 x 4 = 8 values in the CACHED_PATTERNS whose max size should be >= 8.
74     for (Locale locale : new Locale[] {Locale.US, Locale.getDefault()}) {
75       getTimePattern(locale, false, false);
76       getTimePattern(locale, false, true);
77       getTimePattern(locale, true, false);
78       getTimePattern(locale, true, true);
79     }
80   }
81 
82   /**
83    * Returns an array of two-letter ISO 639-1 language codes, either from ICU or our cache.
84    */
getISOLanguages()85   public static String[] getISOLanguages() {
86     if (isoLanguages == null) {
87       isoLanguages = getISOLanguagesNative();
88     }
89     return isoLanguages.clone();
90   }
91 
92   /**
93    * Returns an array of two-letter ISO 3166 country codes, either from ICU or our cache.
94    */
getISOCountries()95   public static String[] getISOCountries() {
96     return getISOCountriesInternal().clone();
97   }
98 
99   /**
100    * Returns true if the string is a 2-letter ISO 3166 country code.
101    */
isIsoCountry(String country)102   public static boolean isIsoCountry(String country) {
103     if (isoCountriesSet == null) {
104       String[] isoCountries = getISOCountriesInternal();
105       Set<String> newSet = new HashSet<>(isoCountries.length);
106       for (String isoCountry : isoCountries) {
107         newSet.add(isoCountry);
108       }
109       isoCountriesSet = newSet;
110     }
111     return country != null && isoCountriesSet.contains(country);
112   }
113 
getISOCountriesInternal()114   private static String[] getISOCountriesInternal() {
115     if (isoCountries == null) {
116       isoCountries = getISOCountriesNative();
117     }
118     return isoCountries;
119   }
120 
121 
122 
123   private static final int IDX_LANGUAGE = 0;
124   private static final int IDX_SCRIPT = 1;
125   private static final int IDX_REGION = 2;
126   private static final int IDX_VARIANT = 3;
127 
128   /*
129    * Parse the {Language, Script, Region, Variant*} section of the ICU locale
130    * ID. This is the bit that appears before the keyword separate "@". The general
131    * structure is a series of ASCII alphanumeric strings (subtags)
132    * separated by underscores.
133    *
134    * Each subtag is interpreted according to its position in the list of subtags
135    * AND its length (groan...). The various cases are explained in comments
136    * below.
137    */
parseLangScriptRegionAndVariants(String string, String[] outputArray)138   private static void parseLangScriptRegionAndVariants(String string,
139           String[] outputArray) {
140     final int first = string.indexOf('_');
141     final int second = string.indexOf('_', first + 1);
142     final int third = string.indexOf('_', second + 1);
143 
144     if (first == -1) {
145       outputArray[IDX_LANGUAGE] = string;
146     } else if (second == -1) {
147       // Language and country ("ja_JP") OR
148       // Language and script ("en_Latn") OR
149       // Language and variant ("en_POSIX").
150 
151       outputArray[IDX_LANGUAGE] = string.substring(0, first);
152       final String secondString = string.substring(first + 1);
153 
154       if (secondString.length() == 4) {
155           // 4 Letter ISO script code.
156           outputArray[IDX_SCRIPT] = secondString;
157       } else if (secondString.length() == 2 || secondString.length() == 3) {
158           // 2 or 3 Letter region code.
159           outputArray[IDX_REGION] = secondString;
160       } else {
161           // If we're here, the length of the second half is either 1 or greater
162           // than 5. Assume that ICU won't hand us malformed tags, and therefore
163           // assume the rest of the string is a series of variant tags.
164           outputArray[IDX_VARIANT] = secondString;
165       }
166     } else if (third == -1) {
167       // Language and country and variant ("ja_JP_TRADITIONAL") OR
168       // Language and script and variant ("en_Latn_POSIX") OR
169       // Language and script and region ("en_Latn_US"). OR
170       // Language and variant with multiple subtags ("en_POSIX_XISOP")
171 
172       outputArray[IDX_LANGUAGE] = string.substring(0, first);
173       final String secondString = string.substring(first + 1, second);
174       final String thirdString = string.substring(second + 1);
175 
176       if (secondString.length() == 4) {
177           // The second subtag is a script.
178           outputArray[IDX_SCRIPT] = secondString;
179 
180           // The third subtag can be either a region or a variant, depending
181           // on its length.
182           if (thirdString.length() == 2 || thirdString.length() == 3 ||
183                   thirdString.isEmpty()) {
184               outputArray[IDX_REGION] = thirdString;
185           } else {
186               outputArray[IDX_VARIANT] = thirdString;
187           }
188       } else if (secondString.isEmpty() ||
189               secondString.length() == 2 || secondString.length() == 3) {
190           // The second string is a region, and the third a variant.
191           outputArray[IDX_REGION] = secondString;
192           outputArray[IDX_VARIANT] = thirdString;
193       } else {
194           // Variant with multiple subtags.
195           outputArray[IDX_VARIANT] = string.substring(first + 1);
196       }
197     } else {
198       // Language, script, region and variant with 1 or more subtags
199       // ("en_Latn_US_POSIX") OR
200       // Language, region and variant with 2 or more subtags
201       // (en_US_POSIX_VARIANT).
202       outputArray[IDX_LANGUAGE] = string.substring(0, first);
203       final String secondString = string.substring(first + 1, second);
204       if (secondString.length() == 4) {
205           outputArray[IDX_SCRIPT] = secondString;
206           outputArray[IDX_REGION] = string.substring(second + 1, third);
207           outputArray[IDX_VARIANT] = string.substring(third + 1);
208       } else {
209           outputArray[IDX_REGION] = secondString;
210           outputArray[IDX_VARIANT] = string.substring(second + 1);
211       }
212     }
213   }
214 
215   /**
216    * Returns the appropriate {@code Locale} given a {@code String} of the form returned
217    * by {@code toString}. This is very lenient, and doesn't care what's between the underscores:
218    * this method can parse strings that {@code Locale.toString} won't produce.
219    * Used to remove duplication.
220    */
localeFromIcuLocaleId(String localeId)221   public static Locale localeFromIcuLocaleId(String localeId) {
222     // @ == ULOC_KEYWORD_SEPARATOR_UNICODE (uloc.h).
223     final int extensionsIndex = localeId.indexOf('@');
224 
225     Map<Character, String> extensionsMap = Collections.EMPTY_MAP;
226     Map<String, String> unicodeKeywordsMap = Collections.EMPTY_MAP;
227     Set<String> unicodeAttributeSet = Collections.EMPTY_SET;
228 
229     if (extensionsIndex != -1) {
230       extensionsMap = new HashMap<Character, String>();
231       unicodeKeywordsMap = new HashMap<String, String>();
232       unicodeAttributeSet = new HashSet<String>();
233 
234       // ICU sends us a semi-colon (ULOC_KEYWORD_ITEM_SEPARATOR) delimited string
235       // containing all "keywords" it could parse. An ICU keyword is a key-value pair
236       // separated by an "=" (ULOC_KEYWORD_ASSIGN).
237       //
238       // Each keyword item can be one of three things :
239       // - A unicode extension attribute list: In this case the item key is "attribute"
240       //   and the value is a hyphen separated list of unicode attributes.
241       // - A unicode extension keyword: In this case, the item key will be larger than
242       //   1 char in length, and the value will be the unicode extension value.
243       // - A BCP-47 extension subtag: In this case, the item key will be exactly one
244       //   char in length, and the value will be a sequence of unparsed subtags that
245       //   represent the extension.
246       //
247       // Note that this implies that unicode extension keywords are "promoted" to
248       // to the same namespace as the top level extension subtags and their values.
249       // There can't be any collisions in practice because the BCP-47 spec imposes
250       // restrictions on their lengths.
251       final String extensionsString = localeId.substring(extensionsIndex + 1);
252       final String[] extensions = extensionsString.split(";");
253       for (String extension : extensions) {
254         // This is the special key for the unicode attributes
255         if (extension.startsWith("attribute=")) {
256           String unicodeAttributeValues = extension.substring("attribute=".length());
257           for (String unicodeAttribute : unicodeAttributeValues.split("-")) {
258             unicodeAttributeSet.add(unicodeAttribute);
259           }
260         } else {
261           final int separatorIndex = extension.indexOf('=');
262 
263           if (separatorIndex == 1) {
264             // This is a BCP-47 extension subtag.
265             final String value = extension.substring(2);
266             final char extensionId = extension.charAt(0);
267 
268             extensionsMap.put(extensionId, value);
269           } else {
270             // This is a unicode extension keyword.
271             unicodeKeywordsMap.put(extension.substring(0, separatorIndex),
272             extension.substring(separatorIndex + 1));
273           }
274         }
275       }
276     }
277 
278     final String[] outputArray = new String[] { "", "", "", "" };
279     if (extensionsIndex == -1) {
280       parseLangScriptRegionAndVariants(localeId, outputArray);
281     } else {
282       parseLangScriptRegionAndVariants(localeId.substring(0, extensionsIndex),
283           outputArray);
284     }
285     Locale.Builder builder = new Locale.Builder();
286     builder.setLanguage(outputArray[IDX_LANGUAGE]);
287     builder.setRegion(outputArray[IDX_REGION]);
288     builder.setVariant(outputArray[IDX_VARIANT]);
289     builder.setScript(outputArray[IDX_SCRIPT]);
290     for (String attribute : unicodeAttributeSet) {
291       builder.addUnicodeLocaleAttribute(attribute);
292     }
293     for (Entry<String, String> keyword : unicodeKeywordsMap.entrySet()) {
294       builder.setUnicodeLocaleKeyword(keyword.getKey(), keyword.getValue());
295     }
296 
297     for (Entry<Character, String> extension : extensionsMap.entrySet()) {
298       builder.setExtension(extension.getKey(), extension.getValue());
299     }
300 
301     return builder.build();
302   }
303 
localesFromStrings(String[] localeNames)304   public static Locale[] localesFromStrings(String[] localeNames) {
305     // We need to remove duplicates caused by the conversion of "he" to "iw", et cetera.
306     // Java needs the obsolete code, ICU needs the modern code, but we let ICU know about
307     // both so that we never need to convert back when talking to it.
308     LinkedHashSet<Locale> set = new LinkedHashSet<Locale>();
309     for (String localeName : localeNames) {
310       set.add(localeFromIcuLocaleId(localeName));
311     }
312     return set.toArray(new Locale[set.size()]);
313   }
314 
getAvailableLocales()315   public static Locale[] getAvailableLocales() {
316     if (availableLocalesCache == null) {
317       availableLocalesCache = localesFromStrings(getAvailableLocalesNative());
318     }
319     return availableLocalesCache.clone();
320   }
321 
getTimePattern(Locale locale, boolean is24Hour, boolean withSecond)322   /* package */ static String getTimePattern(Locale locale, boolean is24Hour, boolean withSecond) {
323     final String skeleton;
324     if (withSecond) {
325       skeleton = is24Hour ? "Hms" : "hms";
326     } else {
327       skeleton = is24Hour ? "Hm" : "hm";
328     }
329     return getBestDateTimePattern(skeleton, locale);
330   }
331 
332   @UnsupportedAppUsage
getBestDateTimePattern(String skeleton, Locale locale)333   public static String getBestDateTimePattern(String skeleton, Locale locale) {
334     String languageTag = locale.toLanguageTag();
335     String key = skeleton + "\t" + languageTag;
336     synchronized (CACHED_PATTERNS) {
337       String pattern = CACHED_PATTERNS.get(key);
338       if (pattern == null) {
339         pattern = getBestDateTimePattern0(skeleton, locale);
340         CACHED_PATTERNS.put(key, pattern);
341       }
342       return pattern;
343     }
344   }
345 
getBestDateTimePattern0(String skeleton, Locale locale)346   private static String getBestDateTimePattern0(String skeleton, Locale locale) {
347       DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(locale);
348       return dtpg.getBestPattern(skeleton);
349   }
350 
351   @UnsupportedAppUsage
getBestDateTimePatternNative(String skeleton, String languageTag)352   private static String getBestDateTimePatternNative(String skeleton, String languageTag) {
353     return getBestDateTimePattern0(skeleton, Locale.forLanguageTag(languageTag));
354   }
355 
356   @UnsupportedAppUsage
getDateFormatOrder(String pattern)357   public static char[] getDateFormatOrder(String pattern) {
358     char[] result = new char[3];
359     int resultIndex = 0;
360     boolean sawDay = false;
361     boolean sawMonth = false;
362     boolean sawYear = false;
363 
364     for (int i = 0; i < pattern.length(); ++i) {
365       char ch = pattern.charAt(i);
366       if (ch == 'd' || ch == 'L' || ch == 'M' || ch == 'y') {
367         if (ch == 'd' && !sawDay) {
368           result[resultIndex++] = 'd';
369           sawDay = true;
370         } else if ((ch == 'L' || ch == 'M') && !sawMonth) {
371           result[resultIndex++] = 'M';
372           sawMonth = true;
373         } else if ((ch == 'y') && !sawYear) {
374           result[resultIndex++] = 'y';
375           sawYear = true;
376         }
377       } else if (ch == 'G') {
378         // Ignore the era specifier, if present.
379       } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
380         throw new IllegalArgumentException("Bad pattern character '" + ch + "' in " + pattern);
381       } else if (ch == '\'') {
382         if (i < pattern.length() - 1 && pattern.charAt(i + 1) == '\'') {
383           ++i;
384         } else {
385           i = pattern.indexOf('\'', i + 1);
386           if (i == -1) {
387             throw new IllegalArgumentException("Bad quoting in " + pattern);
388           }
389           ++i;
390         }
391       } else {
392         // Ignore spaces and punctuation.
393       }
394     }
395     return result;
396   }
397 
398   /**
399    * {@link java.time.format.DateTimeFormatter} does not handle some date symbols, e.g. 'B' / 'b',
400    * and thus we use a heuristic algorithm to remove the symbol. See http://b/174804526.
401    * See {@link #transformIcuDateTimePattern(String)} for documentation about the implementation.
402    */
transformIcuDateTimePattern_forJavaTime(String pattern)403   public static String transformIcuDateTimePattern_forJavaTime(String pattern) {
404     return transformIcuDateTimePattern(pattern);
405   }
406 
407   /**
408    * {@link java.text.SimpleDateFormat} does not handle some date symbols, e.g. 'B' / 'b',
409    * and simply ignore the symbol in formatting. Instead, we should avoid exposing the symbol
410    * entirely in all public APIs, e.g. {@link java.text.SimpleDateFormat#toPattern()},
411    * and thus we use a heuristic algorithm to remove the symbol. See http://b/174804526.
412    * See {@link #transformIcuDateTimePattern(String)} for documentation about the implementation.
413    */
transformIcuDateTimePattern_forJavaText(String pattern)414   public static String transformIcuDateTimePattern_forJavaText(String pattern) {
415     return transformIcuDateTimePattern(pattern);
416   }
417 
418   /**
419    * Rewrite the date/time pattern coming ICU to be consumed by libcore classes.
420    * It's an ideal place to rewrite the pattern entirely when multiple symbols not digested
421    * by libcore need to be removed/processed. Rewriting in single place could be more efficient
422    * in a small or constant number of scans instead of scanning for every symbol.
423    *
424    * {@link LocaleData#initLocaleData(Locale)} also rewrites time format, but only a subset of
425    * patterns. In the future, that should migrate to this function in order to handle the symbols
426    * in one place, but now separate because java.text and java.time handles different sets of
427    * symbols.
428    */
transformIcuDateTimePattern(String pattern)429   private static String transformIcuDateTimePattern(String pattern) {
430     if (pattern == null) {
431       return null;
432     }
433 
434     // For details about the different symbols, see
435     // http://cldr.unicode.org/translation/date-time-1/date-time-patterns#TOC-Day-period-patterns
436     // The symbols B means "Day periods with locale-specific ranges".
437     // English example: 2:00 at night, 10:00 in the morning, 12:00 in the afternoon.
438     boolean contains_B = pattern.indexOf('B') != -1;
439     // AM, PM, noon and midnight. English example: 10:00 AM, 12:00 noon, 7:00 PM
440     boolean contains_b = pattern.indexOf('b') != -1;
441 
442     // Simply remove the symbol 'B' and 'b' if 24-hour 'H' exists because the 24-hour format
443     // provides enough information and the day periods are optional. See http://b/174804526.
444     // Don't handle symbol 'B'/'b' with 12-hour 'h' because it's much more complicated because
445     // we likely need to replace 'B'/'b' with 'a' inserted into a new right position or use other
446     // ways.
447     boolean remove_B_and_b = (contains_B || contains_b) && (pattern.indexOf('H') != -1);
448 
449     if (remove_B_and_b) {
450       pattern = rewriteIcuDateTimePattern(pattern);
451     }
452     return pattern;
453   }
454 
455   /**
456    * Rewrite pattern with heuristics. It's known to
457    *   - Remove 'b' and 'B' from simple patterns, e.g. "B H:mm" and "dd-MM-yy B HH:mm:ss" only.
458    *   - (Append the new heuristics)
459    */
rewriteIcuDateTimePattern(String pattern)460   private static String rewriteIcuDateTimePattern(String pattern) {
461     // The below implementation can likely be replaced by a regular expression via
462     // String.replaceAll(). However, it's known that libcore's regex implementation is more
463     // memory-intensive, and the below implementation is likely cheaper, but it's not yet measured.
464     StringBuilder sb = new StringBuilder(pattern.length());
465     char prev = ' '; // the initial value is not used.
466     for (int i = 0; i < pattern.length(); i++) {
467       char curr = pattern.charAt(i);
468       switch(curr) {
469         case 'B':
470         case 'b':
471           // Ignore 'B' and 'b'
472           break;
473         case ' ': // Ascii whitespace
474           // caveat: Ideally it's a case for all Unicode whitespaces by UCharacter.isUWhiteSpace(c)
475           // but checking ascii whitespace only is enough for the CLDR data when this is written.
476           if (i != 0 && (prev == 'B' || prev == 'b')) {
477             // Ignore the whitespace behind the symbol 'B'/'b' because it's likely a whitespace to
478             // separate the day period with the next text.
479           } else {
480             sb.append(curr);
481           }
482           break;
483         default:
484           sb.append(curr);
485           break;
486       }
487       prev = curr;
488     }
489 
490     // Remove the trailing whitespace which is likely following the symbol 'B'/'b' in the original
491     // pattern, e.g. "hh:mm B" (12:00 in the afternoon).
492     int lastIndex = sb.length() - 1;
493     if (lastIndex >= 0 && sb.charAt(lastIndex) == ' ') {
494       sb.deleteCharAt(lastIndex);
495     }
496     return sb.toString();
497   }
498 
499   /**
500    * Returns the version of the CLDR data in use, such as "22.1.1".
501    *
502    */
getCldrVersion()503   public static native String getCldrVersion();
504 
505   /**
506    * Returns the icu4c version in use, such as "50.1.1".
507    */
getIcuVersion()508   public static native String getIcuVersion();
509 
510   /**
511    * Returns the Unicode version our ICU supports, such as "6.2".
512    */
getUnicodeVersion()513   public static native String getUnicodeVersion();
514 
515   // --- Errors.
516 
517   // --- Native methods accessing ICU's database.
518 
getAvailableLocalesNative()519   private static native String[] getAvailableLocalesNative();
520 
521     /**
522      * Query ICU for the currency being used in the country right now.
523      * @param countryCode ISO 3166 two-letter country code
524      * @return ISO 4217 3-letter currency code if found, otherwise null.
525      */
getCurrencyCode(String countryCode)526   public static String getCurrencyCode(String countryCode) {
527       // Fail fast when country code is not valid.
528       if (countryCode == null || countryCode.length() == 0) {
529           return null;
530       }
531       final ULocale countryLocale;
532       try {
533           countryLocale = new ULocale.Builder().setRegion(countryCode).build();
534       } catch (IllformedLocaleException e) {
535           return null; // Return null on invalid country code.
536       }
537       String[] isoCodes = Currency.getAvailableCurrencyCodes(countryLocale, new Date());
538       if (isoCodes == null || isoCodes.length == 0) {
539         return null;
540       }
541       return isoCodes[0];
542   }
543 
544 
getISO3Country(String languageTag)545   public static native String getISO3Country(String languageTag);
546 
getISO3Language(String languageTag)547   public static native String getISO3Language(String languageTag);
548 
549   /**
550    * @deprecated Use {@link android.icu.util.ULocale#addLikelySubtags(ULocale)} instead.
551    * The method is only kept for @UnsupportedAppUsage.
552    */
553   @UnsupportedAppUsage
554   @Deprecated
addLikelySubtags(Locale locale)555   public static Locale addLikelySubtags(Locale locale) {
556       return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale();
557   }
558 
559   /**
560    * @return ICU localeID
561    * @deprecated Use {@link android.icu.util.ULocale#addLikelySubtags(ULocale)} instead.
562    * The method is only kept for @UnsupportedAppUsage.
563    */
564   @UnsupportedAppUsage
565   @Deprecated
addLikelySubtags(String locale)566   public static String addLikelySubtags(String locale) {
567       return ULocale.addLikelySubtags(new ULocale(locale)).getName();
568   }
569 
570   /**
571    * @deprecated use {@link java.util.Locale#getScript()} instead. This has been kept
572    *     around only for the support library.
573    */
574   @UnsupportedAppUsage
575   @Deprecated
getScript(String locale)576   public static native String getScript(String locale);
577 
getISOLanguagesNative()578   private static native String[] getISOLanguagesNative();
getISOCountriesNative()579   private static native String[] getISOCountriesNative();
580 
581   /**
582    * Takes a BCP-47 language tag (Locale.toLanguageTag()). e.g. en-US, not en_US
583    */
setDefaultLocale(String languageTag)584   public static void setDefaultLocale(String languageTag) {
585     LocaleNative.setDefault(languageTag);
586   }
587 
588   /**
589    * Returns a locale name, not a BCP-47 language tag. e.g. en_US not en-US.
590    */
getDefaultLocale()591   public static native String getDefaultLocale();
592 
593 
594   /**
595    * @param calendarType LDML-defined legacy calendar type. See keyTypeData.txt in ICU.
596    */
getExtendedCalendar(Locale locale, String calendarType)597   public static ExtendedCalendar getExtendedCalendar(Locale locale, String calendarType) {
598       ULocale uLocale = ULocale.forLocale(locale)
599               .setKeywordValue("calendar", calendarType);
600       return ExtendedCalendar.getInstance(uLocale);
601   }
602 }
603