/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.text; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.conf.MuConfigurations; import com.mucommander.conf.MuPreference; /** * This class takes care of all text localization issues by loading all text entries from a dictionary file on startup * and translating them into the current language on demand. * * <p>All public methods are static to make it easy to call them throughout the application.</p> * * <p>See dictionary file for more information about th dictionary file format.</p> * * @author Maxence Bernard, Arik Hadas */ public class Translator { private static final Logger LOGGER = LoggerFactory.getLogger(Translator.class); /** List of all available languages in the dictionary file */ private static List<Locale> availableLanguages = new ArrayList<Locale>(); /** Current language */ private static Locale language; private static ResourceBundle dictionaryBundle; private static ResourceBundle languagesBundle; /** * Prevents instance creation. */ private Translator() { } static { registerLocale(Locale.forLanguageTag("ar")); registerLocale(Locale.forLanguageTag("be")); registerLocale(Locale.forLanguageTag("ca")); registerLocale(Locale.forLanguageTag("cs")); registerLocale(Locale.forLanguageTag("da")); registerLocale(Locale.forLanguageTag("de")); registerLocale(Locale.forLanguageTag("en")); registerLocale(Locale.forLanguageTag("en-GB")); registerLocale(Locale.forLanguageTag("es")); registerLocale(Locale.forLanguageTag("fr")); registerLocale(Locale.forLanguageTag("hu")); registerLocale(Locale.forLanguageTag("it")); registerLocale(Locale.forLanguageTag("ja")); registerLocale(Locale.forLanguageTag("ko")); registerLocale(Locale.forLanguageTag("nb")); registerLocale(Locale.forLanguageTag("nl")); registerLocale(Locale.forLanguageTag("pl")); registerLocale(Locale.forLanguageTag("pt-BR")); registerLocale(Locale.forLanguageTag("ro")); registerLocale(Locale.forLanguageTag("ru")); registerLocale(Locale.forLanguageTag("sk")); registerLocale(Locale.forLanguageTag("sl")); registerLocale(Locale.forLanguageTag("sv")); registerLocale(Locale.forLanguageTag("tr")); registerLocale(Locale.forLanguageTag("uk")); registerLocale(Locale.forLanguageTag("zh-CN")); registerLocale(Locale.forLanguageTag("zh-TW")); } public static void registerLocale(Locale locale) { availableLanguages.add(locale); } private static Locale loadLocale() { String localeNameFromConf = MuConfigurations.getPreferences().getVariable(MuPreference.LANGUAGE); if (localeNameFromConf == null) { // language is not set in preferences, use system's language // Try to match language with the system's language, only if the system's language // has values in dictionary, otherwise use default language (English). Locale defaultLocale = Locale.getDefault(); LOGGER.info("Language not set in preferences, trying to match system's language ("+defaultLocale+")"); return defaultLocale; } LOGGER.info("Using language set in preferences: "+localeNameFromConf); return Locale.forLanguageTag(localeNameFromConf.replace('_', '-')); } private static final class Utf8ResourceBundleControl extends ResourceBundle.Control { @Override public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { String bundleName = toBundleName(baseName, locale); String resourceName = toResourceName(bundleName, "properties"); URL resourceURL = loader.getResource(resourceName); if (resourceURL != null) { try { return new PropertyResourceBundle(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8)); } catch (Exception e) { LOGGER.debug("Language "+locale+" failed to load, non english characters might be broken",e); } } return super.newBundle(baseName, locale, format, loader, reload); } } private static Locale match(Locale loadedLocale) { for (Locale locale : availableLanguages) if (locale.getLanguage().equals(loadedLocale.getLanguage()) && Objects.equals(locale.getCountry(), loadedLocale.getCountry())) { LOGGER.info("Found exact match (language+country) for locale {}", locale); return locale; } for (Locale locale : availableLanguages) if (locale.getLanguage().equals(loadedLocale.getLanguage())) { LOGGER.info("Found close match (language) for locale {}", loadedLocale); return locale; } LOGGER.info("Locale {} is not available, falling back to English", loadedLocale); return Locale.ENGLISH; } public static void init() { Locale locale = match(loadLocale()); final Utf8ResourceBundleControl utf8ResourceBundleControl = new Utf8ResourceBundleControl(); ResourceBundle resourceBundle= ResourceBundle.getBundle("dictionary", locale, utf8ResourceBundleControl); dictionaryBundle = new Translator.ResolveVariableResourceBundle(resourceBundle); // Set preferred language in configuration file MuConfigurations.getPreferences().setVariable(MuPreference.LANGUAGE, locale.toLanguageTag()); LOGGER.debug("Current language has been set to "+Translator.language); languagesBundle = ResourceBundle.getBundle("languages", utf8ResourceBundleControl); } /** * Returns the current language as a language code ("EN", "FR", "pt_BR", ...). * * @return lang a language code */ public static String getLanguage() { return language.getLanguage(); } /** * Returns an array of available languages, expressed as language codes ("EN", "FR", "pt_BR"...). * The returned array is sorted by language codes in case insensitive order. * * @return an array of language codes. */ public static List<Locale> getAvailableLanguages() { return availableLanguages; } /** * Returns <code>true</code> if the given entry's key has a value in the current language. * If the <code>useDefaultLanguage</code> parameter is <code>true</code>, entries that have no value in the * {@link #getLanguage() current language} but one in the {@link #DEFAULT_LANGUAGE} will be considered as having * a value (<code>true</code> will be returned). * * @param key key of the requested dictionary entry (case-insensitive) * @param useDefaultLanguage if <code>true</code>, entries that have no value in the {@link #getLanguage() current * language} but one in the {@link #DEFAULT_LANGUAGE} will be considered as having a value * @return <code>true</code> if the given key has a corresponding value in the current language. */ public static boolean hasValue(String key, boolean useDefaultLanguage) { return dictionaryBundle.containsKey(key); } /** * Returns the localized text String for the given key expressed in the current language, or in the default language * if there is no value for the current language. Entry parameters (%1, %2, ...), if any, are replaced by the * specified values. * * @param key key of the requested dictionary entry (case-insensitive) * @param paramValues array of parameters which will be used as values for variables. * @return the localized text String for the given key expressed in the current language */ public static String get(String key, String... paramValues) { if (dictionaryBundle.containsKey(key)) return MessageFormat.format(dictionaryBundle.getString(key), (Object[]) paramValues); if (languagesBundle.containsKey(key)) return languagesBundle.getString(key); return key; } /** * Decorator allowing to resolve the values composed of variables. */ private static class ResolveVariableResourceBundle extends ResourceBundle { /** * Pattern corresponding to a variable. */ private static final Pattern VARIABLE = Pattern.compile("\\$\\[([^]]+)\\]"); /** * The underlying resource bundle. */ private final ResourceBundle resourceBundle; /** * The cache containing the resolved values in case the original value contains at least * one variable. */ private final Map<String, String> cache; /** * Constructs a {@code ResolveVariableResourceBundle} with the specified underlying * {@link ResourceBundle}. * @param resourceBundle The underlying {@link ResourceBundle}. */ ResolveVariableResourceBundle(final ResourceBundle resourceBundle) { this.resourceBundle = resourceBundle; this.cache = ResolveVariableResourceBundle.resolve(resourceBundle); } @Override protected Object handleGetObject(final String key) { final Object result = cache.get(key); if (result == null) { return resourceBundle.getObject(key); } return result; } @Override public Enumeration<String> getKeys() { return resourceBundle.getKeys(); } /** * Resolves all the values composed of variables. * @param resourceBundle The {@code ResourceBundle} from which we extract the values to resolve. * @return A {@code Map} containing all the values that have been resolved */ private static Map<String, String> resolve(final ResourceBundle resourceBundle) { final Map<String, String> result = new HashMap<String, String>(); for (final Enumeration<String> enumeration = resourceBundle.getKeys(); enumeration.hasMoreElements(); ) { final String key = enumeration.nextElement(); ResolveVariableResourceBundle.resolve(key, resourceBundle, result); } return Collections.unmodifiableMap(result); } /** * Resolves the value of the specified key if needed and stores the result in the specified map. * @param key The key to resolve. * @param resource The resource bundle from which we extract the value to resolve. * @param map The map in which we store the result. * @return The resolved value of the specified key. */ private static Object resolve(final String key, final ResourceBundle resource, final Map<String, String> map) { Object result = resource.getObject(key); if (result instanceof String) { final String value = (String) result; final Matcher matcher = VARIABLE.matcher(value); int startIndex = 0; final StringBuilder buffer = new StringBuilder(64); while (matcher.find(startIndex)) { buffer.append(value, startIndex, matcher.start()); try { buffer.append(ResolveVariableResourceBundle.resolve(matcher.group(1), resource, map)); } catch (MissingResourceException e) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("The key '{}' is missing", key); } buffer.append(value, matcher.start(), matcher.end()); } startIndex = matcher.end(); } if (buffer.length() > 0) { buffer.append(value.substring(startIndex)); result = buffer.toString(); map.put(key, (String) result); } } return result; } } }