1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.latin.common; 18 19 import java.util.HashMap; 20 import java.util.HashSet; 21 import java.util.Locale; 22 23 import javax.annotation.Nonnull; 24 import javax.annotation.Nullable; 25 26 /** 27 * A class to help with handling Locales in string form. 28 * 29 * This file has the same meaning and features (and shares all of its code) with the one with the 30 * same name in Latin IME. They need to be kept synchronized; for any update/bugfix to 31 * this file, consider also updating/fixing the version in Latin IME. 32 */ 33 public final class LocaleUtils { LocaleUtils()34 private LocaleUtils() { 35 // Intentional empty constructor for utility class. 36 } 37 38 // Locale match level constants. 39 // A higher level of match is guaranteed to have a higher numerical value. 40 // Some room is left within constants to add match cases that may arise necessary 41 // in the future, for example differentiating between the case where the countries 42 // are both present and different, and the case where one of the locales does not 43 // specify the countries. This difference is not needed now. 44 45 // Nothing matches. 46 public static final int LOCALE_NO_MATCH = 0; 47 // The languages matches, but the country are different. Or, the reference locale requires a 48 // country and the tested locale does not have one. 49 public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3; 50 // The languages and country match, but the variants are different. Or, the reference locale 51 // requires a variant and the tested locale does not have one. 52 public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6; 53 // The required locale is null or empty so it will accept anything, and the tested locale 54 // is non-null and non-empty. 55 public static final int LOCALE_ANY_MATCH = 10; 56 // The language matches, and the tested locale specifies a country but the reference locale 57 // does not require one. 58 public static final int LOCALE_LANGUAGE_MATCH = 15; 59 // The language and the country match, and the tested locale specifies a variant but the 60 // reference locale does not require one. 61 public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20; 62 // The compared locales are fully identical. This is the best match level. 63 public static final int LOCALE_FULL_MATCH = 30; 64 65 // The level at which a match is "normally" considered a locale match with standard algorithms. 66 // Don't use this directly, use #isMatch to test. 67 private static final int LOCALE_MATCH = LOCALE_ANY_MATCH; 68 69 // Make this match the maximum match level. If this evolves to have more than 2 digits 70 // when written in base 10, also adjust the getMatchLevelSortedString method. 71 private static final int MATCH_LEVEL_MAX = 30; 72 73 /** 74 * Return how well a tested locale matches a reference locale. 75 * 76 * This will check the tested locale against the reference locale and return a measure of how 77 * a well it matches the reference. The general idea is that the tested locale has to match 78 * every specified part of the required locale. A full match occur when they are equal, a 79 * partial match when the tested locale agrees with the reference locale but is more specific, 80 * and a difference when the tested locale does not comply with all requirements from the 81 * reference locale. 82 * In more detail, if the reference locale specifies at least a language and the testedLocale 83 * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the 84 * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH 85 * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and 86 * tested locale agree on the language, but not on the country, 87 * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country, 88 * and LOCALE_LANGUAGE_MATCH otherwise. 89 * If they agree on both the language and the country, but not on the variant, 90 * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale 91 * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches, 92 * LOCALE_FULL_MATCH is returned. 93 * Examples: 94 * en <=> en_US => LOCALE_LANGUAGE_MATCH 95 * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER 96 * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER 97 * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH 98 * sp_US <=> en_US => LOCALE_NO_MATCH 99 * de <=> de => LOCALE_FULL_MATCH 100 * en_US <=> en_US => LOCALE_FULL_MATCH 101 * "" <=> en_US => LOCALE_ANY_MATCH 102 * 103 * @param referenceLocale the reference locale to test against. 104 * @param testedLocale the locale to test. 105 * @return a constant that measures how well the tested locale matches the reference locale. 106 */ getMatchLevel(@ullable final String referenceLocale, @Nullable final String testedLocale)107 public static int getMatchLevel(@Nullable final String referenceLocale, 108 @Nullable final String testedLocale) { 109 if (StringUtils.isEmpty(referenceLocale)) { 110 return StringUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH; 111 } 112 if (null == testedLocale) return LOCALE_NO_MATCH; 113 final String[] referenceParams = referenceLocale.split("_", 3); 114 final String[] testedParams = testedLocale.split("_", 3); 115 // By spec of String#split, [0] cannot be null and length cannot be 0. 116 if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH; 117 switch (referenceParams.length) { 118 case 1: 119 return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH; 120 case 2: 121 if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 122 if (!referenceParams[1].equals(testedParams[1])) 123 return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 124 if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH; 125 return LOCALE_FULL_MATCH; 126 case 3: 127 if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 128 if (!referenceParams[1].equals(testedParams[1])) 129 return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 130 if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; 131 if (!referenceParams[2].equals(testedParams[2])) 132 return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; 133 return LOCALE_FULL_MATCH; 134 } 135 // It should be impossible to come here 136 return LOCALE_NO_MATCH; 137 } 138 139 /** 140 * Return a string that represents this match level, with better matches first. 141 * 142 * The strings are sorted in lexicographic order: a better match will always be less than 143 * a worse match when compared together. 144 */ getMatchLevelSortedString(final int matchLevel)145 public static String getMatchLevelSortedString(final int matchLevel) { 146 // This works because the match levels are 0~99 (actually 0~30) 147 // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel 148 return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel); 149 } 150 151 /** 152 * Find out whether a match level should be considered a match. 153 * 154 * This method takes a match level as returned by the #getMatchLevel method, and returns whether 155 * it should be considered a match in the usual sense with standard Locale functions. 156 * 157 * @param level the match level, as returned by getMatchLevel. 158 * @return whether this is a match or not. 159 */ isMatch(final int level)160 public static boolean isMatch(final int level) { 161 return LOCALE_MATCH <= level; 162 } 163 164 private static final HashMap<String, Locale> sLocaleCache = new HashMap<>(); 165 166 /** 167 * Creates a locale from a string specification. 168 * @param localeString a string specification of a locale, in a format of "ll_cc_variant" where 169 * "ll" is a language code, "cc" is a country code. 170 */ 171 @Nonnull constructLocaleFromString(@onnull final String localeString)172 public static Locale constructLocaleFromString(@Nonnull final String localeString) { 173 synchronized (sLocaleCache) { 174 if (sLocaleCache.containsKey(localeString)) { 175 return sLocaleCache.get(localeString); 176 } 177 final String[] elements = localeString.split("_", 3); 178 final Locale locale; 179 if (elements.length == 1) { 180 locale = new Locale(elements[0] /* language */); 181 } else if (elements.length == 2) { 182 locale = new Locale(elements[0] /* language */, elements[1] /* country */); 183 } else { // localeParams.length == 3 184 locale = new Locale(elements[0] /* language */, elements[1] /* country */, 185 elements[2] /* variant */); 186 } 187 sLocaleCache.put(localeString, locale); 188 return locale; 189 } 190 } 191 192 // TODO: Get this information from the framework instead of maintaining here by ourselves. 193 private static final HashSet<String> sRtlLanguageCodes = new HashSet<>(); 194 static { 195 // List of known Right-To-Left language codes. 196 sRtlLanguageCodes.add("ar"); // Arabic 197 sRtlLanguageCodes.add("fa"); // Persian 198 sRtlLanguageCodes.add("iw"); // Hebrew 199 sRtlLanguageCodes.add("ku"); // Kurdish 200 sRtlLanguageCodes.add("ps"); // Pashto 201 sRtlLanguageCodes.add("sd"); // Sindhi 202 sRtlLanguageCodes.add("ug"); // Uyghur 203 sRtlLanguageCodes.add("ur"); // Urdu 204 sRtlLanguageCodes.add("yi"); // Yiddish 205 } 206 isRtlLanguage(@onnull final Locale locale)207 public static boolean isRtlLanguage(@Nonnull final Locale locale) { 208 return sRtlLanguageCodes.contains(locale.getLanguage()); 209 } 210 } 211