xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/CLDRLocale.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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