| /* |
| * Copyright 2009 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.i18n.server; |
| |
| import com.google.gwt.i18n.shared.GwtLocale; |
| import com.google.gwt.i18n.shared.GwtLocaleFactory; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Class representing GWT locales and conversion to/from other formats. |
| * |
| * These locales correspond to BCP47. |
| */ |
| public class GwtLocaleImpl implements GwtLocale { |
| // TODO(jat): implement a version of this suitable for client-side use, |
| // probably using a generator to include only the information relevant to |
| // the set of locales supported, and then figure out a way to get that into |
| // the property provider to handle inheritance there. |
| |
| /** |
| * Maps deprecated language codes to the canonical code. Strings are always |
| * in pairs, with the first being the canonical code and the second being |
| * a deprecated code which maps to it. |
| * <p> |
| * Source: http://www.loc.gov/standards/iso639-2/php/code_changes.php |
| * <p> |
| * TODO: consider building maps if this list grows much. |
| */ |
| private static final String[] deprecatedLanguages = new String[] { |
| "he", "iw", // Hebrew |
| "id", "in", // Indonesian |
| "jv", "jw", // Javanese, typo in original publication |
| "ro", "mo", // Moldovian |
| "yi", "ji", // Yiddish |
| }; |
| |
| /** |
| * Maps deprecated region codes to the canonical code. Strings are always |
| * in pairs, with the first being the canonical code and the second being |
| * a deprecated code which maps to it. |
| * |
| * Note that any mappings which split an old code into multiple new codes |
| * cannot be done automatically (such as cs -> rs/me) -- perhaps we could |
| * have a way of flagging region codes which are no longer valid and allow |
| * an appropriate warning message. |
| * <p> |
| * Source: http://en.wikipedia.org/wiki/ISO_3166-1 |
| * <p> |
| * TODO: consider building maps if this list grows much. |
| */ |
| private static final String[] deprecatedRegions = new String[] { |
| // East Timor - http://www.iso.org/iso/newsletter_v-5_east_timor.pdf |
| "TL", "TP", |
| }; |
| |
| /** |
| * Add in the missing locale of a deprecated pair. |
| * |
| * @param factory GwtLocaleFactory to create new instances with |
| * @param locale locale to add deprecated pair for |
| * @param aliases where to store the alias if present |
| */ |
| private static void addDeprecatedPairs(GwtLocaleFactory factory, |
| GwtLocale locale, List<GwtLocale> aliases) { |
| int n = deprecatedLanguages.length; |
| for (int i = 0; i < n; i += 2) { |
| if (deprecatedLanguages[i].equals(locale.getLanguage())) { |
| aliases.add(factory.fromComponents(deprecatedLanguages[i + 1], |
| locale.getScript(), locale.getRegion(), locale.getVariant())); |
| break; |
| } |
| if (deprecatedLanguages[i + 1].equals(locale.getLanguage())) { |
| aliases.add(factory.fromComponents(deprecatedLanguages[i], |
| locale.getScript(), locale.getRegion(), locale.getVariant())); |
| break; |
| } |
| } |
| } |
| |
| private static void addImmediateParentRegions(GwtLocaleFactory factory, |
| GwtLocale locale, Collection<GwtLocale> work) { |
| String language = locale.getLanguage(); |
| String script = locale.getScript(); |
| String region = locale.getRegion(); |
| String variant = locale.getVariant(); |
| if (variant != null) { |
| work.add(factory.fromComponents(language, script, region, null)); |
| } |
| Set<String> immediateParents = RegionInheritance.getImmediateParents(region); |
| for (String parent : immediateParents) { |
| work.add(factory.fromComponents(language, script, parent, variant)); |
| if (variant != null) { |
| work.add(factory.fromComponents(language, script, parent, null)); |
| } |
| } |
| if (immediateParents.isEmpty()) { |
| work.add(factory.fromComponents(language, script, null, variant)); |
| if (variant != null) { |
| work.add(factory.fromComponents(language, script, null, null)); |
| } |
| } |
| if (script != null) { |
| work.add(factory.fromComponents(language, null, region, variant)); |
| if (variant != null) { |
| work.add(factory.fromComponents(language, null, region, null)); |
| } |
| } |
| } |
| |
| /** |
| * Add inherited regions for a given locale. |
| * |
| * @param factory |
| * @param inherits |
| * @param language |
| * @param script |
| * @param region |
| * @param variant |
| */ |
| private static void addParentRegionLocales(GwtLocaleFactory factory, |
| List<GwtLocale> inherits, String language, String script, String region, |
| String variant) { |
| for (String parent : RegionInheritance.getAllAncestors(region)) { |
| inherits.add(factory.fromComponents(language, script, parent, variant)); |
| } |
| } |
| |
| /** |
| * Add special aliases for a given locale. |
| * |
| * This includes things like choosing the default region for Chinese based on |
| * the script, handling Norwegian language changes, and treating pt_BR as the |
| * default pt type. |
| * |
| * @param factory GwtLocaleFactory to create new instances with |
| * @param locale locale to add deprecated pair for |
| * @param aliases where to store the alias if present |
| */ |
| private static void addSpecialAliases(GwtLocaleFactory factory, |
| GwtLocale locale, String initialScript, List<GwtLocale> aliases) { |
| String language = locale.getLanguage(); |
| String script = locale.getScript(); |
| String region = locale.getRegion(); |
| String variant = locale.getVariant(); |
| if ("zh".equals(language) && region == null |
| && (initialScript == null || initialScript.equals(script))) { |
| // Add aliases for main users of particular scripts, but only if the |
| // script matches what was initially supplied. This is to avoid cases |
| // like zh_TW => zh => zh_CN, while still allowing zh => zh_CN. |
| aliases.add(factory.fromComponents("zh", script, |
| "Hant".equals(script) ? "TW" : "CN", variant)); |
| } else if ("no".equals(language)) { |
| if (variant == null || "BOKMAL".equals(variant)) { |
| aliases.add(factory.fromComponents("nb", script, region, null)); |
| aliases.add(factory.fromComponents("no-bok", script, region, null)); |
| } else if ("NYNORSK".equals(variant)) { |
| aliases.add(factory.fromComponents("nn", script, region, null)); |
| aliases.add(factory.fromComponents("no-nyn", script, region, null)); |
| } |
| } else if ("nb".equals(language)) { |
| aliases.add(factory.fromComponents("no", script, region, "BOKMAL")); |
| } else if ("nn".equals(language)) { |
| aliases.add(factory.fromComponents("no", script, region, "NYNORSK")); |
| } else if ("pt".equals(language)) { |
| if (region == null) { |
| aliases.add(factory.fromComponents("pt", script, "BR", variant)); |
| } else if ("BR".equals(region)) { |
| aliases.add(factory.fromComponents("pt", script, null, variant)); |
| } |
| } |
| } |
| |
| private static boolean equalsNullCheck(String str1, String str2) { |
| if (str1 == null) { |
| return str2 == null; |
| } |
| return str1.equals(str2); |
| } |
| |
| /** |
| * Compare strings, accounting for nulls (which are treated as before any |
| * other value). |
| * |
| * @param a first string |
| * @param b second string |
| * |
| * @return positive if a>b, negative if a<b, 0 if equal |
| */ |
| private static int stringCompare(String a, String b) { |
| if (a == null) { |
| return b == null ? 0 : -1; |
| } |
| if (b == null) { |
| return 1; |
| } |
| return a.compareTo(b); |
| } |
| |
| private final GwtLocaleFactory factory; |
| |
| private final String language; |
| |
| private final String region; |
| |
| private final String script; |
| |
| private final String variant; |
| |
| private final Object cacheLock = new Object[0]; |
| |
| // protected by cacheLock |
| private List<GwtLocale> cachedSearchList; |
| |
| // protected by cacheLock |
| private List<GwtLocale> cachedAliases; |
| |
| /** |
| * Must only be called from a factory to preserve instance caching. |
| */ |
| GwtLocaleImpl(GwtLocaleFactory factory, String language, String region, |
| String script, String variant) { |
| this.factory = factory; |
| this.language = language; |
| this.region = region; |
| this.script = script; |
| this.variant = variant; |
| } |
| |
| public int compareTo(GwtLocale o) { |
| int c = stringCompare(language, o.getLanguage()); |
| if (c == 0) { |
| c = stringCompare(script, o.getScript()); |
| } |
| if (c == 0) { |
| c = stringCompare(region, o.getRegion()); |
| } |
| if (c == 0) { |
| c = stringCompare(variant, o.getVariant()); |
| } |
| return c; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof GwtLocale)) { |
| return false; |
| } |
| GwtLocale other = (GwtLocale) obj; |
| return equalsNullCheck(language, other.getLanguage()) |
| && equalsNullCheck(region, other.getRegion()) |
| && equalsNullCheck(script, other.getScript()) |
| && equalsNullCheck(variant, other.getVariant()); |
| } |
| |
| public List<GwtLocale> getAliases() { |
| // TODO(jat): more locale aliases? better way to encode them? |
| synchronized (cacheLock) { |
| if (cachedAliases == null) { |
| cachedAliases = new ArrayList<GwtLocale>(); |
| GwtLocale canonicalForm = getCanonicalForm(); |
| Set<GwtLocale> seen = new HashSet<GwtLocale>(); |
| cachedAliases.add(canonicalForm); |
| ArrayList<GwtLocale> nextGroup = new ArrayList<GwtLocale>(); |
| nextGroup.add(this); |
| // Account for default script |
| String defaultScript = DefaultLanguageScripts.getDefaultScript(language, |
| region); |
| if (defaultScript != null) { |
| if (script == null) { |
| nextGroup.add(factory.fromComponents(language, defaultScript, |
| region, variant)); |
| } else if (script.equals(defaultScript)) { |
| nextGroup.add(factory.fromComponents(language, null, region, |
| variant)); |
| } |
| } |
| String initialScript = script == null ? defaultScript : script; |
| while (!nextGroup.isEmpty()) { |
| List<GwtLocale> thisGroup = nextGroup; |
| nextGroup = new ArrayList<GwtLocale>(); |
| for (GwtLocale locale : thisGroup) { |
| if (seen.contains(locale)) { |
| continue; |
| } |
| seen.add(locale); |
| if (!locale.equals(canonicalForm)) { |
| cachedAliases.add(locale); |
| } |
| addDeprecatedPairs(factory, locale, nextGroup); |
| addSpecialAliases(factory, locale, initialScript, nextGroup); |
| } |
| } |
| cachedAliases = Collections.unmodifiableList(cachedAliases); |
| } |
| return cachedAliases; |
| } |
| } |
| |
| public String getAsString() { |
| StringBuilder buf = new StringBuilder(); |
| if (language != null) { |
| buf.append(language); |
| } |
| if (script != null) { |
| buf.append('_'); |
| buf.append(script); |
| } |
| if (region != null) { |
| buf.append('_'); |
| buf.append(region); |
| } |
| if (variant != null) { |
| buf.append('_'); |
| buf.append(variant); |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Returns this locale in canonical form. Changes for canonical form are: |
| * <ul> |
| * <li>Deprecated language/region tags are replaced with official versions |
| * </ul> |
| * |
| * @return GwtLocale instance |
| */ |
| public GwtLocale getCanonicalForm() { |
| String canonLanguage = language; |
| String canonScript = script; |
| String canonRegion = region; |
| String canonVariant = variant; |
| // Handle deprecated language codes |
| int n = deprecatedLanguages.length; |
| for (int i = 0; i < n; i += 2) { |
| if (deprecatedLanguages[i + 1].equals(canonLanguage)) { |
| canonLanguage = deprecatedLanguages[i]; |
| break; |
| } |
| } |
| // Handle deprecated region codes |
| n = deprecatedRegions.length; |
| for (int i = 0; i < n; i += 2) { |
| if (deprecatedRegions[i + 1].equals(canonRegion)) { |
| canonRegion = deprecatedRegions[i]; |
| break; |
| } |
| } |
| // Special-case Chinese default scripts |
| if ("zh".equals(canonLanguage)) { |
| if (canonRegion != null) { |
| if ("CN".equals(canonRegion) && "Hans".equals(canonScript)) { |
| canonScript = null; |
| } else if ("TW".equals(canonRegion) && "Hant".equals(canonScript)) { |
| canonScript = null; |
| } |
| } else if ("Hans".equals(canonScript)) { |
| canonRegion = "CN"; |
| canonScript = null; |
| } else if ("Hant".equals(canonScript)) { |
| canonRegion = "TW"; |
| canonScript = null; |
| } |
| } |
| // Special-case no->nb/nn split |
| if ("no-bok".equals(canonLanguage)) { |
| canonLanguage = "nb"; |
| canonVariant = null; |
| } else if ("no-nyn".equals(canonLanguage)) { |
| canonLanguage = "nn"; |
| canonVariant = null; |
| } else if ("no".equals(canonLanguage)) { |
| if (canonVariant == null || "BOKMAL".equals(canonVariant)) { |
| canonLanguage = "nb"; |
| canonVariant = null; |
| } else if ("NYNORSK".equals(canonVariant)) { |
| canonLanguage = "nn"; |
| canonVariant = null; |
| } |
| } |
| // Remove any default script for the language |
| if (canonScript != null && canonScript.equals( |
| DefaultLanguageScripts.getDefaultScript(canonLanguage, canonRegion))) { |
| canonScript = null; |
| } |
| return factory.fromComponents(canonLanguage, canonScript, canonRegion, |
| canonVariant); |
| } |
| |
| public List<GwtLocale> getCompleteSearchList() { |
| // TODO(jat): base order on distance from the initial locale, such as the |
| // draft proposal at: |
| // http://cldr.unicode.org/development/design-proposals/languagedistance |
| // This will ensure that zh_Hant will come before zh in the search list for |
| // zh_TW, and pa_Arab to come before pa in the search list for pa_PK. |
| synchronized (cacheLock) { |
| if (cachedSearchList == null) { |
| cachedSearchList = new ArrayList<GwtLocale>(); |
| Set<GwtLocale> seen = new HashSet<GwtLocale>(); |
| String initialScript = script; |
| if (initialScript == null) { |
| initialScript = DefaultLanguageScripts.getDefaultScript(language, |
| region); |
| } |
| List<GwtLocale> thisGroup = new ArrayList<GwtLocale>(); |
| if (initialScript != null) { |
| // Make sure the default script is listed first in the search list, |
| // which ensures that zh_Hant appears before zh in the search list for |
| // zh_TW. |
| thisGroup.add(factory.fromComponents(language, initialScript, region, |
| variant)); |
| } |
| thisGroup.add(this); |
| seen.addAll(thisGroup); |
| GwtLocale defLocale = factory.getDefault(); |
| seen.add(defLocale); |
| while (!thisGroup.isEmpty()) { |
| cachedSearchList.addAll(thisGroup); |
| List<GwtLocale> nextGroup = new ArrayList<GwtLocale>(); |
| for (GwtLocale locale : thisGroup) { |
| List<GwtLocale> aliases = locale.getAliases(); |
| aliases = filterBadScripts(aliases, initialScript); |
| List<GwtLocale> work = new ArrayList<GwtLocale>(aliases); |
| work.removeAll(seen); |
| nextGroup.addAll(work); |
| seen.addAll(work); |
| work.clear(); |
| if (locale.getRegion() != null) { |
| addImmediateParentRegions(factory, locale, work); |
| } else if (locale.getVariant() != null) { |
| work.add(factory.fromComponents(locale.getLanguage(), |
| locale.getScript(), null, null)); |
| } else if (locale.getScript() != null) { |
| work.add(factory.fromComponents(locale.getLanguage(), null, null, |
| null)); |
| } |
| work.removeAll(seen); |
| nextGroup.addAll(work); |
| seen.addAll(work); |
| } |
| thisGroup = nextGroup; |
| } |
| if (!isDefault()) { |
| cachedSearchList.add(defLocale); |
| } |
| cachedSearchList = Collections.unmodifiableList(cachedSearchList); |
| } |
| return cachedSearchList; |
| } |
| } |
| |
| /** |
| * Return a list of locales to search for, in order of preference. The |
| * current locale is always first on the list. Aliases are not included |
| * in the list -- use {@link #getAliases} to expand those. |
| * |
| * @return inheritance list |
| */ |
| public List<GwtLocale> getInheritanceChain() { |
| List<GwtLocale> inherits = new ArrayList<GwtLocale>(); |
| inherits.add(this); |
| if (variant != null) { |
| inherits.add(factory.fromComponents(language, script, region, null)); |
| } |
| if (region != null) { |
| addParentRegionLocales(factory, inherits, language, script, region, |
| null); |
| inherits.add(factory.fromComponents(language, script, null, null)); |
| } |
| if (script != null) { |
| inherits.add(factory.fromComponents(language, null, region, null)); |
| addParentRegionLocales(factory, inherits, language, null, region, null); |
| if (region != null) { |
| inherits.add(factory.fromComponents(language, null, null, null)); |
| } |
| } |
| if (language != null) { |
| inherits.add(factory.fromComponents(null, null, null, null)); |
| } |
| return inherits; |
| } |
| |
| public String getLanguage() { |
| return language; |
| } |
| |
| public String getLanguageNotNull() { |
| return language == null ? "" : language; |
| } |
| |
| public String getRegion() { |
| return region; |
| } |
| |
| public String getRegionNotNull() { |
| return region == null ? "" : region; |
| } |
| |
| public String getScript() { |
| return script; |
| } |
| |
| public String getScriptNotNull() { |
| return script == null ? "" : script; |
| } |
| |
| public String getVariant() { |
| return variant; |
| } |
| |
| public String getVariantNotNull() { |
| return variant == null ? "" : variant; |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = ((language == null) ? 0 : language.hashCode()); |
| result = 37 * result + ((region == null) ? 0 : region.hashCode()); |
| result = 43 * result + ((script == null) ? 0 : script.hashCode()); |
| result = 53 * result + ((variant == null) ? 0 : variant.hashCode()); |
| return result; |
| } |
| |
| /** |
| * Return true if this locale inherits from the specified locale. Note that |
| * locale.inheritsFrom(locale) is false -- if you want that to be true, you |
| * should just use locale.getInheritanceChain().contains(x). |
| * |
| * @param parent locale to test against |
| * @return true if parent is an ancestor of this locale |
| */ |
| public boolean inheritsFrom(GwtLocale parent) { |
| if (equals(parent)) { |
| return false; |
| } |
| return getInheritanceChain().contains(parent); |
| } |
| |
| public boolean isDefault() { |
| return language == null; |
| } |
| |
| @Override |
| public String toString() { |
| if (language == null) { |
| return DEFAULT_LOCALE; |
| } |
| return getAsString(); |
| } |
| |
| /** |
| * Checks if this locale uses the same script as another locale, taking into |
| * account default scripts. |
| * |
| * @param other |
| * @return true if the scripts are the same |
| */ |
| public boolean usesSameScript(GwtLocale other) { |
| String myScript = script != null ? script : DefaultLanguageScripts.getDefaultScript(language, |
| region); |
| String otherScript = other.getScript() != null ? other.getScript() |
| : DefaultLanguageScripts.getDefaultScript(other.getLanguage(), other.getRegion()); |
| // two locales with an unspecified script and no default for the language |
| // match only if the language is the same |
| if (myScript == null) { |
| return equalsNullCheck(language, other.getLanguage()); |
| } else { |
| return myScript.equals(otherScript); |
| } |
| } |
| |
| /** |
| * Remove aliases which are incompatible with the initial script provided or |
| * defaulted in a locale. |
| * |
| * @param aliases |
| * @param initialScript |
| * @return copy of aliases with incompatible scripts removed |
| */ |
| private List<GwtLocale> filterBadScripts(List<GwtLocale> aliases, |
| String initialScript) { |
| if (initialScript == null) { |
| return aliases; |
| } |
| List<GwtLocale> result = new ArrayList<GwtLocale>(); |
| for (GwtLocale alias : aliases) { |
| String aliasScript = alias.getScript(); |
| if (aliasScript == null || aliasScript.equals(initialScript)) { |
| result.add(alias); |
| } |
| } |
| return result; |
| } |
| } |