/* * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * 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 org.jivesoftware.util; import java.text.DateFormat; import java.text.Format; import java.text.MessageFormat; import java.text.NumberFormat; import java.text.ParseException; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.container.Plugin; import org.jivesoftware.openfire.container.PluginManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A set of methods for retrieving and converting locale specific strings and numbers. * * @author Jive Software */ public class LocaleUtils { private static final Logger Log = LoggerFactory.getLogger(LocaleUtils.class); private static final Map<Locale, String[][]> timeZoneLists = new ConcurrentHashMap<>(); // The basename to use for looking up the appropriate resource bundles // TODO - extract this out into a test that grabs the resource name from JiveGlobals // TODO and defaults to openfire_i18n if nothing set. private static final String resourceBaseName = "openfire_i18n"; private LocaleUtils() { } /** * Converts a locale string like "en", "en_US" or "en_US_win" to a Java * locale object. If the conversion fails, null is returned. * * @param localeCode the locale code for a Java locale. See the {@link java.util.Locale} * class for more details. * @return The Java Locale that matches the locale code, or <tt>null</tt>. */ public static Locale localeCodeToLocale(String localeCode) { Locale locale = null; if (localeCode != null) { String language = null; String country = null; String variant = null; StringTokenizer tokenizer = new StringTokenizer(localeCode, "_"); if (tokenizer.hasMoreTokens()) { language = tokenizer.nextToken(); if (tokenizer.hasMoreTokens()) { country = tokenizer.nextToken(); if (tokenizer.hasMoreTokens()) { variant = tokenizer.nextToken(); } } } locale = new Locale(language, ((country != null) ? country : ""), ((variant != null) ? variant : "")); } return locale; } // The list of supported timezone ids. The list tries to include all of the relevant // time zones for the world without any extraneous zones. private static String[] timeZoneIds = new String[]{"GMT", "Pacific/Apia", "HST", "AST", "America/Los_Angeles", "America/Phoenix", "America/Mazatlan", "America/Denver", "America/Belize", "America/Chicago", "America/Mexico_City", "America/Regina", "America/Bogota", "America/New_York", "America/Indianapolis", "America/Halifax", "America/Caracas", "America/Santiago", "America/St_Johns", "America/Sao_Paulo", "America/Buenos_Aires", "America/Godthab", "Atlantic/South_Georgia", "Atlantic/Azores", "Atlantic/Cape_Verde", "Africa/Casablanca", "Europe/Dublin", "Europe/Berlin", "Europe/Belgrade", "Europe/Paris", "Europe/Warsaw", "ECT", "Europe/Athens", "Europe/Bucharest", "Africa/Cairo", "Africa/Harare", "Europe/Helsinki", "Asia/Jerusalem", "Asia/Baghdad", "Asia/Kuwait", "Europe/Moscow", "Africa/Nairobi", "Asia/Tehran", "Asia/Muscat", "Asia/Baku", "Asia/Kabul", "Asia/Yekaterinburg", "Asia/Karachi", "Asia/Calcutta", "Asia/Katmandu", "Asia/Almaty", "Asia/Dhaka", "Asia/Colombo", "Asia/Rangoon", "Asia/Bangkok", "Asia/Krasnoyarsk", "Asia/Hong_Kong", "Asia/Irkutsk", "Asia/Kuala_Lumpur", "Australia/Perth", "Asia/Taipei", "Asia/Tokyo", "Asia/Seoul", "Asia/Yakutsk", "Australia/Adelaide", "Australia/Darwin", "Australia/Brisbane", "Australia/Sydney", "Pacific/Guam", "Australia/Hobart", "Asia/Vladivostok", "Pacific/Noumea", "Pacific/Auckland", "Pacific/Fiji", "Pacific/Tongatapu" }; // A mapping from the supported timezone ids to friendly english names. private static final Map<String, String> nameMap = new HashMap<>(); static { nameMap.put(timeZoneIds[0], "International Date Line West"); nameMap.put(timeZoneIds[1], "Midway Island, Samoa"); nameMap.put(timeZoneIds[2], "Hawaii"); nameMap.put(timeZoneIds[3], "Alaska"); nameMap.put(timeZoneIds[4], "Pacific Time (US & Canada); Tijuana"); nameMap.put(timeZoneIds[5], "Arizona"); nameMap.put(timeZoneIds[6], "Chihuahua, La Pax, Mazatlan"); nameMap.put(timeZoneIds[7], "Mountain Time (US & Canada)"); nameMap.put(timeZoneIds[8], "Central America"); nameMap.put(timeZoneIds[9], "Central Time (US & Canada)"); nameMap.put(timeZoneIds[10], "Guadalajara, Mexico City, Monterrey"); nameMap.put(timeZoneIds[11], "Saskatchewan"); nameMap.put(timeZoneIds[12], "Bogota, Lima, Quito"); nameMap.put(timeZoneIds[13], "Eastern Time (US & Canada)"); nameMap.put(timeZoneIds[14], "Indiana (East)"); nameMap.put(timeZoneIds[15], "Atlantic Time (Canada)"); nameMap.put(timeZoneIds[16], "Caracas, La Paz"); nameMap.put(timeZoneIds[17], "Santiago"); nameMap.put(timeZoneIds[18], "Newfoundland"); nameMap.put(timeZoneIds[19], "Brasilia"); nameMap.put(timeZoneIds[20], "Buenos Aires, Georgetown"); nameMap.put(timeZoneIds[21], "Greenland"); nameMap.put(timeZoneIds[22], "Mid-Atlantic"); nameMap.put(timeZoneIds[23], "Azores"); nameMap.put(timeZoneIds[24], "Cape Verde Is."); nameMap.put(timeZoneIds[25], "Casablanca, Monrovia"); nameMap.put(timeZoneIds[26], "Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London"); nameMap.put(timeZoneIds[27], "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"); nameMap.put(timeZoneIds[28], "Belgrade, Bratislava, Budapest, Ljubljana, Prague"); nameMap.put(timeZoneIds[29], "Brussels, Copenhagen, Madrid, Paris"); nameMap.put(timeZoneIds[30], "Sarajevo, Skopje, Warsaw, Zagreb"); nameMap.put(timeZoneIds[31], "West Central Africa"); nameMap.put(timeZoneIds[32], "Athens, Istanbul, Minsk"); nameMap.put(timeZoneIds[33], "Bucharest"); nameMap.put(timeZoneIds[34], "Cairo"); nameMap.put(timeZoneIds[35], "Harare, Pretoria"); nameMap.put(timeZoneIds[36], "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"); nameMap.put(timeZoneIds[37], "Jerusalem"); nameMap.put(timeZoneIds[38], "Baghdad"); nameMap.put(timeZoneIds[39], "Kuwait, Riyadh"); nameMap.put(timeZoneIds[40], "Moscow, St. Petersburg, Volgograd"); nameMap.put(timeZoneIds[41], "Nairobi"); nameMap.put(timeZoneIds[42], "Tehran"); nameMap.put(timeZoneIds[43], "Abu Dhabi, Muscat"); nameMap.put(timeZoneIds[44], "Baku, Tbilisi, Muscat"); nameMap.put(timeZoneIds[45], "Kabul"); nameMap.put(timeZoneIds[46], "Ekaterinburg"); nameMap.put(timeZoneIds[47], "Islamabad, Karachi, Tashkent"); nameMap.put(timeZoneIds[48], "Chennai, Kolkata, Mumbai, New Dehli"); nameMap.put(timeZoneIds[49], "Kathmandu"); nameMap.put(timeZoneIds[50], "Almaty, Novosibirsk"); nameMap.put(timeZoneIds[51], "Astana, Dhaka"); nameMap.put(timeZoneIds[52], "Sri Jayawardenepura"); nameMap.put(timeZoneIds[53], "Rangoon"); nameMap.put(timeZoneIds[54], "Bangkok, Hanoi, Jakarta"); nameMap.put(timeZoneIds[55], "Krasnoyarsk"); nameMap.put(timeZoneIds[56], "Beijing, Chongqing, Hong Kong, Urumqi"); nameMap.put(timeZoneIds[57], "Irkutsk, Ulaan Bataar"); nameMap.put(timeZoneIds[58], "Kuala Lumpur, Singapore"); nameMap.put(timeZoneIds[59], "Perth"); nameMap.put(timeZoneIds[60], "Taipei"); nameMap.put(timeZoneIds[61], "Osaka, Sapporo, Tokyo"); nameMap.put(timeZoneIds[62], "Seoul"); nameMap.put(timeZoneIds[63], "Yakutsk"); nameMap.put(timeZoneIds[64], "Adelaide"); nameMap.put(timeZoneIds[65], "Darwin"); nameMap.put(timeZoneIds[66], "Brisbane"); nameMap.put(timeZoneIds[67], "Canberra, Melbourne, Sydney"); nameMap.put(timeZoneIds[68], "Guam, Port Moresby"); nameMap.put(timeZoneIds[69], "Hobart"); nameMap.put(timeZoneIds[70], "Vladivostok"); nameMap.put(timeZoneIds[71], "Magadan, Solomon Is., New Caledonia"); nameMap.put(timeZoneIds[72], "Auckland, Wellington"); nameMap.put(timeZoneIds[73], "Fiji, Kamchatka, Marshall Is."); nameMap.put(timeZoneIds[74], "Nuku'alofa"); } /** * Returns a list of all available time zone's as a String [][]. The first * entry in each list item is the timeZoneID, and the second is the * display name. * <p> * The list of time zones attempts to be inclusive of all of the worlds * zones while being as concise as possible. For "en" language locales * the name is a friendly english name. For non-"en" language locales * the standard JDK name is used for the given Locale. The GMT+/- time * is also included for readability.</p> * * @return a list of time zones, as a tuple of the zime zone ID, and its * display name. */ public static String[][] getTimeZoneList() { Locale jiveLocale = JiveGlobals.getLocale(); String[][] timeZoneList = timeZoneLists.get(jiveLocale); if (timeZoneList == null) { String[] timeZoneIDs = timeZoneIds; // Now, create String[][] using the unique zones. timeZoneList = new String[timeZoneIDs.length][2]; for (int i = 0; i < timeZoneList.length; i++) { String zoneID = timeZoneIDs[i]; timeZoneList[i][0] = zoneID; timeZoneList[i][1] = getTimeZoneName(zoneID, jiveLocale); } // Add the new list to the map of locales to lists timeZoneLists.put(jiveLocale, timeZoneList); } return timeZoneList; } /** * Returns the display name for a time zone. The display name is the name * specified by the Java TimeZone class for non-"en" locales or a friendly english * name for "en", with the addition of the GMT offset * for human readability. * * @param zoneID the time zone to get the name for. * @param locale the locale to use. * @return the display name for the time zone. */ public static String getTimeZoneName(String zoneID, Locale locale) { TimeZone zone = TimeZone.getTimeZone(zoneID); StringBuilder buf = new StringBuilder(); // Add in the GMT part to the name. First, figure out the offset. int offset = zone.getRawOffset(); if (zone.inDaylightTime(new Date()) && zone.useDaylightTime()) { offset += (int)JiveConstants.HOUR; } buf.append('('); if (offset < 0) { buf.append("GMT-"); } else { buf.append("GMT+"); } offset = Math.abs(offset); int hours = offset / (int)JiveConstants.HOUR; int minutes = (offset % (int)JiveConstants.HOUR) / (int)JiveConstants.MINUTE; buf.append(hours).append(':'); if (minutes < 10) { buf.append('0').append(minutes); } else { buf.append(minutes); } buf.append(") "); // Use a friendly english timezone name if the locale is en, otherwise use the timezone id if ("en".equals(locale.getLanguage())) { String name = nameMap.get(zoneID); if (name == null) { name = zoneID; } buf.append(name); } else { buf.append( zone.getDisplayName(true, TimeZone.LONG, locale).replace('_', ' ').replace('/', ' ')); } return buf.toString(); } /** * Returns the specified resource bundle, which is a properties file * that aids in localization of skins. This method is handy since it * uses the class loader that other Jive classes are loaded from (hence, * it can load bundles that are stored in jive.jar). * * @param baseName the name of the resource bundle to load. * @param locale the desired Locale. * @return the specified resource bundle, if it exists. */ public static ResourceBundle getResourceBundle(String baseName, Locale locale) { return ResourceBundle.getBundle(baseName, locale); } /** * Returns an internationalized string loaded from a resource bundle. * The locale used will be the locale specified by JiveGlobals.getLocale(). * * @param key the key to use for retrieving the string from the * appropriate resource bundle. * @return the localized string. */ public static String getLocalizedString(String key) { Locale locale = JiveGlobals.getLocale(); try { ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale); return getLocalizedString(key, locale, null, bundle); } catch (MissingResourceException mre) { Log.error(mre.getMessage()); return key; } } /** * Returns an internationalized string loaded from a resource bundle using * the passed in Locale. * * @param key the key to use for retrieving the string from the * appropriate resource bundle. * @param locale the locale to use for retrieving the appropriate * locale-specific string. * @return the localized string. */ public static String getLocalizedString(String key, Locale locale) { ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale); return getLocalizedString(key, locale, null, bundle); } /** * Returns an internationalized string loaded from a resource bundle using * the locale specified by JiveGlobals.getLocale() substituting the passed * in arguments. Substitution is handled using the * {@link java.text.MessageFormat} class. * * @param key the key to use for retrieving the string from the * appropriate resource bundle. * @param arguments a list of objects to use which are formatted, then * inserted into the pattern at the appropriate places. * @return the localized string. */ public static String getLocalizedString(String key, List<?> arguments) { Locale locale = JiveGlobals.getLocale(); ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale); return getLocalizedString(key, locale, arguments, bundle); } /** * Returns an internationalized string loaded from a resource bundle from the passed * in plugin. If the plugin name is <tt>null</tt>, the key will be looked up using * the standard resource bundle. * * @param key the key to use for retrieving the string from the * appropriate resource bundle. * @param pluginName the name of the plugin to load the require resource bundle from. * @return the localized string. */ public static String getLocalizedString(String key, String pluginName) { return getLocalizedString(key, pluginName, null); } /** * Returns an internationalized string loaded from a resource bundle from the passed * in plugin. If the plugin name is <tt>null</tt>, the key will be looked up using * the standard resource bundle. * * @param key the key to use for retrieving the string from the * appropriate resource bundle. * @param pluginName the name of the plugin to load the require resource bundle from. * @param arguments a list of objects to use which are formatted, then * inserted into the pattern at the appropriate places. * @return the localized string. */ public static String getLocalizedString(String key, String pluginName, List arguments) { return getLocalizedString(key, pluginName, arguments, null, false); } /** * Returns an internationalized string loaded from a resource bundle from * the passed in plugin, using the passed in Locale. * * If the plugin name is <tt>null</tt>, the key will be looked up using the * standard resource bundle. * * If the locale is <tt>null</tt>, the Jive Global locale will be used. * * @param key * the key to use for retrieving the string from the appropriate * resource bundle. * @param pluginName * the name of the plugin to load the require resource bundle * from. * @param arguments * a list of objects to use which are formatted, then inserted * into the pattern at the appropriate places. * @param locale * the locale to use for retrieving the appropriate * locale-specific string. * @param fallback * if <tt>true</tt>, the global locale used by Openfire will be * used if the requested locale is not available) * @return the localized string. */ public static String getLocalizedString(String key, String pluginName, List<?> arguments, Locale locale, boolean fallback) { if (pluginName == null) { return getLocalizedString(key, arguments); } if (locale == null) { locale = JiveGlobals.getLocale(); } String i18nFile = getI18nFile(pluginName); // Retrieve classloader from pluginName. final XMPPServer xmppServer = XMPPServer.getInstance(); PluginManager pluginManager = xmppServer.getPluginManager(); Plugin plugin = pluginManager.getPlugin(pluginName); if (plugin == null) { throw new NullPointerException("Plugin could not be located: " + pluginName); } ClassLoader pluginClassLoader = pluginManager.getPluginClassloader(plugin); try { ResourceBundle bundle = ResourceBundle.getBundle(i18nFile, locale, pluginClassLoader); return getLocalizedString(key, locale, arguments, bundle); } catch (MissingResourceException mre) { Locale jivesLocale = JiveGlobals.getLocale(); if (fallback && !jivesLocale.equals(locale)) { Log.info("Could not find the requested locale. Falling back to default locale.", mre); return getLocalizedString(key, pluginName, arguments, jivesLocale, false); } Log.error(mre.getMessage(), mre); return key; } } /** * Retrieve the <code>ResourceBundle</code> that is used with this plugin. * * @param pluginName the name of the plugin. * @return the ResourceBundle used with this plugin. * @throws Exception thrown if an exception occurs. */ public static ResourceBundle getPluginResourceBundle(String pluginName) throws Exception { final Locale locale = JiveGlobals.getLocale(); String i18nFile = getI18nFile(pluginName); // Retrieve classloader from pluginName. final XMPPServer xmppServer = XMPPServer.getInstance(); PluginManager pluginManager = xmppServer.getPluginManager(); Plugin plugin = pluginManager.getPlugin(pluginName); if (plugin == null) { throw new NullPointerException("Plugin could not be located."); } ClassLoader pluginClassLoader = pluginManager.getPluginClassloader(plugin); return ResourceBundle.getBundle(i18nFile, locale, pluginClassLoader); } private static String getI18nFile(String pluginName) { return pluginName.replace('.', '_') + "_i18n"; } /** * Returns an internationalized string loaded from a resource bundle using * the passed in Locale substituting the passed in arguments. Substitution * is handled using the {@link MessageFormat} class. * * @param key the key to use for retrieving the string from the * appropriate resource bundle. * @param locale the locale to use for retrieving the appropriate * locale-specific string. * @param arguments a list of objects to use which are formatted, then * inserted into the pattern at the appropriate places. * @param bundle The resource bundle from which to return the localized * string. * @return the localized string. */ public static String getLocalizedString(String key, Locale locale, List arguments, ResourceBundle bundle) { if (key == null) { throw new NullPointerException("Key cannot be null"); } if (locale == null) { locale = JiveGlobals.getLocale(); } String value; // See if the bundle has a value try { // The jdk caches resource bundles on it's own, so we won't bother. value = bundle.getString(key); // perform argument substitutions if (arguments != null) { MessageFormat messageFormat = new MessageFormat(""); messageFormat.setLocale(bundle.getLocale()); messageFormat.applyPattern(value); try { // This isn't fool-proof, but it's better than nothing // The idea is to try and convert strings into the // types of objects that the formatters expects // i.e. Numbers and Dates Format[] formats = messageFormat.getFormats(); for (int i = 0; i < formats.length; i++) { Format format = formats[i]; if (format != null) { if (format instanceof DateFormat) { if (arguments.size() > i) { Object val = arguments.get(i); if (val instanceof String) { DateFormat dateFmt = (DateFormat)format; try { val = dateFmt.parse((String)val); arguments.set(i, val); } catch (ParseException e) { Log.error(e.getMessage(), e); } } } } else if (format instanceof NumberFormat) { if (arguments.size() > i) { Object val = arguments.get(i); if (val instanceof String) { NumberFormat nbrFmt = (NumberFormat)format; try { val = nbrFmt.parse((String)val); arguments.set(i, val); } catch (ParseException e) { Log.error(e.getMessage(), e); } } } } } } value = messageFormat.format(arguments.toArray()); } catch (IllegalArgumentException e) { Log.error("Unable to format resource string for key: " + key + ", argument type not supported"); value = ""; } } } catch (java.util.MissingResourceException mre) { Log.warn("Missing resource for key: " + key + " in locale " + locale.toString()); value = ""; } return value; } /** * Returns an internationalized String representation of the number using * the default locale. * * @param number the number to format. * @return an internationalized String representation of the number. */ public static String getLocalizedNumber(long number) { return NumberFormat.getInstance().format(number); } /** * Returns an internationalized String representation of the number using * the specified locale. * * @param number the number to format. * @param locale the locale to use for formatting. * @return an internationalized String representation of the number. */ public static String getLocalizedNumber(long number, Locale locale) { return NumberFormat.getInstance(locale).format(number); } /** * Returns an internationalized String representation of the number using * the default locale. * * @param number the number to format. * @return an internationalized String representation of the number. */ public static String getLocalizedNumber(double number) { return NumberFormat.getInstance().format(number); } /** * Returns an internationalized String representation of the number using * the specified locale. * * @param number the number to format. * @param locale the locale to use for formatting. * @return an internationalized String representation of the number. */ public static String getLocalizedNumber(double number, Locale locale) { return NumberFormat.getInstance(locale).format(number); } }