• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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