1 // Copyright 2014 The Chromium Authors. All rights reserved. 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.annotation.TargetApi; 8 import android.os.Build; 9 import android.os.LocaleList; 10 import android.text.TextUtils; 11 12 import org.chromium.base.annotations.CalledByNative; 13 14 import java.util.ArrayList; 15 import java.util.Collections; 16 import java.util.HashMap; 17 import java.util.Locale; 18 import java.util.Map; 19 20 /** 21 * This class provides the locale related methods. 22 */ 23 public class LocaleUtils { 24 /** 25 * Guards this class from being instantiated. 26 */ LocaleUtils()27 private LocaleUtils() { 28 } 29 30 private static final Map<String, String> LANGUAGE_MAP_FOR_CHROMIUM; 31 private static final Map<String, String> LANGUAGE_MAP_FOR_ANDROID; 32 33 static { 34 // A variation of this mapping also exists in: 35 // build/android/gyp/package_resources.py 36 HashMap<String, String> mapForChromium = new HashMap<>(); 37 mapForChromium.put("iw", "he"); // Hebrew 38 mapForChromium.put("ji", "yi"); // Yiddish 39 mapForChromium.put("in", "id"); // Indonesian 40 mapForChromium.put("tl", "fil"); // Filipino 41 LANGUAGE_MAP_FOR_CHROMIUM = Collections.unmodifiableMap(mapForChromium); 42 } 43 44 static { 45 HashMap<String, String> mapForAndroid = new HashMap<>(); 46 mapForAndroid.put("und", ""); // Undefined 47 mapForAndroid.put("fil", "tl"); // Filipino 48 LANGUAGE_MAP_FOR_ANDROID = Collections.unmodifiableMap(mapForAndroid); 49 } 50 51 /** 52 * Java keeps deprecated language codes for Hebrew, Yiddish and Indonesian but Chromium uses 53 * updated ones. Similarly, Android uses "tl" while Chromium uses "fil" for Tagalog/Filipino. 54 * So apply a mapping here. 55 * See http://developer.android.com/reference/java/util/Locale.html 56 * @return a updated language code for Chromium with given language string. 57 */ getUpdatedLanguageForChromium(String language)58 public static String getUpdatedLanguageForChromium(String language) { 59 String updatedLanguageCode = LANGUAGE_MAP_FOR_CHROMIUM.get(language); 60 return updatedLanguageCode == null ? language : updatedLanguageCode; 61 } 62 63 /** 64 * @return a locale with updated language codes for Chromium, with translated modern language 65 * codes used by Chromium. 66 */ 67 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 68 @VisibleForTesting getUpdatedLocaleForChromium(Locale locale)69 public static Locale getUpdatedLocaleForChromium(Locale locale) { 70 String languageForChrome = LANGUAGE_MAP_FOR_CHROMIUM.get(locale.getLanguage()); 71 if (languageForChrome == null) { 72 return locale; 73 } 74 return new Locale.Builder().setLocale(locale).setLanguage(languageForChrome).build(); 75 } 76 77 /** 78 * Android uses "tl" while Chromium uses "fil" for Tagalog/Filipino. 79 * So apply a mapping here. 80 * See http://developer.android.com/reference/java/util/Locale.html 81 * @return a updated language code for Android with given language string. 82 */ getUpdatedLanguageForAndroid(String language)83 public static String getUpdatedLanguageForAndroid(String language) { 84 String updatedLanguageCode = LANGUAGE_MAP_FOR_ANDROID.get(language); 85 return updatedLanguageCode == null ? language : updatedLanguageCode; 86 } 87 88 /** 89 * @return a locale with updated language codes for Android, from translated modern language 90 * codes used by Chromium. 91 */ 92 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 93 @VisibleForTesting getUpdatedLocaleForAndroid(Locale locale)94 public static Locale getUpdatedLocaleForAndroid(Locale locale) { 95 String languageForAndroid = LANGUAGE_MAP_FOR_ANDROID.get(locale.getLanguage()); 96 if (languageForAndroid == null) { 97 return locale; 98 } 99 return new Locale.Builder().setLocale(locale).setLanguage(languageForAndroid).build(); 100 } 101 102 /** 103 * This function creates a Locale object from xx-XX style string where xx is language code 104 * and XX is a country code. This works for API level lower than 21. 105 * @return the locale that best represents the language tag. 106 */ forLanguageTagCompat(String languageTag)107 public static Locale forLanguageTagCompat(String languageTag) { 108 String[] tag = languageTag.split("-"); 109 if (tag.length == 0) { 110 return new Locale(""); 111 } 112 String language = getUpdatedLanguageForAndroid(tag[0]); 113 if ((language.length() != 2 && language.length() != 3) || language.equals("und")) { 114 return new Locale(""); 115 } 116 if (tag.length == 1) { 117 return new Locale(language); 118 } 119 String country = tag[1]; 120 if (country.length() != 2 && country.length() != 3) { 121 return new Locale(language); 122 } 123 return new Locale(language, country); 124 } 125 126 /** 127 * This function creates a Locale object from xx-XX style string where xx is language code 128 * and XX is a country code. 129 * @return the locale that best represents the language tag. 130 */ forLanguageTag(String languageTag)131 public static Locale forLanguageTag(String languageTag) { 132 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 133 Locale locale = Locale.forLanguageTag(languageTag); 134 return getUpdatedLocaleForAndroid(locale); 135 } 136 return forLanguageTagCompat(languageTag); 137 } 138 139 /** 140 * Converts Locale object to the BCP 47 compliant string format. 141 * This works for API level lower than 24. 142 * 143 * Note that for Android M or before, we cannot use Locale.getLanguage() and 144 * Locale.toLanguageTag() for this purpose. Since Locale.getLanguage() returns deprecated 145 * language code even if the Locale object is constructed with updated language code. As for 146 * Locale.toLanguageTag(), it does a special conversion from deprecated language code to updated 147 * one, but it is only usable for Android N or after. 148 * @return a well-formed IETF BCP 47 language tag with language and country code that 149 * represents this locale. 150 */ toLanguageTag(Locale locale)151 public static String toLanguageTag(Locale locale) { 152 String language = getUpdatedLanguageForChromium(locale.getLanguage()); 153 String country = locale.getCountry(); 154 if (language.equals("no") && country.equals("NO") && locale.getVariant().equals("NY")) { 155 return "nn-NO"; 156 } 157 return country.isEmpty() ? language : language + "-" + country; 158 } 159 160 /** 161 * Converts LocaleList object to the comma separated BCP 47 compliant string format. 162 * 163 * @return a well-formed IETF BCP 47 language tag with language and country code that 164 * represents this locale list. 165 */ 166 @TargetApi(Build.VERSION_CODES.N) toLanguageTags(LocaleList localeList)167 public static String toLanguageTags(LocaleList localeList) { 168 ArrayList<String> newLocaleList = new ArrayList<>(); 169 for (int i = 0; i < localeList.size(); i++) { 170 Locale locale = getUpdatedLocaleForChromium(localeList.get(i)); 171 newLocaleList.add(toLanguageTag(locale)); 172 } 173 return TextUtils.join(",", newLocaleList); 174 } 175 176 /** 177 * @return a comma separated language tags string that represents a default locale. 178 * Each language tag is well-formed IETF BCP 47 language tag with language and country 179 * code. 180 */ 181 @CalledByNative getDefaultLocaleString()182 public static String getDefaultLocaleString() { 183 return toLanguageTag(Locale.getDefault()); 184 } 185 186 /** 187 * @return a comma separated language tags string that represents a default locale or locales. 188 * Each language tag is well-formed IETF BCP 47 language tag with language and country 189 * code. 190 */ getDefaultLocaleListString()191 public static String getDefaultLocaleListString() { 192 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 193 return toLanguageTags(LocaleList.getDefault()); 194 } 195 return getDefaultLocaleString(); 196 } 197 198 /** 199 * @return The default country code set during install. 200 */ 201 @CalledByNative getDefaultCountryCode()202 private static String getDefaultCountryCode() { 203 CommandLine commandLine = CommandLine.getInstance(); 204 return commandLine.hasSwitch(BaseSwitches.DEFAULT_COUNTRY_CODE_AT_INSTALL) 205 ? commandLine.getSwitchValue(BaseSwitches.DEFAULT_COUNTRY_CODE_AT_INSTALL) 206 : Locale.getDefault().getCountry(); 207 } 208 209 } 210