• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.app;
18 
19 import android.annotation.IntRange;
20 import android.annotation.UnsupportedAppUsage;
21 import android.icu.text.ListFormatter;
22 import android.icu.util.ULocale;
23 import android.os.LocaleList;
24 import android.text.TextUtils;
25 
26 import java.text.Collator;
27 import java.util.Comparator;
28 import java.util.Locale;
29 
30 /**
31  * This class implements some handy methods to process with locales.
32  */
33 public class LocaleHelper {
34 
35     /**
36      * Sentence-case (first character uppercased).
37      *
38      * <p>There is no good API available for this, not even in ICU.
39      * We can revisit this if we get some ICU support later.</p>
40      *
41      * <p>There are currently several tickets requesting this feature:</p>
42      * <ul>
43      * <li>ICU needs to provide an easy way to titlecase only one first letter
44      *   http://bugs.icu-project.org/trac/ticket/11729</li>
45      * <li>Add "initial case"
46      *    http://bugs.icu-project.org/trac/ticket/8394</li>
47      * <li>Add code for initialCase, toTitlecase don't modify after Lt,
48      *   avoid 49Ers, low-level language-specific casing
49      *   http://bugs.icu-project.org/trac/ticket/10410</li>
50      * <li>BreakIterator.getFirstInstance: Often you need to titlecase just the first
51      *   word, and leave the rest of the string alone.  (closed as duplicate)
52      *   http://bugs.icu-project.org/trac/ticket/8946</li>
53      * </ul>
54      *
55      * <p>A (clunky) option with the current ICU API is:</p>
56      * {{
57      *   BreakIterator breakIterator = BreakIterator.getSentenceInstance(locale);
58      *   String result = UCharacter.toTitleCase(locale,
59      *       source, breakIterator, UCharacter.TITLECASE_NO_LOWERCASE);
60      * }}
61      *
62      * <p>That also means creating a BreakIterator for each locale. Expensive...</p>
63      *
64      * @param str the string to sentence-case.
65      * @param locale the locale used for the case conversion.
66      * @return the string converted to sentence-case.
67      */
toSentenceCase(String str, Locale locale)68     public static String toSentenceCase(String str, Locale locale) {
69         if (str.isEmpty()) {
70             return str;
71         }
72         final int firstCodePointLen = str.offsetByCodePoints(0, 1);
73         return str.substring(0, firstCodePointLen).toUpperCase(locale)
74                 + str.substring(firstCodePointLen);
75     }
76 
77     /**
78      * Normalizes a string for locale name search. Does case conversion for now,
79      * but might do more in the future.
80      *
81      * <p>Warning: it is only intended to be used in searches by the locale picker.
82      * Don't use it for other things, it is very limited.</p>
83      *
84      * @param str the string to normalize
85      * @param locale the locale that might be used for certain operations (i.e. case conversion)
86      * @return the string normalized for search
87      */
88     @UnsupportedAppUsage
normalizeForSearch(String str, Locale locale)89     public static String normalizeForSearch(String str, Locale locale) {
90         // TODO: tbd if it needs to be smarter (real normalization, remove accents, etc.)
91         // If needed we might use case folding and ICU/CLDR's collation-based loose searching.
92         // TODO: decide what should the locale be, the default locale, or the locale of the string.
93         // Uppercase is better than lowercase because of things like sharp S, Greek sigma, ...
94         return str.toUpperCase();
95     }
96 
97     // For some locales we want to use a "dialect" form, for instance
98     // "Dari" instead of "Persian (Afghanistan)", or "Moldavian" instead of "Romanian (Moldova)"
shouldUseDialectName(Locale locale)99     private static boolean shouldUseDialectName(Locale locale) {
100         final String lang = locale.getLanguage();
101         return "fa".equals(lang) // Persian
102                 || "ro".equals(lang) // Romanian
103                 || "zh".equals(lang); // Chinese
104     }
105 
106     /**
107      * Returns the locale localized for display in the provided locale.
108      *
109      * @param locale the locale whose name is to be displayed.
110      * @param displayLocale the locale in which to display the name.
111      * @param sentenceCase true if the result should be sentence-cased
112      * @return the localized name of the locale.
113      */
114     @UnsupportedAppUsage
getDisplayName(Locale locale, Locale displayLocale, boolean sentenceCase)115     public static String getDisplayName(Locale locale, Locale displayLocale, boolean sentenceCase) {
116         final ULocale displayULocale = ULocale.forLocale(displayLocale);
117         String result = shouldUseDialectName(locale)
118                 ? ULocale.getDisplayNameWithDialect(locale.toLanguageTag(), displayULocale)
119                 : ULocale.getDisplayName(locale.toLanguageTag(), displayULocale);
120         return sentenceCase ? toSentenceCase(result, displayLocale) : result;
121     }
122 
123     /**
124      * Returns the locale localized for display in the default locale.
125      *
126      * @param locale the locale whose name is to be displayed.
127      * @param sentenceCase true if the result should be sentence-cased
128      * @return the localized name of the locale.
129      */
getDisplayName(Locale locale, boolean sentenceCase)130     public static String getDisplayName(Locale locale, boolean sentenceCase) {
131         return getDisplayName(locale, Locale.getDefault(), sentenceCase);
132     }
133 
134     /**
135      * Returns a locale's country localized for display in the provided locale.
136      *
137      * @param locale the locale whose country will be displayed.
138      * @param displayLocale the locale in which to display the name.
139      * @return the localized country name.
140      */
141     @UnsupportedAppUsage
getDisplayCountry(Locale locale, Locale displayLocale)142     public static String getDisplayCountry(Locale locale, Locale displayLocale) {
143         final String languageTag = locale.toLanguageTag();
144         final ULocale uDisplayLocale = ULocale.forLocale(displayLocale);
145         final String country = ULocale.getDisplayCountry(languageTag, uDisplayLocale);
146         final String numberingSystem = locale.getUnicodeLocaleType("nu");
147         if (numberingSystem != null) {
148             return String.format("%s (%s)", country,
149                     ULocale.getDisplayKeywordValue(languageTag, "numbers", uDisplayLocale));
150         } else {
151             return country;
152         }
153     }
154 
155     /**
156      * Returns a locale's country localized for display in the default locale.
157      *
158      * @param locale the locale whose country will be displayed.
159      * @return the localized country name.
160      */
getDisplayCountry(Locale locale)161     public static String getDisplayCountry(Locale locale) {
162         return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.getDefault());
163     }
164 
165     /**
166      * Returns the locale list localized for display in the provided locale.
167      *
168      * @param locales the list of locales whose names is to be displayed.
169      * @param displayLocale the locale in which to display the names.
170      *                      If this is null, it will use the default locale.
171      * @param maxLocales maximum number of locales to display. Generates ellipsis after that.
172      * @return the locale aware list of locale names
173      */
getDisplayLocaleList( LocaleList locales, Locale displayLocale, @IntRange(from=1) int maxLocales)174     public static String getDisplayLocaleList(
175             LocaleList locales, Locale displayLocale, @IntRange(from=1) int maxLocales) {
176 
177         final Locale dispLocale = displayLocale == null ? Locale.getDefault() : displayLocale;
178 
179         final boolean ellipsisNeeded = locales.size() > maxLocales;
180         final int localeCount, listCount;
181         if (ellipsisNeeded) {
182             localeCount = maxLocales;
183             listCount = maxLocales + 1;  // One extra slot for the ellipsis
184         } else {
185             listCount = localeCount = locales.size();
186         }
187         final String[] localeNames = new String[listCount];
188         for (int i = 0; i < localeCount; i++) {
189             localeNames[i] = LocaleHelper.getDisplayName(locales.get(i), dispLocale, false);
190         }
191         if (ellipsisNeeded) {
192             // Theoretically, we want to extract this from ICU's Resource Bundle for
193             // "Ellipsis/final", which seems to have different strings than the normal ellipsis for
194             // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two
195             // problems: it's expensive to extract it, and in case the output string becomes
196             // automatically ellipsized, it can result in weird output.
197             localeNames[maxLocales] = TextUtils.getEllipsisString(TextUtils.TruncateAt.END);
198         }
199 
200         ListFormatter lfn = ListFormatter.getInstance(dispLocale);
201         return lfn.format((Object[]) localeNames);
202     }
203 
204     /**
205      * Adds the likely subtags for a provided locale ID.
206      *
207      * @param locale the locale to maximize.
208      * @return the maximized Locale instance.
209      */
addLikelySubtags(Locale locale)210     public static Locale addLikelySubtags(Locale locale) {
211         return libcore.icu.ICU.addLikelySubtags(locale);
212     }
213 
214     /**
215      * Locale-sensitive comparison for LocaleInfo.
216      *
217      * <p>It uses the label, leaving the decision on what to put there to the LocaleInfo.
218      * For instance fr-CA can be shown as "français" as a generic label in the language selection,
219      * or "français (Canada)" if it is a suggestion, or "Canada" in the country selection.</p>
220      *
221      * <p>Gives priority to suggested locales (to sort them at the top).</p>
222      */
223     public static final class LocaleInfoComparator implements Comparator<LocaleStore.LocaleInfo> {
224         private final Collator mCollator;
225         private final boolean mCountryMode;
226         private static final String PREFIX_ARABIC = "\u0627\u0644"; // ALEF-LAM, ال
227 
228         /**
229          * Constructor.
230          *
231          * @param sortLocale the locale to be used for sorting.
232          */
233         @UnsupportedAppUsage
LocaleInfoComparator(Locale sortLocale, boolean countryMode)234         public LocaleInfoComparator(Locale sortLocale, boolean countryMode) {
235             mCollator = Collator.getInstance(sortLocale);
236             mCountryMode = countryMode;
237         }
238 
239         /*
240          * The Arabic collation should ignore Alef-Lam at the beginning (b/26277596)
241          *
242          * We look at the label's locale, not the current system locale.
243          * This is because the name of the Arabic language itself is in Arabic,
244          * and starts with Alef-Lam, no matter what the system locale is.
245          */
removePrefixForCompare(Locale locale, String str)246         private String removePrefixForCompare(Locale locale, String str) {
247             if ("ar".equals(locale.getLanguage()) && str.startsWith(PREFIX_ARABIC)) {
248                 return str.substring(PREFIX_ARABIC.length());
249             }
250             return str;
251         }
252 
253         /**
254          * Compares its two arguments for order.
255          *
256          * @param lhs   the first object to be compared
257          * @param rhs   the second object to be compared
258          * @return  a negative integer, zero, or a positive integer as the first
259          *          argument is less than, equal to, or greater than the second.
260          */
261         @UnsupportedAppUsage
262         @Override
compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs)263         public int compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs) {
264             // We don't care about the various suggestion types, just "suggested" (!= 0)
265             // and "all others" (== 0)
266             if (lhs.isSuggested() == rhs.isSuggested()) {
267                 // They are in the same "bucket" (suggested / others), so we compare the text
268                 return mCollator.compare(
269                         removePrefixForCompare(lhs.getLocale(), lhs.getLabel(mCountryMode)),
270                         removePrefixForCompare(rhs.getLocale(), rhs.getLabel(mCountryMode)));
271             } else {
272                 // One locale is suggested and one is not, so we put them in different "buckets"
273                 return lhs.isSuggested() ? -1 : 1;
274             }
275         }
276     }
277 }
278