/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2011, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.swing.locale; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.util.logging.Logging; /** * Provides localized text strings to GUI elements. This class hides most of the fiddly bits * associated with {@linkplain Locale} and {@linkplain ResourceBundle} from other gt-swing classes. * You do not create instances of this class, it is basically just a wrapper around static data. * Text strings are stored in properties files as per standard Java internationalization. All files * are located in the {@code org/geotools/swing/locale} directory of the swing module. * <p> * * An application wishing to display GUI elements in a non-default locale must call either * {@linkplain #setLocale(Locale)} or {@linkplain #setLocale(List)} before constructing any GUI * elements which display localized text. At present, this class does not support switching locales * for components that have already been constructed. * <p> * * Clients retrieve text strings by specifying the base name of the relevant properties file and a * key as shown here: * * <pre><code> * String localizedText = LocaleUtils.getValue("CursorTool", "ZoomInTool"); * </code></pre> * * Adding support for a new language simply involves adding the new locale to all or some of the * properties files. If the locale is only provided for some files, it will be recorded by as * partially supported and used where available. * <p> * * You can set a single working locale with {@linkplain #setLocale(Locale)}. If the specified * locale is only partially supported, the {@linkplain #getValue(String, String)} method will fall * back to the default {@linkplain Locale#ROOT} when retrieving text strings lacking this locale. * Alternatively, you can specify a list of locales in order of preference using the {@linkplain * #setLocale(List)} method. * <p> * * If the {@code setLocale} method is not called, the locale defaults to * {@linkplain Locale#ROOT} (which is English language in the properties files distributed * with GeoTools). * * @author Michael Bedward * @since 8.0 * * @source $URL$ * @version $Id$ */ public class LocaleUtils { private static final Logger LOGGER = Logging.getLogger("org.geotools.swing"); private static final String PREFIX = "org/geotools/swing/locale/"; /* * Represents a supported locale. */ private static class LocaleInfo { Locale locale; boolean fullySupported; public LocaleInfo(Locale locale, boolean fullySupported) { this.locale = locale; this.fullySupported = fullySupported; } } private static final List<LocaleInfo> supportedLocales; /* * Locales in order of client preference. Always has Locale.ROOT * as the final element. */ private static final List<Locale> workingLocales; /* * Cached resource bundles. Each bundle will be that of the * highest available preference locale. */ private static final Map<String, ResourceBundle> bundles = new ConcurrentHashMap<String, ResourceBundle>(); private static final ReadWriteLock lock = new ReentrantReadWriteLock(); /* * Scan properties files for GUI text strings to record the * locales present and, for each, whether it is fully or partially * supported. */ static { try { supportedLocales = loadLocaleInfo(); workingLocales = new ArrayList<Locale>(); workingLocales.add(Locale.ROOT); } catch (IOException ex) { throw new RuntimeException("Unable to read the locale directory", ex); } } /** * Private constructor to prevent clients creating instances. */ private LocaleUtils() {} /** * Tests whether the given {@code Locale} is fully supported (ie. has been * provided for every GUI text string properties file. * * @param locale the locale * @return {@code true} if fully supported; {@code false} if partially or * not supported */ public static boolean isFullySupportedLocale(Locale locale) { for (LocaleInfo li : supportedLocales) { if (li.locale.equals(locale)) { return li.fullySupported; } } return false; } /** * Tests whether the given {@code Locale} is supported. A locale is treated * as supported if it has been provided for at least one GUI text string * properties file. * * @param locale the locale * @return {@code true} if the locale is at least partially supported * * @see #isFullySupportedLocale(Locale) */ public static boolean isSupportedLocale(Locale locale) { for (LocaleInfo li : supportedLocales) { if (li.locale.equals(locale)) { return true; } } return false; } /** * Sets a single preferred locale for text string retrieval. Any text * strings for which this locale has not been provided will fall back * to the default {@linkplain Locale#ROOT} (English language). If * {@code preferredLocale} is {@code null} the working locale will be set * to {@linkplain Locale#ROOT}. * * @param preferredLocale the locale */ public static void setLocale(Locale preferredLocale) { setLocale(Collections.singletonList(preferredLocale)); } /** * Sets the preferred locales for text string retrieval. The input list * is ordered from highest (first element) to lowest (last element) preference. * There is no need to include {@linkplain Locale#ROOT} in the input list. * It will be added automatically. If {@code preferredLocales} is {@code null} * or empty, the working locale will be set to {@linkplain Locale#ROOT}. * * @param preferredLocales locales in descending order of preference */ public static void setLocale(List<Locale> preferredLocales) { lock.writeLock().lock(); try { filterAndCopy(preferredLocales); bundles.clear(); } finally { lock.writeLock().unlock(); } } /** * Retrieves a GUI text string identified by the base name of a * properties file and a key within that file. * <pre><code> * String localName = LocaleUtils.getValue("CursorTool", "ZoomIn"); * </code></pre> * * @param baseFileName base name of the properties file containing the text string * @param key key for the text string * @return the localized text string * * @throws MissingResourceException if the {@code baseFileName:key} pair * cannot be found * @throws IllegalArgumentException if either argument is {@code null} */ public static String getValue(String baseFileName, String key) { lock.readLock().lock(); try { if (baseFileName == null || key == null) { throw new IllegalArgumentException("arguments must not be null"); } return getBundle(baseFileName).getString(key); } finally { lock.readLock().unlock(); } } /** * Loads a {@linkplain ResourceBundle} into the cache according to * the current order of locale preferences. * * @param resBundleName bundle name * @return the resource bundle * @throws MissingResourceException if the bundle is not found */ private static ResourceBundle getBundle(String resBundleName) { ResourceBundle rb = bundles.get(resBundleName); if (rb == null) { rb = ResourceBundle.getBundle(PREFIX + resBundleName, new ResourceBundle.Control() { @Override public List<Locale> getCandidateLocales(String baseName, Locale locale) { return workingLocales; } }); bundles.put(resBundleName, rb); } return rb; } /** * TAkes a list of {@code Locales} in preference order and copies them to the list of * working locales, ensuring that only supported locales are present and that * {@code Locale.ROOT} appears last. * * @param requestedLocales list of locales in preference order */ private static void filterAndCopy(List<Locale> requestedLocales) { workingLocales.clear(); for (Locale locale : requestedLocales) { if (!locale.equals(Locale.ROOT)) { if (isSupportedLocale(locale)) { // Locale is immutable so no need to create copy workingLocales.add(locale); } else { LOGGER.log(Level.WARNING, "{0} is not currently supported", locale); } } } workingLocales.add(Locale.ROOT); } /** * Scans the properties files in the resource directory, compiles a list * of locales, and determines whether each of them is fully or partially * supported. * * @return list of locale info * @throws IOException on error scanning resources directory */ private static List<LocaleInfo> loadLocaleInfo() throws IOException { PropertiesFileFinder finder = new PropertiesFileFinder(); List<PropertiesFileInfo> infoList = finder.scan(PREFIX); Set<Locale> allLocales = new HashSet<Locale>(); for (PropertiesFileInfo info : infoList) { allLocales.addAll(info.getLocales()); } List<LocaleInfo> localeInfoList = new ArrayList<LocaleInfo>(); for (Locale l : allLocales) { localeInfoList.add(new LocaleInfo(l, true)); } for (PropertiesFileInfo info : infoList) { List<Locale> locales = info.getLocales(); for (LocaleInfo li : localeInfoList) { if (!locales.contains(li.locale)) { li.fullySupported = false; } } } return localeInfoList; } }