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.compat.annotation.UnsupportedAppUsage; 21 import android.icu.text.CaseMap; 22 import android.icu.text.ListFormatter; 23 import android.icu.text.NumberingSystem; 24 import android.icu.util.ULocale; 25 import android.os.LocaleList; 26 import android.text.TextUtils; 27 28 import java.text.Collator; 29 import java.util.Comparator; 30 import java.util.Locale; 31 32 /** 33 * This class implements some handy methods to process with locales. 34 */ 35 public class LocaleHelper { 36 37 /** 38 * Sentence-case (first character uppercased). 39 * 40 * @param str the string to sentence-case. 41 * @param locale the locale used for the case conversion. 42 * @return the string converted to sentence-case. 43 */ toSentenceCase(String str, Locale locale)44 public static String toSentenceCase(String str, Locale locale) { 45 // Titlecases only the character at index 0, don't touch anything else 46 return CaseMap.toTitle().wholeString().noLowercase().apply(locale, null, str); 47 } 48 49 /** 50 * Normalizes a string for locale name search. Does case conversion for now, 51 * but might do more in the future. 52 * 53 * <p>Warning: it is only intended to be used in searches by the locale picker. 54 * Don't use it for other things, it is very limited.</p> 55 * 56 * @param str the string to normalize 57 * @param locale the locale that might be used for certain operations (i.e. case conversion) 58 * @return the string normalized for search 59 */ 60 @UnsupportedAppUsage normalizeForSearch(String str, Locale locale)61 public static String normalizeForSearch(String str, Locale locale) { 62 // TODO: tbd if it needs to be smarter (real normalization, remove accents, etc.) 63 // If needed we might use case folding and ICU/CLDR's collation-based loose searching. 64 // TODO: decide what should the locale be, the default locale, or the locale of the string. 65 // Uppercase is better than lowercase because of things like sharp S, Greek sigma, ... 66 return str.toUpperCase(); 67 } 68 69 // For some locales we want to use a "dialect" form, for instance 70 // "Dari" instead of "Persian (Afghanistan)", or "Moldavian" instead of "Romanian (Moldova)" shouldUseDialectName(Locale locale)71 private static boolean shouldUseDialectName(Locale locale) { 72 final String lang = locale.getLanguage(); 73 return "fa".equals(lang) // Persian 74 || "ro".equals(lang) // Romanian 75 || "zh".equals(lang); // Chinese 76 } 77 78 /** 79 * Returns the locale localized for display in the provided locale. 80 * 81 * @param locale the locale whose name is to be displayed. 82 * @param displayLocale the locale in which to display the name. 83 * @param sentenceCase true if the result should be sentence-cased 84 * @return the localized name of the locale. 85 */ 86 @UnsupportedAppUsage getDisplayName(Locale locale, Locale displayLocale, boolean sentenceCase)87 public static String getDisplayName(Locale locale, Locale displayLocale, boolean sentenceCase) { 88 final ULocale displayULocale = ULocale.forLocale(displayLocale); 89 String result = shouldUseDialectName(locale) 90 ? ULocale.getDisplayNameWithDialect(locale.toLanguageTag(), displayULocale) 91 : ULocale.getDisplayName(locale.toLanguageTag(), displayULocale); 92 return sentenceCase ? toSentenceCase(result, displayLocale) : result; 93 } 94 95 /** 96 * Returns the locale localized for display in the default locale. 97 * 98 * @param locale the locale whose name is to be displayed. 99 * @param sentenceCase true if the result should be sentence-cased 100 * @return the localized name of the locale. 101 */ getDisplayName(Locale locale, boolean sentenceCase)102 public static String getDisplayName(Locale locale, boolean sentenceCase) { 103 return getDisplayName(locale, Locale.getDefault(), sentenceCase); 104 } 105 106 /** 107 * Returns a locale's country localized for display in the provided locale. 108 * 109 * @param locale the locale whose country will be displayed. 110 * @param displayLocale the locale in which to display the name. 111 * @return the localized country name. 112 */ 113 @UnsupportedAppUsage getDisplayCountry(Locale locale, Locale displayLocale)114 public static String getDisplayCountry(Locale locale, Locale displayLocale) { 115 final String languageTag = locale.toLanguageTag(); 116 final ULocale uDisplayLocale = ULocale.forLocale(displayLocale); 117 final String country = ULocale.getDisplayCountry(languageTag, uDisplayLocale); 118 final String numberingSystem = locale.getUnicodeLocaleType("nu"); 119 if (numberingSystem != null) { 120 return String.format("%s (%s)", country, 121 ULocale.getDisplayKeywordValue(languageTag, "numbers", uDisplayLocale)); 122 } else { 123 return country; 124 } 125 } 126 127 /** 128 * Returns a locale's country localized for display in the default locale. 129 * 130 * @param locale the locale whose country will be displayed. 131 * @return the localized country name. 132 */ getDisplayCountry(Locale locale)133 public static String getDisplayCountry(Locale locale) { 134 return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.getDefault()); 135 } 136 137 /** 138 * Returns the locale list localized for display in the provided locale. 139 * 140 * @param locales the list of locales whose names is to be displayed. 141 * @param displayLocale the locale in which to display the names. 142 * If this is null, it will use the default locale. 143 * @param maxLocales maximum number of locales to display. Generates ellipsis after that. 144 * @return the locale aware list of locale names 145 */ getDisplayLocaleList( LocaleList locales, Locale displayLocale, @IntRange(from=1) int maxLocales)146 public static String getDisplayLocaleList( 147 LocaleList locales, Locale displayLocale, @IntRange(from=1) int maxLocales) { 148 149 final Locale dispLocale = displayLocale == null ? Locale.getDefault() : displayLocale; 150 151 final boolean ellipsisNeeded = locales.size() > maxLocales; 152 final int localeCount, listCount; 153 if (ellipsisNeeded) { 154 localeCount = maxLocales; 155 listCount = maxLocales + 1; // One extra slot for the ellipsis 156 } else { 157 listCount = localeCount = locales.size(); 158 } 159 final String[] localeNames = new String[listCount]; 160 for (int i = 0; i < localeCount; i++) { 161 localeNames[i] = LocaleHelper.getDisplayName(locales.get(i), dispLocale, false); 162 } 163 if (ellipsisNeeded) { 164 // Theoretically, we want to extract this from ICU's Resource Bundle for 165 // "Ellipsis/final", which seems to have different strings than the normal ellipsis for 166 // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two 167 // problems: it's expensive to extract it, and in case the output string becomes 168 // automatically ellipsized, it can result in weird output. 169 localeNames[maxLocales] = TextUtils.getEllipsisString(TextUtils.TruncateAt.END); 170 } 171 172 ListFormatter lfn = ListFormatter.getInstance(dispLocale); 173 return lfn.format((Object[]) localeNames); 174 } 175 176 /** 177 * Returns numbering system value of a locale for display in the provided locale. 178 * 179 * @param locale The locale whose key value is displayed. 180 * @param displayLocale The locale in which to display the key value. 181 * @return The string of numbering system. 182 */ getDisplayNumberingSystemKeyValue( Locale locale, Locale displayLocale)183 public static String getDisplayNumberingSystemKeyValue( 184 Locale locale, Locale displayLocale) { 185 ULocale uLocale = new ULocale.Builder() 186 .setUnicodeLocaleKeyword("nu", NumberingSystem.getInstance(locale).getName()) 187 .build(); 188 return uLocale.getDisplayKeywordValue("numbers", ULocale.forLocale(displayLocale)); 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 */ addLikelySubtags(Locale locale)197 public static Locale addLikelySubtags(Locale locale) { 198 return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale(); 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 "français" as a generic label in the language selection, 206 * or "français (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 @UnsupportedAppUsage LocaleInfoComparator(Locale sortLocale, boolean countryMode)221 public LocaleInfoComparator(Locale sortLocale, boolean countryMode) { 222 mCollator = Collator.getInstance(sortLocale); 223 mCountryMode = countryMode; 224 } 225 226 /* 227 * The Arabic collation should ignore Alef-Lam at the beginning (b/26277596) 228 * 229 * We look at the label's locale, not the current system locale. 230 * This is because the name of the Arabic language itself is in Arabic, 231 * and starts with Alef-Lam, no matter what the system locale is. 232 */ removePrefixForCompare(Locale locale, String str)233 private String removePrefixForCompare(Locale locale, String str) { 234 if ("ar".equals(locale.getLanguage()) && str.startsWith(PREFIX_ARABIC)) { 235 return str.substring(PREFIX_ARABIC.length()); 236 } 237 return str; 238 } 239 240 /** 241 * Compares its two arguments for order. 242 * 243 * @param lhs the first object to be compared 244 * @param rhs the second object to be compared 245 * @return a negative integer, zero, or a positive integer as the first 246 * argument is less than, equal to, or greater than the second. 247 */ 248 @UnsupportedAppUsage 249 @Override compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs)250 public int compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs) { 251 // We don't care about the various suggestion types, just "suggested" (!= 0) 252 // and "all others" (== 0) 253 if (lhs.isAppCurrentLocale() || rhs.isAppCurrentLocale()) { 254 return lhs.isAppCurrentLocale() ? -1 : 1; 255 } else if (lhs.isSystemLocale() || rhs.isSystemLocale()) { 256 return lhs.isSystemLocale() ? -1 : 1; 257 } else if (lhs.isSuggested() == rhs.isSuggested()) { 258 // They are in the same "bucket" (suggested / others), so we compare the text 259 return mCollator.compare( 260 removePrefixForCompare(lhs.getLocale(), lhs.getLabel(mCountryMode)), 261 removePrefixForCompare(rhs.getLocale(), rhs.getLabel(mCountryMode))); 262 } else { 263 // One locale is suggested and one is not, so we put them in different "buckets" 264 return lhs.isSuggested() ? -1 : 1; 265 } 266 } 267 } 268 } 269