1 // Copyright (C) 2008-2012 IBM Corporation and Others. All Rights Reserved. 2 3 package org.unicode.cldr.util; 4 5 import com.google.common.cache.Cache; 6 import com.google.common.cache.CacheBuilder; 7 import com.ibm.icu.text.LocaleDisplayNames; 8 import com.ibm.icu.text.Transform; 9 import com.ibm.icu.util.ULocale; 10 import java.util.Iterator; 11 import java.util.Set; 12 import java.util.TreeSet; 13 import java.util.concurrent.Callable; 14 import java.util.concurrent.ConcurrentHashMap; 15 import java.util.concurrent.ExecutionException; 16 17 /** 18 * This class implements a CLDR UTS#35 compliant locale. It differs from ICU and Java locales in 19 * that it is singleton based, and that it is Comparable. It uses LocaleIDParser to do the heavy 20 * lifting of parsing. 21 * 22 * @author srl 23 * @see LocaleIDParser 24 * @see ULocale 25 */ 26 public final class CLDRLocale implements Comparable<CLDRLocale> { 27 private static final boolean DEBUG = false; 28 29 public interface NameFormatter { getDisplayName(CLDRLocale cldrLocale)30 String getDisplayName(CLDRLocale cldrLocale); 31 getDisplayName( CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker)32 String getDisplayName( 33 CLDRLocale cldrLocale, 34 boolean onlyConstructCompound, 35 Transform<String, String> altPicker); 36 getDisplayLanguage(CLDRLocale cldrLocale)37 String getDisplayLanguage(CLDRLocale cldrLocale); 38 getDisplayScript(CLDRLocale cldrLocale)39 String getDisplayScript(CLDRLocale cldrLocale); 40 getDisplayVariant(CLDRLocale cldrLocale)41 String getDisplayVariant(CLDRLocale cldrLocale); 42 getDisplayCountry(CLDRLocale cldrLocale)43 String getDisplayCountry(CLDRLocale cldrLocale); 44 } 45 46 public static class SimpleFormatter implements NameFormatter { 47 private LocaleDisplayNames ldn; 48 SimpleFormatter(ULocale displayLocale)49 public SimpleFormatter(ULocale displayLocale) { 50 this.ldn = LocaleDisplayNames.getInstance(displayLocale); 51 } 52 getDisplayNames()53 public LocaleDisplayNames getDisplayNames() { 54 return ldn; 55 } 56 setDisplayNames(LocaleDisplayNames ldn)57 public LocaleDisplayNames setDisplayNames(LocaleDisplayNames ldn) { 58 return this.ldn = ldn; 59 } 60 61 @Override getDisplayVariant(CLDRLocale cldrLocale)62 public String getDisplayVariant(CLDRLocale cldrLocale) { 63 return ldn.variantDisplayName(cldrLocale.getVariant()); 64 } 65 66 @Override getDisplayCountry(CLDRLocale cldrLocale)67 public String getDisplayCountry(CLDRLocale cldrLocale) { 68 return ldn.regionDisplayName(cldrLocale.getCountry()); 69 } 70 71 @Override getDisplayName(CLDRLocale cldrLocale)72 public String getDisplayName(CLDRLocale cldrLocale) { 73 StringBuffer sb = new StringBuffer(); 74 String l = cldrLocale.getLanguage(); 75 String s = cldrLocale.getScript(); 76 String r = cldrLocale.getCountry(); 77 String v = cldrLocale.getVariant(); 78 79 if (l != null && !l.isEmpty()) { 80 sb.append(getDisplayLanguage(cldrLocale)); 81 } else { 82 sb.append("?"); 83 } 84 if ((s != null && !s.isEmpty()) 85 || (r != null && !r.isEmpty()) 86 || (v != null && !v.isEmpty())) { 87 sb.append(" ("); 88 if (s != null && !s.isEmpty()) { 89 sb.append(getDisplayScript(cldrLocale)).append(","); 90 } 91 if (r != null && !r.isEmpty()) { 92 sb.append(getDisplayCountry(cldrLocale)).append(","); 93 } 94 if (v != null && !v.isEmpty()) { 95 sb.append(getDisplayVariant(cldrLocale)).append(","); 96 } 97 sb.replace(sb.length() - 1, sb.length(), ")"); 98 } 99 return sb.toString(); 100 } 101 102 @Override getDisplayScript(CLDRLocale cldrLocale)103 public String getDisplayScript(CLDRLocale cldrLocale) { 104 return ldn.scriptDisplayName(cldrLocale.getScript()); 105 } 106 107 @Override getDisplayLanguage(CLDRLocale cldrLocale)108 public String getDisplayLanguage(CLDRLocale cldrLocale) { 109 return ldn.languageDisplayName(cldrLocale.getLanguage()); 110 } 111 112 @SuppressWarnings("unused") 113 @Override getDisplayName( CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker)114 public String getDisplayName( 115 CLDRLocale cldrLocale, 116 boolean onlyConstructCompound, 117 Transform<String, String> altPicker) { 118 return getDisplayName(cldrLocale); 119 } 120 } 121 122 /** 123 * @author srl 124 * <p>This formatter will delegate to CLDRFile.getName if a CLDRFile is given, otherwise 125 * StandardCodes 126 */ 127 public static class CLDRFormatter extends SimpleFormatter { 128 private FormatBehavior behavior = FormatBehavior.extend; 129 130 private CLDRFile file = null; 131 CLDRFormatter(CLDRFile fromFile)132 public CLDRFormatter(CLDRFile fromFile) { 133 super(CLDRLocale.getInstance(fromFile.getLocaleID()).toULocale()); 134 file = fromFile; 135 } 136 CLDRFormatter(CLDRFile fromFile, FormatBehavior behavior)137 public CLDRFormatter(CLDRFile fromFile, FormatBehavior behavior) { 138 super(CLDRLocale.getInstance(fromFile.getLocaleID()).toULocale()); 139 this.behavior = behavior; 140 file = fromFile; 141 } 142 CLDRFormatter()143 public CLDRFormatter() { 144 super(ULocale.ROOT); 145 } 146 CLDRFormatter(FormatBehavior behavior)147 public CLDRFormatter(FormatBehavior behavior) { 148 super(ULocale.ROOT); 149 this.behavior = behavior; 150 } 151 152 @Override getDisplayVariant(CLDRLocale cldrLocale)153 public String getDisplayVariant(CLDRLocale cldrLocale) { 154 if (file != null) return file.getName("variant", cldrLocale.getVariant()); 155 return tryForBetter(super.getDisplayVariant(cldrLocale), cldrLocale.getVariant()); 156 } 157 158 @Override getDisplayName(CLDRLocale cldrLocale)159 public String getDisplayName(CLDRLocale cldrLocale) { 160 if (file != null) return file.getName(cldrLocale.toDisplayLanguageTag(), true, null); 161 return super.getDisplayName(cldrLocale); 162 } 163 164 @Override getDisplayName( CLDRLocale cldrLocale, boolean onlyConstructCompound, Transform<String, String> altPicker)165 public String getDisplayName( 166 CLDRLocale cldrLocale, 167 boolean onlyConstructCompound, 168 Transform<String, String> altPicker) { 169 if (file != null) 170 return file.getName( 171 cldrLocale.toDisplayLanguageTag(), onlyConstructCompound, altPicker); 172 return super.getDisplayName(cldrLocale); 173 } 174 175 @Override getDisplayScript(CLDRLocale cldrLocale)176 public String getDisplayScript(CLDRLocale cldrLocale) { 177 if (file != null) return file.getName("script", cldrLocale.getScript()); 178 return tryForBetter(super.getDisplayScript(cldrLocale), cldrLocale.getScript()); 179 } 180 181 @Override getDisplayLanguage(CLDRLocale cldrLocale)182 public String getDisplayLanguage(CLDRLocale cldrLocale) { 183 if (file != null) return file.getName("language", cldrLocale.getLanguage()); 184 return tryForBetter(super.getDisplayLanguage(cldrLocale), cldrLocale.getLanguage()); 185 } 186 187 @Override getDisplayCountry(CLDRLocale cldrLocale)188 public String getDisplayCountry(CLDRLocale cldrLocale) { 189 if (file != null) return file.getName("territory", cldrLocale.getCountry()); 190 return tryForBetter(super.getDisplayLanguage(cldrLocale), cldrLocale.getLanguage()); 191 } 192 tryForBetter(String superString, String code)193 private String tryForBetter(String superString, String code) { 194 if (superString.equals(code)) { 195 String fromLst = StandardCodes.make().getData("language", code); 196 if (fromLst != null && !fromLst.equals(code)) { 197 switch (behavior) { 198 case replace: 199 return fromLst; 200 case extend: 201 return superString + " [" + fromLst + "]"; 202 case extendHtml: 203 return superString + " [<i>" + fromLst + "</i>]"; 204 } 205 } 206 } 207 return superString; 208 } 209 } 210 211 public enum FormatBehavior { 212 replace, 213 extend, 214 extendHtml 215 } 216 217 /** The parent locale id string, or null if no parent */ 218 private String parentId; 219 220 /** 221 * Reference to the parent CLDRLocale. 222 * 223 * <p>It is volatile, and accessed directly only by getParent, since it uses the double-check 224 * idiom for lazy initialization. 225 */ 226 private volatile CLDRLocale parentLocale; 227 228 /** Cached ICU format locale */ 229 private ULocale ulocale; 230 /** base name, 'without parameters'. Currently same as fullname. */ 231 private String basename; 232 /** Full name */ 233 private String fullname; 234 /** The LocaleIDParser interprets the various parts (language, country, script, etc). */ 235 private LocaleIDParser parts = null; 236 237 /** 238 * Returns the BCP47 language tag for all except root. For root, returns "root" = 239 * LocaleNames.ROOT. 240 * 241 * @return 242 */ toDisplayLanguageTag()243 private String toDisplayLanguageTag() { 244 if (getBaseName().equals(LocaleNames.ROOT)) { 245 return LocaleNames.ROOT; 246 } else { 247 return toLanguageTag(); 248 } 249 } 250 251 /** 252 * Return BCP47 language tag 253 * 254 * @return 255 */ toLanguageTag()256 public String toLanguageTag() { 257 return ulocale.toLanguageTag(); 258 } 259 260 /** 261 * Return BCP47 languageTag, using special rules for root 262 * 263 * @param locale 264 * @return 265 */ toLanguageTag(final String locale)266 public static String toLanguageTag(final String locale) { 267 return getInstance(locale).toLanguageTag(); 268 } 269 270 /** 271 * Construct a CLDRLocale from a string with the full locale ID. Internal, called by the factory 272 * function. 273 * 274 * @param str the string representing a locale. 275 * <p>If str is empty, it's equal to ULocale.ROOT.getBaseName(), and we are initializing a 276 * CLDRLocale for root. 277 */ CLDRLocale(String str)278 private CLDRLocale(String str) { 279 str = process(str); 280 if (rootMatches(str)) { 281 fullname = LocaleNames.ROOT; 282 parentId = null; 283 } else { 284 parts = new LocaleIDParser(); 285 parts.set(str); 286 fullname = parts.toString(); 287 parentId = 288 LocaleIDParser.getParent( 289 str); // Note, this does now handle explicit parentLocales 290 if (DEBUG) System.out.println(str + " par = " + parentId); 291 } 292 basename = fullname; 293 if (ulocale == null) { 294 ulocale = new ULocale(fullname); 295 } 296 } 297 298 /** Return the full locale name, in CLDR format. */ 299 @Override toString()300 public String toString() { 301 return fullname; 302 } 303 304 /** 305 * Return the base locale name, in CLDR format, without any @keywords 306 * 307 * @return 308 */ getBaseName()309 public String getBaseName() { 310 return basename; 311 } 312 313 /** 314 * internal: process a string from ICU to CLDR form. For now, just collapse double underscores. 315 * 316 * @param baseName 317 * @return 318 * @internal 319 */ process(String baseName)320 private String process(String baseName) { 321 return baseName.replaceAll("__", "_"); 322 } 323 324 /** Compare to another CLDRLocale. Uses string order of toString(). */ 325 @Override compareTo(CLDRLocale o)326 public int compareTo(CLDRLocale o) { 327 if (o == this) return 0; 328 return fullname.compareTo(o.fullname); 329 } 330 331 /** Hashcode - is the hashcode of the full string */ 332 @Override hashCode()333 public int hashCode() { 334 return fullname.hashCode(); 335 } 336 337 /** 338 * Convert to an ICU compatible ULocale. 339 * 340 * @return 341 */ toULocale()342 public ULocale toULocale() { 343 return ulocale; 344 } 345 346 /** 347 * Allocate a CLDRLocale (could be a singleton). If null is passed in, null will be returned. 348 * 349 * @param s 350 * @return 351 */ getInstance(String s)352 public static CLDRLocale getInstance(String s) { 353 if (s == null) { 354 return null; 355 } 356 /* 357 * Normalize variations of LocaleNames.ROOT before checking stringToLoc. 358 */ 359 if (rootMatches(s)) { 360 s = LocaleNames.ROOT; 361 } 362 return stringToLoc.computeIfAbsent(s, k -> new CLDRLocale(k)); 363 } 364 365 /** 366 * Does the given string match the root locale? Treat empty string as matching, for 367 * compatibility with ULocale.ROOT (which is NOT the same as CLDRLocale.ROOT). Also, ignore 368 * case, so "RooT" matches. 369 * 370 * @param s the string 371 * @return true if the string matches LocaleNames.ROOT, else false 372 */ rootMatches(String s)373 private static boolean rootMatches(String s) { 374 /* 375 * Important: 376 * ULocale.ROOT.getBaseName() is "", the empty string, not LocaleNames.ROOT = "root". 377 * CLDRLocale.ROOT.getBaseName() is LocaleNames.ROOT. 378 */ 379 return s.equals(ULocale.ROOT.getBaseName()) || s.equalsIgnoreCase(LocaleNames.ROOT); 380 } 381 382 /** 383 * Public factory function. Allocate a CLDRLocale (could be a singleton). If null is passed in, 384 * null will be returned. 385 * 386 * @param u the ULocale 387 * @return the CLDRLocale 388 */ getInstance(ULocale u)389 public static CLDRLocale getInstance(ULocale u) { 390 if (u == null) { 391 return null; 392 } 393 return getInstance(u.getBaseName()); 394 } 395 396 private static ConcurrentHashMap<String, CLDRLocale> stringToLoc = new ConcurrentHashMap<>(); 397 398 /** 399 * Return the parent locale of this item, using component=main. Null if no parent (root has no 400 * parent) 401 * 402 * @return the parent locale, or null 403 * <p>Use lazy initialization for parentLocale, since getInstance calling itself recursively 404 * for the parent could cause ConcurrentHashMap to hang within computeIfAbsent. 405 * <p>Use the "double-check idiom with a volatile field" for high-performance thread-safe 406 * lazy initialization: 407 * https://www.oracle.com/technical-resources/articles/javase/bloch-effective-08-qa.html 408 * <p>For further efficiency, return null immediately if parentId is null. 409 */ getParent()410 public CLDRLocale getParent() { 411 if (parentId == null) { 412 return null; 413 } 414 CLDRLocale result = parentLocale; 415 if (result == null) { 416 synchronized (this) { 417 result = parentLocale; 418 if (result == null) { 419 parentLocale = result = CLDRLocale.getInstance(parentId); 420 } 421 } 422 } 423 return result; 424 } 425 426 /** 427 * Returns true if other is equal to or is an ancestor of this, using component=main, false 428 * otherwise 429 */ childOf(CLDRLocale other)430 public boolean childOf(CLDRLocale other) { 431 if (other == null) return false; 432 if (other == this) return true; 433 CLDRLocale parent = getParent(); 434 if (parent == null) return false; // end 435 return parent.childOf(other); 436 } 437 438 /** 439 * Return an iterator that will iterate over locale, parent, parent etc, using component=main, 440 * finally reaching root. 441 * 442 * @return 443 */ getParentIterator()444 public Iterable<CLDRLocale> getParentIterator() { 445 final CLDRLocale newThis = this; 446 return new Iterable<>() { 447 @Override 448 public Iterator<CLDRLocale> iterator() { 449 return new Iterator<>() { 450 CLDRLocale what = newThis; 451 452 @Override 453 public boolean hasNext() { 454 return what.getParent() != null; 455 } 456 457 @Override 458 public CLDRLocale next() { 459 CLDRLocale curr = what; 460 if (what != null) { 461 what = what.getParent(); 462 } 463 return curr; 464 } 465 466 @Override 467 public void remove() { 468 throw new InternalError("unmodifiable iterator"); 469 } 470 }; 471 } 472 }; 473 } 474 475 /** 476 * Get the 'language' locale, as an object. Might be 'this'. 477 * 478 * @return 479 */ 480 public CLDRLocale getLanguageLocale() { 481 return getInstance(getLanguage()); 482 } 483 484 public String getLanguage() { 485 return parts == null ? fullname : parts.getLanguage(); 486 } 487 488 public String getScript() { 489 return parts == null ? null : parts.getScript(); 490 } 491 492 public boolean isLanguageLocale() { 493 return this.equals(getLanguageLocale()); 494 } 495 496 /** 497 * Return the region 498 * 499 * @return 500 */ 501 public String getCountry() { 502 return parts == null ? null : parts.getRegion(); 503 } 504 505 /** 506 * Return "the" variant. 507 * 508 * @return 509 */ 510 public String getVariant() { 511 return toULocale().getVariant(); // TODO: replace with parts? 512 } 513 514 /** Most objects should be singletons, and so equality/inequality comparison is done first. */ 515 @Override 516 public boolean equals(Object o) { 517 if (o == this) return true; 518 if (!(o instanceof CLDRLocale)) return false; 519 return (0 == compareTo((CLDRLocale) o)); 520 } 521 522 /** The root locale, a singleton. */ 523 public static final CLDRLocale ROOT = getInstance(ULocale.ROOT); 524 525 public String getDisplayName() { 526 return getDisplayName(getDefaultFormatter()); 527 } 528 529 public String getDisplayRegion() { 530 return getDisplayCountry(getDefaultFormatter()); 531 } 532 533 public String getDisplayVariant() { 534 return getDisplayVariant(getDefaultFormatter()); 535 } 536 537 public String getDisplayName(boolean combined, Transform<String, String> picker) { 538 return getDisplayName(getDefaultFormatter(), combined, picker); 539 } 540 541 /** 542 * These functions wrap calls to the displayLocale, but are provided to supply an interface that 543 * looks similar to ULocale.getDisplay___(displayLocale) 544 * 545 * @param displayLocale 546 * @return 547 */ 548 public String getDisplayName(NameFormatter displayLocale) { 549 if (displayLocale == null) displayLocale = getDefaultFormatter(); 550 return displayLocale.getDisplayName(this); 551 } 552 553 // private static LruMap<ULocale, NameFormatter> defaultFormatters = new LruMap<ULocale, 554 // NameFormatter>(1); 555 private static Cache<ULocale, NameFormatter> defaultFormatters = 556 CacheBuilder.newBuilder().initialCapacity(1).build(); 557 private static NameFormatter gDefaultFormatter = getSimpleFormatterFor(ULocale.getDefault()); 558 559 public static NameFormatter getSimpleFormatterFor(ULocale loc) { 560 // NameFormatter nf = defaultFormatters.get(loc); 561 // if (nf == null) { 562 // nf = new SimpleFormatter(loc); 563 // defaultFormatters.put(loc, nf); 564 // } 565 // return nf; 566 // return defaultFormatters.getIfPresent(loc); 567 final ULocale uLocFinal = loc; 568 try { 569 return defaultFormatters.get( 570 loc, 571 new Callable<NameFormatter>() { 572 573 @Override 574 public NameFormatter call() throws Exception { 575 return new SimpleFormatter(uLocFinal); 576 } 577 }); 578 } catch (ExecutionException e) { 579 e.printStackTrace(); 580 return null; 581 } 582 } 583 584 public String getDisplayName(ULocale displayLocale) { 585 return getSimpleFormatterFor(displayLocale).getDisplayName(this); 586 } 587 588 public static NameFormatter getDefaultFormatter() { 589 return gDefaultFormatter; 590 } 591 592 public static NameFormatter setDefaultFormatter(NameFormatter nf) { 593 return gDefaultFormatter = nf; 594 } 595 596 /** 597 * These functions wrap calls to the displayLocale, but are provided to supply an interface that 598 * looks similar to ULocale.getDisplay___(displayLocale) 599 * 600 * @param displayLocale 601 * @return 602 */ 603 public String getDisplayCountry(NameFormatter displayLocale) { 604 if (displayLocale == null) displayLocale = getDefaultFormatter(); 605 return displayLocale.getDisplayCountry(this); 606 } 607 608 /** 609 * These functions wrap calls to the displayLocale, but are provided to supply an interface that 610 * looks similar to ULocale.getDisplay___(displayLocale) 611 * 612 * @param displayLocale 613 * @return 614 */ 615 public String getDisplayVariant(NameFormatter displayLocale) { 616 if (displayLocale == null) displayLocale = getDefaultFormatter(); 617 return displayLocale.getDisplayVariant(this); 618 } 619 620 /** 621 * Construct an instance from an array 622 * 623 * @param available 624 * @return 625 */ 626 public static Set<CLDRLocale> getInstance(Iterable<String> available) { 627 Set<CLDRLocale> s = new TreeSet<>(); 628 for (String str : available) { 629 s.add(CLDRLocale.getInstance(str)); 630 } 631 return s; 632 } 633 634 public interface SublocaleProvider { 635 public Set<CLDRLocale> subLocalesOf(CLDRLocale forLocale); 636 } 637 638 public String getDisplayName( 639 NameFormatter engFormat, boolean combined, Transform<String, String> picker) { 640 return engFormat.getDisplayName(this, combined, picker); 641 } 642 643 /** 644 * Return the highest parent that is a child of root, or null. 645 * 646 * @return highest parent, or null. ROOT.getHighestNonrootParent() also returns null. 647 */ 648 public CLDRLocale getHighestNonrootParent() { 649 CLDRLocale res; 650 if (this == ROOT) { 651 res = null; 652 } else { 653 CLDRLocale parent = getParent(); 654 if (parent == ROOT || parent == null) { 655 res = this; 656 } else { 657 res = parent.getHighestNonrootParent(); 658 } 659 } 660 if (DEBUG) System.out.println(this + ".HNRP=" + res); 661 return res; 662 } 663 664 public boolean isParentRoot() { 665 return CLDRLocale.ROOT == getParent(); 666 } 667 668 public int getRank() { 669 if (this == CLDRLocale.ROOT) { 670 return 0; 671 } else { 672 return 1 + getParent().getRank(); 673 } 674 } 675 } 676