/* Locales.java Purpose: Description: History: 2002/01/25 11:58:34, Create, Tom M. Yeh. Copyright (C) 2001 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.util; import java.util.LinkedList; import java.util.Map; import java.util.HashMap; import java.util.Locale; import java.util.Collection; import org.zkoss.lang.Objects; /** * The locale relevant utilities. * * @author tomyeh */ public class Locales { private final static InheritableThreadLocal<Locale> _thdLocale = new InheritableThreadLocal<Locale>(); /** Returns the current locale; never null. * This is the locale that every other objects shall use, * unless they have special consideration. * * <p>Default: If {@link #setThreadLocal} was called with non-null, * the value is returned. Otherwise, Locale.getDefault() is returned, */ public static final Locale getCurrent() { final Locale l = _thdLocale.get(); return l != null ? l: Locale.getDefault(); } /** Returns whether the current locale ({@link #getCurrent}) belongs * to the specified language and/or country. * * @param lang the language code, e.g., en and zh. Ignored if null. * @param country the country code, e.g., US. Ignored if null. * If empty, it means no country code at all. */ public static final boolean testCurrent(String lang, String country) { final Locale l = getCurrent(); return (lang == null || lang.equals(l.getLanguage())) && (country == null || country.equals(l.getCountry())); } /** * Sets the locale for the current thread only. * * <p>Each thread could have an independent locale, called * the thread locale. * * <p>When Invoking this method under a thread that serves requests, * remember to clean up the setting upon completing each request. * * <pre><code>Locale old = Locales.setThreadLocal(newValue); *try { * ... *} finally { * Locales.setThreadLocal(old); *}</code></pre> * * @param locale the thread locale; null to denote no thread locale * @return the previous thread locale */ public static final Locale setThreadLocal(Locale locale) { final Locale old = _thdLocale.get(); _thdLocale.set(locale); return old; } /** * Returns the locale defined by {@link #setThreadLocal}. * * @since 3.0.0 * @see #getCurrent */ public static final Locale getThreadLocal() { return _thdLocale.get(); } /** Converts a string that consists of language, country and variant * to a locale. * * <p>The separator between language, country and variant * is customizable, and whitespaces are ignored, e.g., * "zh_TW" and "zh, TW". * * <p>Thus, locale.equals(Locales.getLocale(locale.toString(), '_')). * * @param localeString the locale in string; null is OK * @param separator the separator; ((char)0) means to decide automatically * (either ',' or '_') * @return the locale or null if locale is null or empty */ public static final Locale getLocale(String localeString, char separator) { if (localeString == null) return null; if (separator == (char)0) separator = localeString.indexOf('_') >= 0 ? '_' : ','; LinkedList<String> list = new LinkedList<String>(); CollectionsX.parse(list, localeString, separator); String lang = "", cnt = "", var = ""; switch (list.size()) { case 0: return null; default: assert(list.size() <= 3); var = list.get(2); case 2: cnt = list.get(1); if (cnt.length() != 2) throw new IllegalArgumentException("Not a valid country: "+localeString); case 1: lang = list.get(0); if (lang.length() != 2) throw new IllegalArgumentException("Not a valid language: "+localeString); } return getLocale(new Locale(lang, cnt, var)); } /** Converts a string that consists of language, country and variant * to a locale. * * <p>A shortcut: getLocale(localeString, (char)0). */ public static final Locale getLocale(String localeString) { return getLocale(localeString, (char)0); } /** Converts a Locale to one of them being used before. * To save memory (since locale is used frequently), it is suggested * to pass thru this method after creating a new instance of Locale.<br> * Example, getLocale(new Locale(...)). * * <p>This method first look for any locale */ public static final Locale getLocale(Locale locale) { final Locale l = _founds.get(locale); if (l != null) return l; synchronized (_founds) { final Map<Locale, Locale> fs = new HashMap<Locale, Locale>(_founds); fs.put(locale, locale); _founds = fs; } return locale; } /** Locales that are found so far. */ private static Map<Locale, Locale> _founds = new HashMap<Locale, Locale>(16); static { final Locale[] ls = new Locale[] { Locale.TRADITIONAL_CHINESE, Locale.SIMPLIFIED_CHINESE, Locale.ENGLISH, Locale.US, Locale.JAPAN, Locale.JAPANESE, Locale.KOREA, Locale.KOREAN, Locale.FRANCE, Locale.FRENCH, Locale.GERMANY, Locale.GERMAN, Locale.CHINESE }; for (int j = 0; j < ls.length; ++j) _founds.put(ls[j], ls[j]); } /** Returns any occurrence of the specified Locale or any its fallback * in the value collection, or null if not found. * By fallback, we mean will try without variant and country. * Example, if locale is zh_TW, it will try zh_TW and then zh. */ public static Locale getByFallback(Collection<Locale> values, Locale locale) { if (values.contains(locale)) return locale; final String lang = locale.getLanguage(); final String cnty = locale.getCountry(); final String var = locale.getVariant(); if (var != null && var.length() > 0) { locale = new Locale(lang, cnty); if (values.contains(locale)) return locale; } if (cnty != null && cnty.length() > 0) { locale = new Locale(lang, ""); if (values.contains(locale)) return locale; } //search the first one that matches partially Locale rtn = null; for (Locale l: values) { if (l.getLanguage().equals(lang)) { //case 1: it matches all but the last element -> done if (var == null || var.length() == 0 || Objects.equals(l.getCountry(), cnty))//country might null return l; //case 2: it matches only language, we seeek for any case 1 if (rtn == null) rtn = l; } } return rtn; } /** Returns the index of '_' preceding the country part, starting from j. * It is similar to s.indexOf('_', j), except it detects country part * (which must be only two letter in lower cases. */ public static int indexOfUnderline(String s, int j) { int k, last = s.length() - 2; for (;(k = s.indexOf('_', j)) >= 0 && k < last; j = k + 1) { char cc = s.charAt(k + 1); if (cc < 'a' || cc > 'z') continue; //not found cc = s.charAt(k + 2); if (cc < 'a' || cc > 'z') continue; //not found cc = s.charAt(k + 3); if (cc < 'a' || cc > 'z') return k; //found } return -1; } /** Returns the index of '_' preceding the country part. * It is similar to s.indexOf('_'), except it detects country part * (which must be only two letter in lower cases. */ public static int indexOfUnderline(String s) { return indexOfUnderline(s, 0); } }