1 // Copyright 2014 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.content.Context; 8 import android.content.res.Configuration; 9 import android.os.Build; 10 import android.os.LocaleList; 11 import android.text.TextUtils; 12 13 import androidx.annotation.RequiresApi; 14 import androidx.annotation.VisibleForTesting; 15 16 import org.chromium.base.annotations.CalledByNative; 17 18 import java.util.ArrayList; 19 import java.util.Locale; 20 21 /** 22 * This class provides the locale related methods. 23 */ 24 public class LocaleUtils { 25 /** 26 * Guards this class from being instantiated. 27 */ LocaleUtils()28 private LocaleUtils() { 29 } 30 31 /** 32 * Java keeps deprecated language codes for Hebrew, Yiddish and Indonesian but Chromium uses 33 * updated ones. Similarly, Android uses "tl" while Chromium uses "fil" for Tagalog/Filipino. 34 * So apply a mapping here. 35 * See http://developer.android.com/reference/java/util/Locale.html 36 * @return a updated language code for Chromium with given language string. 37 */ getUpdatedLanguageForChromium(String language)38 public static String getUpdatedLanguageForChromium(String language) { 39 // IMPORTANT: Keep in sync with the mapping found in: 40 // build/android/gyp/util/resource_utils.py (Yiddish and Javanese are not possible Android 41 // languages but are possible Chromium languages, they do not need to be kept in sync). 42 switch (language) { 43 case "iw": 44 return "he"; // Hebrew 45 case "ji": 46 return "yi"; // Yiddish 47 case "in": 48 return "id"; // Indonesian 49 case "tl": 50 return "fil"; // Filipino 51 case "jw": 52 return "jv"; // Javanese 53 default: 54 return language; 55 } 56 } 57 58 /** 59 * @return a locale with updated language codes for Chromium, with translated modern language 60 * codes used by Chromium. 61 */ 62 @VisibleForTesting getUpdatedLocaleForChromium(Locale locale)63 public static Locale getUpdatedLocaleForChromium(Locale locale) { 64 String language = locale.getLanguage(); 65 String languageForChrome = getUpdatedLanguageForChromium(language); 66 if (languageForChrome.equals(language)) { 67 return locale; 68 } 69 return new Locale.Builder().setLocale(locale).setLanguage(languageForChrome).build(); 70 } 71 72 /** 73 * Android uses "tl" while Chromium uses "fil" for Tagalog/Filipino. 74 * So apply a mapping here. 75 * See http://developer.android.com/reference/java/util/Locale.html 76 * @return a updated language code for Android with given language string. 77 */ getUpdatedLanguageForAndroid(String language)78 public static String getUpdatedLanguageForAndroid(String language) { 79 // IMPORTANT: Keep in sync with the mapping found in: 80 // build/android/gyp/util/resource_utils.py 81 switch (language) { 82 case "und": 83 return ""; // Undefined 84 case "fil": 85 return "tl"; // Filipino 86 default: 87 return language; 88 } 89 } 90 91 /** 92 * @return a locale with updated language codes for Android, from translated modern language 93 * codes used by Chromium. 94 */ 95 @VisibleForTesting getUpdatedLocaleForAndroid(Locale locale)96 public static Locale getUpdatedLocaleForAndroid(Locale locale) { 97 String language = locale.getLanguage(); 98 String languageForAndroid = getUpdatedLanguageForAndroid(language); 99 if (languageForAndroid.equals(language)) { 100 return locale; 101 } 102 return new Locale.Builder().setLocale(locale).setLanguage(languageForAndroid).build(); 103 } 104 105 /** 106 * This function creates a Locale object from xx-XX style string where xx is language code 107 * and XX is a country code. This works for API level lower than 21. 108 * @return the locale that best represents the language tag. 109 */ forLanguageTagCompat(String languageTag)110 public static Locale forLanguageTagCompat(String languageTag) { 111 String[] tag = languageTag.split("-"); 112 if (tag.length == 0) { 113 return new Locale(""); 114 } 115 String language = getUpdatedLanguageForAndroid(tag[0]); 116 if ((language.length() != 2 && language.length() != 3)) { 117 return new Locale(""); 118 } 119 if (tag.length == 1) { 120 return new Locale(language); 121 } 122 String country = tag[1]; 123 if (country.length() != 2 && country.length() != 3) { 124 return new Locale(language); 125 } 126 return new Locale(language, country); 127 } 128 129 /** 130 * This function creates a Locale object from xx-XX style string where xx is language code 131 * and XX is a country code. 132 * @return the locale that best represents the language tag. 133 */ forLanguageTag(String languageTag)134 public static Locale forLanguageTag(String languageTag) { 135 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 136 Locale locale = Locale.forLanguageTag(languageTag); 137 return getUpdatedLocaleForAndroid(locale); 138 } 139 return forLanguageTagCompat(languageTag); 140 } 141 142 /** 143 * Converts Locale object to the BCP 47 compliant string format. 144 * This works for API level lower than 24. 145 * 146 * Note that for Android M or before, we cannot use Locale.getLanguage() and 147 * Locale.toLanguageTag() for this purpose. Since Locale.getLanguage() returns deprecated 148 * language code even if the Locale object is constructed with updated language code. As for 149 * Locale.toLanguageTag(), it does a special conversion from deprecated language code to updated 150 * one, but it is only usable for Android N or after. 151 * @return a well-formed IETF BCP 47 language tag with language and country code that 152 * represents this locale. 153 */ toLanguageTag(Locale locale)154 public static String toLanguageTag(Locale locale) { 155 String language = getUpdatedLanguageForChromium(locale.getLanguage()); 156 String country = locale.getCountry(); 157 if (language.equals("no") && country.equals("NO") && locale.getVariant().equals("NY")) { 158 return "nn-NO"; 159 } 160 return country.isEmpty() ? language : language + "-" + country; 161 } 162 163 /** 164 * Converts LocaleList object to the comma separated BCP 47 compliant string format. 165 * 166 * @return a well-formed IETF BCP 47 language tag with language and country code that 167 * represents this locale list. 168 */ 169 @RequiresApi(Build.VERSION_CODES.N) toLanguageTags(LocaleList localeList)170 public static String toLanguageTags(LocaleList localeList) { 171 ArrayList<String> newLocaleList = new ArrayList<>(); 172 for (int i = 0; i < localeList.size(); i++) { 173 Locale locale = getUpdatedLocaleForChromium(localeList.get(i)); 174 newLocaleList.add(toLanguageTag(locale)); 175 } 176 return TextUtils.join(",", newLocaleList); 177 } 178 179 /** 180 * Extracts the base language from a BCP 47 language tag. 181 * @param languageTag language tag of the form xx-XX or xx. 182 * @return the xx part of the language tag. 183 */ toBaseLanguage(String languageTag)184 public static String toBaseLanguage(String languageTag) { 185 int pos = languageTag.indexOf('-'); 186 if (pos < 0) { 187 return languageTag; 188 } 189 return languageTag.substring(0, pos); 190 } 191 192 /** 193 * @param first A BCP 47 formated language tag. 194 * @param second A BCP 47 formated language tag. 195 * @return True if the base language (e.g. "en" for "en-AU") is the same for each tag. 196 */ isBaseLanguageEqual(String first, String second)197 public static boolean isBaseLanguageEqual(String first, String second) { 198 return TextUtils.equals(toBaseLanguage(first), toBaseLanguage(second)); 199 } 200 201 /** 202 * @return a language tag string that represents the default locale. 203 * The language tag is well-formed IETF BCP 47 language tag with language and country 204 * code. 205 */ 206 @CalledByNative getDefaultLocaleString()207 public static String getDefaultLocaleString() { 208 return toLanguageTag(Locale.getDefault()); 209 } 210 211 /** 212 * @return a comma separated language tags string that represents a default locale or locales. 213 * Each language tag is well-formed IETF BCP 47 language tag with language and country 214 * code. 215 */ 216 @CalledByNative getDefaultLocaleListString()217 public static String getDefaultLocaleListString() { 218 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 219 return toLanguageTags(LocaleList.getDefault()); 220 } 221 return getDefaultLocaleString(); 222 } 223 224 /** 225 * @return The default country code set during install. 226 */ 227 @CalledByNative getDefaultCountryCode()228 private static String getDefaultCountryCode() { 229 CommandLine commandLine = CommandLine.getInstance(); 230 return commandLine.hasSwitch(BaseSwitches.DEFAULT_COUNTRY_CODE_AT_INSTALL) 231 ? commandLine.getSwitchValue(BaseSwitches.DEFAULT_COUNTRY_CODE_AT_INSTALL) 232 : Locale.getDefault().getCountry(); 233 } 234 235 /** 236 * Return the language tag of the first language in Configuration. 237 * @param config Configuration to get language for. 238 * @return The BCP 47 tag representation of the configuration's first locale. 239 * Configuration.locale is deprecated on N+. However, read only is equivalent to 240 * Configuration.getLocales()[0]. Change when minSdkVersion >= 24. 241 */ 242 @SuppressWarnings("deprecation") getConfigurationLanguage(Configuration config)243 public static String getConfigurationLanguage(Configuration config) { 244 Locale locale = config.locale; 245 return (locale != null) ? locale.toLanguageTag() : ""; 246 } 247 248 /** 249 * Return the language tag of the first language in the configuration 250 * @param context Context to get language for. 251 * @return The BCP 47 tag representation of the context's first locale. 252 */ getContextLanguage(Context context)253 public static String getContextLanguage(Context context) { 254 return getConfigurationLanguage(context.getResources().getConfiguration()); 255 } 256 257 /** 258 * Prepend languageTag to the default locales on config. 259 * @param base The Context to use for the base configuration. 260 * @param config The Configuration to update. 261 * @param languageTag The language to prepend to default locales. 262 */ updateConfig(Context base, Configuration config, String languageTag)263 public static void updateConfig(Context base, Configuration config, String languageTag) { 264 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 265 ApisN.setConfigLocales(base, config, languageTag); 266 } else { 267 config.setLocale(Locale.forLanguageTag(languageTag)); 268 } 269 } 270 271 /** 272 * Updates the default Locale/LocaleList to those of config. 273 * @param config 274 */ setDefaultLocalesFromConfiguration(Configuration config)275 public static void setDefaultLocalesFromConfiguration(Configuration config) { 276 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 277 ApisN.setLocaleList(config); 278 } else { 279 Locale.setDefault(config.locale); 280 } 281 } 282 283 /** 284 * Helper class for N only code that is not validated on pre-N devices. 285 */ 286 @RequiresApi(Build.VERSION_CODES.N) 287 @VisibleForTesting 288 static class ApisN { setConfigLocales(Context base, Configuration config, String language)289 static void setConfigLocales(Context base, Configuration config, String language) { 290 LocaleList updatedLocales = prependToLocaleList( 291 language, base.getResources().getConfiguration().getLocales()); 292 config.setLocales(updatedLocales); 293 } 294 setLocaleList(Configuration config)295 static void setLocaleList(Configuration config) { 296 LocaleList.setDefault(config.getLocales()); 297 } 298 299 /** 300 * Create a new LocaleList with languageTag added to the front. 301 * If languageTag is already in the list the existing tag is moved to the front. 302 * @param languageTag String of language tag to prepend 303 * @param localeList LocaleList to prepend to. 304 * @return LocaleList 305 */ prependToLocaleList(String languageTag, LocaleList localeList)306 static LocaleList prependToLocaleList(String languageTag, LocaleList localeList) { 307 String languageList = localeList.toLanguageTags(); 308 309 // Remove the first instance of languageTag with associated comma if present. 310 // Pattern example: "(^|,)en-US$|en-US," 311 String pattern = String.format("(^|,)%1$s$|%1$s,", languageTag); 312 languageList = languageList.replaceFirst(pattern, ""); 313 314 return LocaleList.forLanguageTags( 315 String.format("%1$s,%2$s", languageTag, languageList)); 316 } 317 } 318 } 319