/** * Copyright (C) 2012-2017 the original author or authors. * * 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 ninja.i18n; import java.text.MessageFormat; import java.util.Locale; import java.util.Map; import java.util.Optional; import ninja.Context; import ninja.Result; import ninja.utils.NinjaConstant; import ninja.utils.NinjaProperties; import ninja.utils.SwissKnife; import org.apache.commons.configuration.CompositeConfiguration; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationConverter; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.Singleton; @Singleton public class MessagesImpl implements Messages { private static Logger logger = LoggerFactory.getLogger(MessagesImpl.class); private final Map<String, Configuration> langToKeyAndValuesMapping; private final NinjaProperties ninjaProperties; private final Lang lang; @Inject public MessagesImpl(NinjaProperties ninjaProperties, Lang lang) { this.ninjaProperties = ninjaProperties; this.lang = lang; this.langToKeyAndValuesMapping = loadAllMessageFilesForRegisteredLanguages(); } @Override public Optional<String> get(String key, Context context, Optional<Result> result, Object... parameter) { Optional<String> language = lang.getLanguage(context, result); return get(key, language, parameter); } @Override public Optional<String> get(String key, Optional<String> language, Object... params) { Configuration configuration = getLanguageConfigurationForLocale(language); String value = configuration.getString(key); if (value != null) { MessageFormat messageFormat = getMessageFormatForLocale(value, language); return Optional.of(messageFormat.format(params)); } else { return Optional.empty(); } } @Override public Map<Object, Object> getAll(Context context, Optional<Result> result) { Optional<String> language = lang.getLanguage(context, result); return getAll(language); } @Override public Map<Object, Object> getAll(Optional<String> language) { Configuration configuration = getLanguageConfigurationForLocale(language); return ConfigurationConverter.getMap(configuration); } @Override public String getWithDefault(String key, String defaultMessage, Context context, Optional<Result> result, Object... params) { Optional<String> language = lang.getLanguage(context, result); return getWithDefault(key, defaultMessage, language, params); } @Override public String getWithDefault(String key, String defaultMessage, Optional<String> language, Object... params) { Optional<String> value = get(key, language, params); if (value.isPresent()) { return value.get(); } else { MessageFormat messageFormat = getMessageFormatForLocale(defaultMessage, language); return messageFormat.format(params); } } /** * Attempts to load a message file and sets the file changed reloading * strategy on the configuration if the runtime mode is Dev. */ private PropertiesConfiguration loadLanguageConfiguration(String fileOrUrl) { PropertiesConfiguration configuration = SwissKnife .loadConfigurationInUtf8(fileOrUrl); if (configuration != null && ninjaProperties.isDev()) { // enable runtime reloading of translations in dev mode FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy(); configuration.setReloadingStrategy(strategy); } return configuration; } /** * Does all the loading of message files. * * Only registered messages in application.conf are loaded. * */ private Map<String, Configuration> loadAllMessageFilesForRegisteredLanguages() { Map<String, Configuration> langToKeyAndValuesMappingMutable = Maps.newHashMap(); // Load default messages: Configuration defaultLanguage = loadLanguageConfiguration("conf/messages.properties"); // Make sure we got the file. // Everything else does not make much sense. if (defaultLanguage == null) { throw new RuntimeException( "Did not find conf/messages.properties. Please add a default language file."); } else { langToKeyAndValuesMappingMutable.put("", defaultLanguage); } // Get the languages from the application configuration. String[] applicationLangs = ninjaProperties .getStringArray(NinjaConstant.applicationLanguages); // If we don't have any languages declared we just return. // We'll use the default messages.properties file. if (applicationLangs == null) { return ImmutableMap.copyOf(langToKeyAndValuesMappingMutable); } // Load each language into the HashMap containing the languages: for (String lang : applicationLangs) { // First step: Load complete language eg. en-US Configuration configuration = loadLanguageConfiguration(String .format("conf/messages_%s.properties", lang)); Configuration configurationLangOnly = null; // If the language has a country code load the default values for // the language, too. For instance missing variables in en-US will // be // Overwritten by the default languages. if (lang.contains("-")) { // get the lang String langOnly = lang.split("-")[0]; // And load the configuraion configurationLangOnly = loadLanguageConfiguration(String .format("conf/messages_%s.properties", langOnly)); } // This is strange. If you defined the language in application.conf // it should be there propably. if (configuration == null) { logger.info( "Did not find conf/messages_{}.properties but it was specified in application.conf. Using default language instead.", lang); } else { // add new language, but combine with default language if stuff // is missing... CompositeConfiguration compositeConfiguration = new CompositeConfiguration(); // Add eg. "en-US" compositeConfiguration.addConfiguration(configuration); // Add eg. "en" if (configurationLangOnly != null) { compositeConfiguration .addConfiguration(configurationLangOnly); } // Add messages.conf (default pack) compositeConfiguration.addConfiguration(defaultLanguage); // and add the composed configuration to the hashmap with the // mapping. langToKeyAndValuesMappingMutable.put(lang, (Configuration) compositeConfiguration); } } return ImmutableMap.copyOf(langToKeyAndValuesMappingMutable); } /** * Retrieves the matching language file from an arbitrary one or two part * locale String ("en-US", or "en" or "de"...). * <p> * * @param language * A two or one letter language code such as "en-US" or "en" or * "en-US,en;q=0.8,de;q=0.6". * @return The matching configuration from the hashmap. Or the default * mapping if no one has been found. */ private Configuration getLanguageConfigurationForLocale(Optional<String> language) { // if language is null we return the default language. if (!language.isPresent()) { return langToKeyAndValuesMapping.get(""); } // Check if we get a registered mapping for the language input string. // At that point the language may be either language-country or only country. // extract multiple languages from Accept-Language header String[] languages = language.get().split(","); for (String l: languages){ l = l.trim(); // Ignore the relative quality factor in Accept-Language header if (l.contains(";")){ l = l.split(";")[0]; } Configuration configuration = langToKeyAndValuesMapping.get(l); if (configuration != null) { return configuration; } // If we got a two part language code like "en-US" we split it and // search only for the language "en". if (l.contains("-")) { String[] array = l.split("-"); String languageWithoutCountry = array[0]; // Modify country code to upper case for IE and Firefox if(array.length > 1){ String country = array[1]; String languageWithUpperCaseCountry = languageWithoutCountry + "-" + country.toUpperCase(); configuration = langToKeyAndValuesMapping.get(languageWithUpperCaseCountry); if (configuration != null) { return configuration; } } configuration = langToKeyAndValuesMapping .get(languageWithoutCountry); if (configuration != null) { return configuration; } } } // Oops. Nothing found. We return the default language (by convention guaranteed to work). return langToKeyAndValuesMapping.get(""); } MessageFormat getMessageFormatForLocale(String value, Optional<String> language) { Locale locale = lang.getLocaleFromStringOrDefault(language); MessageFormat messageFormat = new MessageFormat(value, locale); return messageFormat; } }