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         return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.forLocale(displayLocale));
    140     }
    141 
    142     /**
    143      * Returns a locale's country localized for display in the default locale.
    144      *
    145      * @param locale the locale whose country will be displayed.
    146      * @return the localized country name.
    147      */
    148     public static String getDisplayCountry(Locale locale) {
    149         return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.getDefault());
    150     }
    151 
    152     /**
    153      * Returns the locale list localized for display in the provided locale.
    154      *
    155      * @param locales the list of locales whose names is to be displayed.
    156      * @param displayLocale the locale in which to display the names.
    157      *                      If this is null, it will use the default locale.
    158      * @param maxLocales maximum number of locales to display. Generates ellipsis after that.
    159      * @return the locale aware list of locale names
    160      */
    161     public static String getDisplayLocaleList(
    162             LocaleList locales, Locale displayLocale, @IntRange(from=1) int maxLocales) {
    163 
    164         final Locale dispLocale = displayLocale == null ? Locale.getDefault() : displayLocale;
    165 
    166         final boolean ellipsisNeeded = locales.size() > maxLocales;
    167         final int localeCount, listCount;
    168         if (ellipsisNeeded) {
    169             localeCount = maxLocales;
    170             listCount = maxLocales + 1;  // One extra slot for the ellipsis
    171         } else {
    172             listCount = localeCount = locales.size();
    173         }
    174         final String[] localeNames = new String[listCount];
    175         for (int i = 0; i < localeCount; i++) {
    176             localeNames[i] = LocaleHelper.getDisplayName(locales.get(i), dispLocale, false);
    177         }
    178         if (ellipsisNeeded) {
    179             // Theoretically, we want to extract this from ICU's Resource Bundle for
    180             // "Ellipsis/final", which seems to have different strings than the normal ellipsis for
    181             // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two
    182             // problems: it's expensive to extract it, and in case the output string becomes
    183             // automatically ellipsized, it can result in weird output.
    184             localeNames[maxLocales] = TextUtils.ELLIPSIS_STRING;
    185         }
    186 
    187         ListFormatter lfn = ListFormatter.getInstance(dispLocale);
    188         return lfn.format((Object[]) localeNames);
    189     }
    190 
    191     /**
    192      * Adds the likely subtags for a provided locale ID.
    193      *
    194      * @param locale the locale to maximize.
    195      * @return the maximized Locale instance.
    196      */
    197     public static Locale addLikelySubtags(Locale locale) {
    198         return libcore.icu.ICU.addLikelySubtags(locale);
    199     }
    200 
    201     /**
    202      * Locale-sensitive comparison for LocaleInfo.
    203      *
    204      * <p>It uses the label, leaving the decision on what to put there to the LocaleInfo.
    205      * For instance fr-CA can be shown as "franais" as a generic label in the language selection,
    206      * or "franais (Canada)" if it is a suggestion, or "Canada" in the country selection.</p>
    207      *
    208      * <p>Gives priority to suggested locales (to sort them at the top).</p>
    209      */
    210     public static final class LocaleInfoComparator implements Comparator<LocaleStore.LocaleInfo> {
    211         private final Collator mCollator;
    212         private final boolean mCountryMode;
    213         private static final String PREFIX_ARABIC = "\u0627\u0644"; // ALEF-LAM, 
    214 
    215         /**
    216          * Constructor.
    217          *
    218          * @param sortLocale the locale to be used for sorting.
    219          */
    220         public LocaleInfoComparator(Locale sortLocale, boolean countryMode) {
    221             mCollator = Collator.getInstance(sortLocale);
    222             mCountryMode = countryMode;
    223         }
    224 
    225         /*
    226          * The Arabic collation should ignore Alef-Lam at the beginning (b/26277596)
    227          *
    228          * We look at the label's locale, not the current system locale.
    229          * This is because the name of the Arabic language itself is in Arabic,
    230          * and starts with Alef-Lam, no matter what the system locale is.
    231          */
    232         private String removePrefixForCompare(Locale locale, String str) {
    233             if ("ar".equals(locale.getLanguage()) && str.startsWith(PREFIX_ARABIC)) {
    234                 return str.substring(PREFIX_ARABIC.length());
    235             }
    236             return str;
    237         }
    238 
    239         /**
    240          * Compares its two arguments for order.
    241          *
    242          * @param lhs   the first object to be compared
    243          * @param rhs   the second object to be compared
    244          * @return  a negative integer, zero, or a positive integer as the first
    245          *          argument is less than, equal to, or greater than the second.
    246          */
    247         @Override
    248         public int compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs) {
    249             // We don't care about the various suggestion types, just "suggested" (!= 0)
    250             // and "all others" (== 0)
    251             if (lhs.isSuggested() == rhs.isSuggested()) {
    252                 // They are in the same "bucket" (suggested / others), so we compare the text
    253                 return mCollator.compare(
    254                         removePrefixForCompare(lhs.getLocale(), lhs.getLabel(mCountryMode)),
    255                         removePrefixForCompare(rhs.getLocale(), rhs.getLabel(mCountryMode)));
    256             } else {
    257                 // One locale is suggested and one is not, so we put them in different "buckets"
    258                 return lhs.isSuggested() ? -1 : 1;
    259             }
    260         }
    261     }
    262 }
    263