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