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