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