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