1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 /* 4 ******************************************************************************* 5 * Copyright (C) 2009-2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 ******************************************************************************* 8 */ 9 package com.ibm.icu.impl; 10 11 import java.util.ArrayList; 12 import java.util.Collections; 13 import java.util.Comparator; 14 import java.util.HashMap; 15 import java.util.HashSet; 16 import java.util.Iterator; 17 import java.util.List; 18 import java.util.Locale; 19 import java.util.Map; 20 import java.util.Map.Entry; 21 import java.util.MissingResourceException; 22 import java.util.Set; 23 24 import com.ibm.icu.impl.CurrencyData.CurrencyDisplayInfo; 25 import com.ibm.icu.impl.locale.AsciiUtil; 26 import com.ibm.icu.lang.UCharacter; 27 import com.ibm.icu.lang.UScript; 28 import com.ibm.icu.text.BreakIterator; 29 import com.ibm.icu.text.CaseMap; 30 import com.ibm.icu.text.DisplayContext; 31 import com.ibm.icu.text.DisplayContext.Type; 32 import com.ibm.icu.text.LocaleDisplayNames; 33 import com.ibm.icu.util.ULocale; 34 import com.ibm.icu.util.UResourceBundle; 35 36 public class LocaleDisplayNamesImpl extends LocaleDisplayNames { 37 private final ULocale locale; 38 private final DialectHandling dialectHandling; 39 private final DisplayContext capitalization; 40 private final DisplayContext nameLength; 41 private final DisplayContext substituteHandling; 42 private final DataTable langData; 43 private final DataTable regionData; 44 // Compiled SimpleFormatter patterns. 45 private final String separatorFormat; 46 private final String format; 47 private final String keyTypeFormat; 48 private final char formatOpenParen; 49 private final char formatReplaceOpenParen; 50 private final char formatCloseParen; 51 private final char formatReplaceCloseParen; 52 private final CurrencyDisplayInfo currencyDisplayInfo; 53 54 private static final Cache cache = new Cache(); 55 56 /** 57 * Capitalization context usage types for locale display names 58 */ 59 private enum CapitalizationContextUsage { 60 LANGUAGE, 61 SCRIPT, 62 TERRITORY, 63 VARIANT, 64 KEY, 65 KEYVALUE 66 } 67 /** 68 * Capitalization transforms. For each usage type, indicates whether to titlecase for 69 * the context specified in capitalization (which we know at construction time). 70 */ 71 private boolean[] capitalizationUsage = null; 72 /** 73 * Map from resource key to CapitalizationContextUsage value 74 */ 75 private static final Map<String, CapitalizationContextUsage> contextUsageTypeMap; 76 static { 77 contextUsageTypeMap=new HashMap<String, CapitalizationContextUsage>(); 78 contextUsageTypeMap.put("languages", CapitalizationContextUsage.LANGUAGE); 79 contextUsageTypeMap.put("script", CapitalizationContextUsage.SCRIPT); 80 contextUsageTypeMap.put("territory", CapitalizationContextUsage.TERRITORY); 81 contextUsageTypeMap.put("variant", CapitalizationContextUsage.VARIANT); 82 contextUsageTypeMap.put("key", CapitalizationContextUsage.KEY); 83 contextUsageTypeMap.put("keyValue", CapitalizationContextUsage.KEYVALUE); 84 } 85 /** 86 * BreakIterator to use for capitalization 87 */ 88 private transient BreakIterator capitalizationBrkIter = null; 89 90 private static final CaseMap.Title TO_TITLE_WHOLE_STRING_NO_LOWERCASE = 91 CaseMap.toTitle().wholeString().noLowercase(); 92 toTitleWholeStringNoLowercase(ULocale locale, String s)93 private static String toTitleWholeStringNoLowercase(ULocale locale, String s) { 94 return TO_TITLE_WHOLE_STRING_NO_LOWERCASE.apply(locale.toLocale(), null, s); 95 } 96 getInstance(ULocale locale, DialectHandling dialectHandling)97 public static LocaleDisplayNames getInstance(ULocale locale, DialectHandling dialectHandling) { 98 synchronized (cache) { 99 return cache.get(locale, dialectHandling); 100 } 101 } 102 getInstance(ULocale locale, DisplayContext... contexts)103 public static LocaleDisplayNames getInstance(ULocale locale, DisplayContext... contexts) { 104 synchronized (cache) { 105 return cache.get(locale, contexts); 106 } 107 } 108 109 private final class CapitalizationContextSink extends UResource.Sink { 110 boolean hasCapitalizationUsage = false; 111 112 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)113 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 114 UResource.Table contextsTable = value.getTable(); 115 for (int i = 0; contextsTable.getKeyAndValue(i, key, value); ++i) { 116 117 CapitalizationContextUsage usage = contextUsageTypeMap.get(key.toString()); 118 if (usage == null) { continue; }; 119 120 int[] intVector = value.getIntVector(); 121 if (intVector.length < 2) { continue; } 122 123 int titlecaseInt = (capitalization == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU) 124 ? intVector[0] : intVector[1]; 125 if (titlecaseInt == 0) { continue; } 126 127 capitalizationUsage[usage.ordinal()] = true; 128 hasCapitalizationUsage = true; 129 } 130 } 131 } 132 LocaleDisplayNamesImpl(ULocale locale, DialectHandling dialectHandling)133 public LocaleDisplayNamesImpl(ULocale locale, DialectHandling dialectHandling) { 134 this(locale, (dialectHandling==DialectHandling.STANDARD_NAMES)? DisplayContext.STANDARD_NAMES: DisplayContext.DIALECT_NAMES, 135 DisplayContext.CAPITALIZATION_NONE); 136 } 137 LocaleDisplayNamesImpl(ULocale locale, DisplayContext... contexts)138 public LocaleDisplayNamesImpl(ULocale locale, DisplayContext... contexts) { 139 DialectHandling dialectHandling = DialectHandling.STANDARD_NAMES; 140 DisplayContext capitalization = DisplayContext.CAPITALIZATION_NONE; 141 DisplayContext nameLength = DisplayContext.LENGTH_FULL; 142 DisplayContext substituteHandling = DisplayContext.SUBSTITUTE; 143 for (DisplayContext contextItem : contexts) { 144 switch (contextItem.type()) { 145 case DIALECT_HANDLING: 146 dialectHandling = (contextItem.value()==DisplayContext.STANDARD_NAMES.value())? 147 DialectHandling.STANDARD_NAMES: DialectHandling.DIALECT_NAMES; 148 break; 149 case CAPITALIZATION: 150 capitalization = contextItem; 151 break; 152 case DISPLAY_LENGTH: 153 nameLength = contextItem; 154 break; 155 case SUBSTITUTE_HANDLING: 156 substituteHandling = contextItem; 157 break; 158 default: 159 break; 160 } 161 } 162 163 this.dialectHandling = dialectHandling; 164 this.capitalization = capitalization; 165 this.nameLength = nameLength; 166 this.substituteHandling = substituteHandling; 167 this.langData = LangDataTables.impl.get(locale, substituteHandling == DisplayContext.NO_SUBSTITUTE); 168 this.regionData = RegionDataTables.impl.get(locale, substituteHandling == DisplayContext.NO_SUBSTITUTE); 169 this.locale = ULocale.ROOT.equals(langData.getLocale()) ? regionData.getLocale() : 170 langData.getLocale(); 171 172 // Note, by going through DataTable, this uses table lookup rather than straight lookup. 173 // That should get us the same data, I think. This way we don't have to explicitly 174 // load the bundle again. Using direct lookup didn't seem to make an appreciable 175 // difference in performance. 176 String sep = langData.get("localeDisplayPattern", "separator"); 177 if (sep == null || "separator".equals(sep)) { 178 sep = "{0}, {1}"; 179 } 180 StringBuilder sb = new StringBuilder(); 181 this.separatorFormat = SimpleFormatterImpl.compileToStringMinMaxArguments(sep, sb, 2, 2); 182 183 String pattern = langData.get("localeDisplayPattern", "pattern"); 184 if (pattern == null || "pattern".equals(pattern)) { 185 pattern = "{0} ({1})"; 186 } 187 this.format = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); 188 if (pattern.contains("(")) { 189 formatOpenParen = '('; 190 formatCloseParen = ')'; 191 formatReplaceOpenParen = '['; 192 formatReplaceCloseParen = ']'; 193 } else { 194 formatOpenParen = '('; 195 formatCloseParen = ')'; 196 formatReplaceOpenParen = '['; 197 formatReplaceCloseParen = ']'; 198 } 199 200 String keyTypePattern = langData.get("localeDisplayPattern", "keyTypePattern"); 201 if (keyTypePattern == null || "keyTypePattern".equals(keyTypePattern)) { 202 keyTypePattern = "{0}={1}"; 203 } 204 this.keyTypeFormat = SimpleFormatterImpl.compileToStringMinMaxArguments( 205 keyTypePattern, sb, 2, 2); 206 207 // Get values from the contextTransforms data if we need them 208 // Also check whether we will need a break iterator (depends on the data) 209 boolean needBrkIter = false; 210 if (capitalization == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU || 211 capitalization == DisplayContext.CAPITALIZATION_FOR_STANDALONE) { 212 capitalizationUsage = new boolean[CapitalizationContextUsage.values().length]; // initialized to all false 213 ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); 214 CapitalizationContextSink sink = new CapitalizationContextSink(); 215 try { 216 rb.getAllItemsWithFallback("contextTransforms", sink); 217 } 218 catch (MissingResourceException e) { 219 // Silently ignore. Not every locale has contextTransforms. 220 } 221 needBrkIter = sink.hasCapitalizationUsage; 222 } 223 // Get a sentence break iterator if we will need it 224 if (needBrkIter || capitalization == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) { 225 capitalizationBrkIter = BreakIterator.getSentenceInstance(locale); 226 } 227 228 this.currencyDisplayInfo = CurrencyData.provider.getInstance(locale, false); 229 } 230 231 @Override getLocale()232 public ULocale getLocale() { 233 return locale; 234 } 235 236 @Override getDialectHandling()237 public DialectHandling getDialectHandling() { 238 return dialectHandling; 239 } 240 241 @Override getContext(DisplayContext.Type type)242 public DisplayContext getContext(DisplayContext.Type type) { 243 DisplayContext result; 244 switch (type) { 245 case DIALECT_HANDLING: 246 result = (dialectHandling==DialectHandling.STANDARD_NAMES)? DisplayContext.STANDARD_NAMES: DisplayContext.DIALECT_NAMES; 247 break; 248 case CAPITALIZATION: 249 result = capitalization; 250 break; 251 case DISPLAY_LENGTH: 252 result = nameLength; 253 break; 254 case SUBSTITUTE_HANDLING: 255 result = substituteHandling; 256 break; 257 default: 258 result = DisplayContext.STANDARD_NAMES; // hmm, we should do something else here 259 break; 260 } 261 return result; 262 } 263 adjustForUsageAndContext(CapitalizationContextUsage usage, String name)264 private String adjustForUsageAndContext(CapitalizationContextUsage usage, String name) { 265 if (name != null && name.length() > 0 && UCharacter.isLowerCase(name.codePointAt(0)) && 266 (capitalization==DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE || 267 (capitalizationUsage != null && capitalizationUsage[usage.ordinal()]) )) { 268 // Note, won't have capitalizationUsage != null && capitalizationUsage[usage.ordinal()] 269 // unless capitalization is CAPITALIZATION_FOR_UI_LIST_OR_MENU or CAPITALIZATION_FOR_STANDALONE 270 synchronized (this) { 271 if (capitalizationBrkIter == null) { 272 // should only happen when deserializing, etc. 273 capitalizationBrkIter = BreakIterator.getSentenceInstance(locale); 274 } 275 return UCharacter.toTitleCase(locale, name, capitalizationBrkIter, 276 UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT); 277 } 278 } 279 return name; 280 } 281 282 @Override localeDisplayName(ULocale locale)283 public String localeDisplayName(ULocale locale) { 284 return localeDisplayNameInternal(locale); 285 } 286 287 @Override localeDisplayName(Locale locale)288 public String localeDisplayName(Locale locale) { 289 return localeDisplayNameInternal(ULocale.forLocale(locale)); 290 } 291 292 @Override localeDisplayName(String localeId)293 public String localeDisplayName(String localeId) { 294 return localeDisplayNameInternal(new ULocale(localeId)); 295 } 296 297 // TODO: implement use of capitalization localeDisplayNameInternal(ULocale locale)298 private String localeDisplayNameInternal(ULocale locale) { 299 // lang 300 // lang (script, country, variant, keyword=value, ...) 301 // script, country, variant, keyword=value, ... 302 303 String resultName = null; 304 305 String lang = locale.getLanguage(); 306 307 // Empty basename indicates root locale (keywords are ignored for this). 308 // For the display name, we treat this as unknown language (ICU-20273). 309 if (lang.isEmpty()) { 310 lang = "und"; 311 } 312 String script = locale.getScript(); 313 String country = locale.getCountry(); 314 String variant = locale.getVariant(); 315 316 boolean hasScript = script.length() > 0; 317 boolean hasCountry = country.length() > 0; 318 boolean hasVariant = variant.length() > 0; 319 320 // always have a value for lang 321 if (dialectHandling == DialectHandling.DIALECT_NAMES) { 322 do { // loop construct is so we can break early out of search 323 if (hasScript && hasCountry) { 324 String langScriptCountry = lang + '_' + script + '_' + country; 325 String result = localeIdName(langScriptCountry); 326 if (result != null && !result.equals(langScriptCountry)) { 327 resultName = result; 328 hasScript = false; 329 hasCountry = false; 330 break; 331 } 332 } 333 if (hasScript) { 334 String langScript = lang + '_' + script; 335 String result = localeIdName(langScript); 336 if (result != null && !result.equals(langScript)) { 337 resultName = result; 338 hasScript = false; 339 break; 340 } 341 } 342 if (hasCountry) { 343 String langCountry = lang + '_' + country; 344 String result = localeIdName(langCountry); 345 if (result != null && !result.equals(langCountry)) { 346 resultName = result; 347 hasCountry = false; 348 break; 349 } 350 } 351 } while (false); 352 } 353 354 if (resultName == null) { 355 String result = localeIdName(lang); 356 if (result == null) { return null; } 357 resultName = result 358 .replace(formatOpenParen, formatReplaceOpenParen) 359 .replace(formatCloseParen, formatReplaceCloseParen); 360 } 361 362 StringBuilder buf = new StringBuilder(); 363 if (hasScript) { 364 // first element, don't need appendWithSep 365 String result = scriptDisplayNameInContext(script, true); 366 if (result == null) { return null; } 367 buf.append(result 368 .replace(formatOpenParen, formatReplaceOpenParen) 369 .replace(formatCloseParen, formatReplaceCloseParen)); 370 } 371 if (hasCountry) { 372 String result = regionDisplayName(country, true); 373 if (result == null) { return null; } 374 appendWithSep(result 375 .replace(formatOpenParen, formatReplaceOpenParen) 376 .replace(formatCloseParen, formatReplaceCloseParen), buf); 377 } 378 if (hasVariant) { 379 String result = variantDisplayName(variant, true); 380 if (result == null) { return null; } 381 appendWithSep(result 382 .replace(formatOpenParen, formatReplaceOpenParen) 383 .replace(formatCloseParen, formatReplaceCloseParen), buf); 384 } 385 386 Iterator<String> keys = locale.getKeywords(); 387 if (keys != null) { 388 while (keys.hasNext()) { 389 String key = keys.next(); 390 String value = locale.getKeywordValue(key); 391 String keyDisplayName = keyDisplayName(key, true); 392 if (keyDisplayName == null) { return null; } 393 keyDisplayName = keyDisplayName 394 .replace(formatOpenParen, formatReplaceOpenParen) 395 .replace(formatCloseParen, formatReplaceCloseParen); 396 String valueDisplayName = keyValueDisplayName(key, value, true); 397 if (valueDisplayName == null) { return null; } 398 valueDisplayName = valueDisplayName 399 .replace(formatOpenParen, formatReplaceOpenParen) 400 .replace(formatCloseParen, formatReplaceCloseParen); 401 if (!valueDisplayName.equals(value)) { 402 appendWithSep(valueDisplayName, buf); 403 } else if (!key.equals(keyDisplayName)) { 404 String keyValue = SimpleFormatterImpl.formatCompiledPattern( 405 keyTypeFormat, keyDisplayName, valueDisplayName); 406 appendWithSep(keyValue, buf); 407 } else { 408 appendWithSep(keyDisplayName, buf) 409 .append("=") 410 .append(valueDisplayName); 411 } 412 } 413 } 414 415 String resultRemainder = null; 416 if (buf.length() > 0) { 417 resultRemainder = buf.toString(); 418 } 419 420 if (resultRemainder != null) { 421 resultName = SimpleFormatterImpl.formatCompiledPattern( 422 format, resultName, resultRemainder); 423 } 424 425 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, resultName); 426 } 427 localeIdName(String localeId)428 private String localeIdName(String localeId) { 429 String locIdName; 430 if (nameLength == DisplayContext.LENGTH_SHORT) { 431 locIdName = langData.get("Languages%short", localeId); 432 if (locIdName != null && !locIdName.equals(localeId)) { 433 return locIdName; 434 } 435 } 436 locIdName = langData.get("Languages", localeId); 437 if ((locIdName == null || locIdName.equals(localeId)) && localeId.indexOf('_') < 0) { 438 // Canonicalize lang and try again, ICU-20870 439 // (only for language codes without script or region) 440 ULocale canonLocale = ULocale.createCanonical(localeId); 441 String canonLocId = canonLocale.getName(); 442 if (nameLength == DisplayContext.LENGTH_SHORT) { 443 locIdName = langData.get("Languages%short", canonLocId); 444 if (locIdName != null && !locIdName.equals(canonLocId)) { 445 return locIdName; 446 } 447 } 448 locIdName = langData.get("Languages", canonLocId); 449 } 450 return locIdName; 451 } 452 453 @Override languageDisplayName(String lang)454 public String languageDisplayName(String lang) { 455 // Special case to eliminate non-languages, which pollute our data. 456 if (lang.equals("root") || lang.indexOf('_') != -1) { 457 return substituteHandling == DisplayContext.SUBSTITUTE ? lang : null; 458 } 459 String langName; 460 if (nameLength == DisplayContext.LENGTH_SHORT) { 461 langName = langData.get("Languages%short", lang); 462 if (langName != null && !langName.equals(lang)) { 463 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName); 464 } 465 } 466 langName = langData.get("Languages", lang); 467 if (langName == null || langName.equals(lang)) { 468 // Canonicalize lang and try again, ICU-20870 469 ULocale canonLocale = ULocale.createCanonical(lang); 470 String canonLocId = canonLocale.getName(); 471 if (nameLength == DisplayContext.LENGTH_SHORT) { 472 langName = langData.get("Languages%short", canonLocId); 473 if (langName != null && !langName.equals(canonLocId)) { 474 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName); 475 } 476 } 477 langName = langData.get("Languages", canonLocId); 478 } 479 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName); 480 } 481 482 @Override scriptDisplayName(String script)483 public String scriptDisplayName(String script) { 484 String str = langData.get("Scripts%stand-alone", script); 485 if (str == null || str.equals(script)) { 486 if (nameLength == DisplayContext.LENGTH_SHORT) { 487 str = langData.get("Scripts%short", script); 488 if (str != null && !str.equals(script)) { 489 return adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, str); 490 } 491 } 492 str = langData.get("Scripts", script); 493 } 494 return adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, str); 495 } 496 scriptDisplayNameInContext(String script, boolean skipAdjust)497 private String scriptDisplayNameInContext(String script, boolean skipAdjust) { 498 if (nameLength == DisplayContext.LENGTH_SHORT) { 499 String scriptName = langData.get("Scripts%short", script); 500 if (scriptName != null && !scriptName.equals(script)) { 501 return skipAdjust? scriptName: adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, scriptName); 502 } 503 } 504 String scriptName = langData.get("Scripts", script); 505 return skipAdjust? scriptName: adjustForUsageAndContext(CapitalizationContextUsage.SCRIPT, scriptName); 506 } 507 508 @Override scriptDisplayNameInContext(String script)509 public String scriptDisplayNameInContext(String script) { 510 return scriptDisplayNameInContext(script, false); 511 } 512 513 @Override scriptDisplayName(int scriptCode)514 public String scriptDisplayName(int scriptCode) { 515 return scriptDisplayName(UScript.getShortName(scriptCode)); 516 } 517 regionDisplayName(String region, boolean skipAdjust)518 private String regionDisplayName(String region, boolean skipAdjust) { 519 if (nameLength == DisplayContext.LENGTH_SHORT) { 520 String regionName = regionData.get("Countries%short", region); 521 if (regionName != null && !regionName.equals(region)) { 522 return skipAdjust? regionName: adjustForUsageAndContext(CapitalizationContextUsage.TERRITORY, regionName); 523 } 524 } 525 String regionName = regionData.get("Countries", region); 526 return skipAdjust? regionName: adjustForUsageAndContext(CapitalizationContextUsage.TERRITORY, regionName); 527 } 528 529 @Override regionDisplayName(String region)530 public String regionDisplayName(String region) { 531 return regionDisplayName(region, false); 532 } 533 variantDisplayName(String variant, boolean skipAdjust)534 private String variantDisplayName(String variant, boolean skipAdjust) { 535 // don't have a resource for short variant names 536 String variantName = langData.get("Variants", variant); 537 return skipAdjust? variantName: adjustForUsageAndContext(CapitalizationContextUsage.VARIANT, variantName); 538 } 539 540 @Override variantDisplayName(String variant)541 public String variantDisplayName(String variant) { 542 return variantDisplayName(variant, false); 543 } 544 keyDisplayName(String key, boolean skipAdjust)545 private String keyDisplayName(String key, boolean skipAdjust) { 546 // don't have a resource for short key names 547 String keyName = langData.get("Keys", key); 548 return skipAdjust? keyName: adjustForUsageAndContext(CapitalizationContextUsage.KEY, keyName); 549 } 550 551 @Override keyDisplayName(String key)552 public String keyDisplayName(String key) { 553 return keyDisplayName(key, false); 554 } 555 keyValueDisplayName(String key, String value, boolean skipAdjust)556 private String keyValueDisplayName(String key, String value, boolean skipAdjust) { 557 String keyValueName = null; 558 559 if (key.equals("currency")) { 560 keyValueName = currencyDisplayInfo.getName(AsciiUtil.toUpperString(value)); 561 if (keyValueName == null) { 562 keyValueName = value; 563 } 564 } else { 565 if (nameLength == DisplayContext.LENGTH_SHORT) { 566 String tmp = langData.get("Types%short", key, value); 567 if (tmp != null && !tmp.equals(value)) { 568 keyValueName = tmp; 569 } 570 } 571 if (keyValueName == null) { 572 keyValueName = langData.get("Types", key, value); 573 } 574 } 575 576 return skipAdjust? keyValueName: adjustForUsageAndContext(CapitalizationContextUsage.KEYVALUE, keyValueName); 577 } 578 579 @Override keyValueDisplayName(String key, String value)580 public String keyValueDisplayName(String key, String value) { 581 return keyValueDisplayName(key, value, false); 582 } 583 584 @Override getUiListCompareWholeItems(Set<ULocale> localeSet, Comparator<UiListItem> comparator)585 public List<UiListItem> getUiListCompareWholeItems(Set<ULocale> localeSet, Comparator<UiListItem> comparator) { 586 DisplayContext capContext = getContext(Type.CAPITALIZATION); 587 588 List<UiListItem> result = new ArrayList<UiListItem>(); 589 Map<ULocale,Set<ULocale>> baseToLocales = new HashMap<ULocale,Set<ULocale>>(); 590 ULocale.Builder builder = new ULocale.Builder(); 591 for (ULocale locOriginal : localeSet) { 592 builder.setLocale(locOriginal); // verify well-formed. We do this here so that we consistently throw exception 593 ULocale loc = ULocale.addLikelySubtags(locOriginal); 594 ULocale base = new ULocale(loc.getLanguage()); 595 Set<ULocale> locales = baseToLocales.get(base); 596 if (locales == null) { 597 baseToLocales.put(base, locales = new HashSet<ULocale>()); 598 } 599 locales.add(loc); 600 } 601 for (Entry<ULocale, Set<ULocale>> entry : baseToLocales.entrySet()) { 602 ULocale base = entry.getKey(); 603 Set<ULocale> values = entry.getValue(); 604 if (values.size() == 1) { 605 ULocale locale = values.iterator().next(); 606 result.add(newRow(ULocale.minimizeSubtags(locale, ULocale.Minimize.FAVOR_SCRIPT), capContext)); 607 } else { 608 Set<String> scripts = new HashSet<String>(); 609 Set<String> regions = new HashSet<String>(); 610 // need the follow two steps to make sure that unusual scripts or regions are displayed 611 ULocale maxBase = ULocale.addLikelySubtags(base); 612 scripts.add(maxBase.getScript()); 613 regions.add(maxBase.getCountry()); 614 for (ULocale locale : values) { 615 scripts.add(locale.getScript()); 616 regions.add(locale.getCountry()); 617 } 618 boolean hasScripts = scripts.size() > 1; 619 boolean hasRegions = regions.size() > 1; 620 for (ULocale locale : values) { 621 ULocale.Builder modified = builder.setLocale(locale); 622 if (!hasScripts) { 623 modified.setScript(""); 624 } 625 if (!hasRegions) { 626 modified.setRegion(""); 627 } 628 result.add(newRow(modified.build(), capContext)); 629 } 630 } 631 } 632 Collections.sort(result, comparator); 633 return result; 634 } 635 newRow(ULocale modified, DisplayContext capContext)636 private UiListItem newRow(ULocale modified, DisplayContext capContext) { 637 ULocale minimized = ULocale.minimizeSubtags(modified, ULocale.Minimize.FAVOR_SCRIPT); 638 String tempName = modified.getDisplayName(locale); 639 boolean titlecase = capContext == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU; 640 String nameInDisplayLocale = 641 titlecase ? toTitleWholeStringNoLowercase(locale, tempName) : tempName; 642 tempName = modified.getDisplayName(modified); 643 String nameInSelf = capContext == 644 DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU ? 645 toTitleWholeStringNoLowercase(modified, tempName) : tempName; 646 return new UiListItem(minimized, modified, nameInDisplayLocale, nameInSelf); 647 } 648 649 public static class DataTable { 650 final boolean nullIfNotFound; 651 DataTable(boolean nullIfNotFound)652 DataTable(boolean nullIfNotFound) { 653 this.nullIfNotFound = nullIfNotFound; 654 } 655 getLocale()656 ULocale getLocale() { 657 return ULocale.ROOT; 658 } 659 get(String tableName, String code)660 String get(String tableName, String code) { 661 return get(tableName, null, code); 662 } 663 get(String tableName, String subTableName, String code)664 String get(String tableName, String subTableName, String code) { 665 return nullIfNotFound ? null : code; 666 } 667 } 668 669 static class ICUDataTable extends DataTable { 670 private final ICUResourceBundle bundle; 671 ICUDataTable(String path, ULocale locale, boolean nullIfNotFound)672 public ICUDataTable(String path, ULocale locale, boolean nullIfNotFound) { 673 super(nullIfNotFound); 674 this.bundle = (ICUResourceBundle) UResourceBundle.getBundleInstance( 675 path, locale.getBaseName()); 676 } 677 678 @Override getLocale()679 public ULocale getLocale() { 680 return bundle.getULocale(); 681 } 682 683 @Override get(String tableName, String subTableName, String code)684 public String get(String tableName, String subTableName, String code) { 685 return ICUResourceTableAccess.getTableString(bundle, tableName, subTableName, 686 code, nullIfNotFound ? null : code); 687 } 688 } 689 690 static abstract class DataTables { get(ULocale locale, boolean nullIfNotFound)691 public abstract DataTable get(ULocale locale, boolean nullIfNotFound); load(String className)692 public static DataTables load(String className) { 693 try { 694 return (DataTables) Class.forName(className).newInstance(); 695 } catch (Throwable t) { 696 return new DataTables() { 697 @Override 698 public DataTable get(ULocale locale, boolean nullIfNotFound) { 699 return new DataTable(nullIfNotFound); 700 } 701 }; 702 } 703 } 704 } 705 706 static abstract class ICUDataTables extends DataTables { 707 private final String path; 708 ICUDataTables(String path)709 protected ICUDataTables(String path) { 710 this.path = path; 711 } 712 713 @Override get(ULocale locale, boolean nullIfNotFound)714 public DataTable get(ULocale locale, boolean nullIfNotFound) { 715 return new ICUDataTable(path, locale, nullIfNotFound); 716 } 717 } 718 719 static class LangDataTables { 720 static final DataTables impl = DataTables.load("com.ibm.icu.impl.ICULangDataTables"); 721 } 722 723 static class RegionDataTables { 724 static final DataTables impl = DataTables.load("com.ibm.icu.impl.ICURegionDataTables"); 725 } 726 727 public static enum DataTableType { 728 LANG, REGION; 729 } 730 haveData(DataTableType type)731 public static boolean haveData(DataTableType type) { 732 switch (type) { 733 case LANG: return LangDataTables.impl instanceof ICUDataTables; 734 case REGION: return RegionDataTables.impl instanceof ICUDataTables; 735 default: 736 throw new IllegalArgumentException("unknown type: " + type); 737 } 738 } 739 appendWithSep(String s, StringBuilder b)740 private StringBuilder appendWithSep(String s, StringBuilder b) { 741 if (b.length() == 0) { 742 b.append(s); 743 } else { 744 SimpleFormatterImpl.formatAndReplace(separatorFormat, b, null, b, s); 745 } 746 return b; 747 } 748 749 private static class Cache { 750 private ULocale locale; 751 private DialectHandling dialectHandling; 752 private DisplayContext capitalization; 753 private DisplayContext nameLength; 754 private DisplayContext substituteHandling; 755 private LocaleDisplayNames cache; get(ULocale locale, DialectHandling dialectHandling)756 public LocaleDisplayNames get(ULocale locale, DialectHandling dialectHandling) { 757 if (!(dialectHandling == this.dialectHandling && DisplayContext.CAPITALIZATION_NONE == this.capitalization && 758 DisplayContext.LENGTH_FULL == this.nameLength && DisplayContext.SUBSTITUTE == this.substituteHandling && 759 locale.equals(this.locale))) { 760 this.locale = locale; 761 this.dialectHandling = dialectHandling; 762 this.capitalization = DisplayContext.CAPITALIZATION_NONE; 763 this.nameLength = DisplayContext.LENGTH_FULL; 764 this.substituteHandling = DisplayContext.SUBSTITUTE; 765 this.cache = new LocaleDisplayNamesImpl(locale, dialectHandling); 766 } 767 return cache; 768 } get(ULocale locale, DisplayContext... contexts)769 public LocaleDisplayNames get(ULocale locale, DisplayContext... contexts) { 770 DialectHandling dialectHandlingIn = DialectHandling.STANDARD_NAMES; 771 DisplayContext capitalizationIn = DisplayContext.CAPITALIZATION_NONE; 772 DisplayContext nameLengthIn = DisplayContext.LENGTH_FULL; 773 DisplayContext substituteHandling = DisplayContext.SUBSTITUTE; 774 for (DisplayContext contextItem : contexts) { 775 switch (contextItem.type()) { 776 case DIALECT_HANDLING: 777 dialectHandlingIn = (contextItem.value()==DisplayContext.STANDARD_NAMES.value())? 778 DialectHandling.STANDARD_NAMES: DialectHandling.DIALECT_NAMES; 779 break; 780 case CAPITALIZATION: 781 capitalizationIn = contextItem; 782 break; 783 case DISPLAY_LENGTH: 784 nameLengthIn = contextItem; 785 break; 786 case SUBSTITUTE_HANDLING: 787 substituteHandling = contextItem; 788 break; 789 default: 790 break; 791 } 792 } 793 if (!(dialectHandlingIn == this.dialectHandling && capitalizationIn == this.capitalization && 794 nameLengthIn == this.nameLength && substituteHandling == this.substituteHandling && 795 locale.equals(this.locale))) { 796 this.locale = locale; 797 this.dialectHandling = dialectHandlingIn; 798 this.capitalization = capitalizationIn; 799 this.nameLength = nameLengthIn; 800 this.substituteHandling = substituteHandling; 801 this.cache = new LocaleDisplayNamesImpl(locale, contexts); 802 } 803 return cache; 804 } 805 } 806 } 807