/****************************************************************************
* Copyright (C) 2012 ecsec GmbH.
* All rights reserved.
* Contact: ecsec GmbH (info@ecsec.de)
*
* This file is part of the Open eCard App.
*
* GNU General Public License Usage
* This file may be used under the terms of the GNU General Public
* License version 3.0 as published by the Free Software Foundation
* and appearing in the file LICENSE.GPL included in the packaging of
* this file. Please review the following information to ensure the
* GNU General Public License version 3.0 requirements will be met:
* http://www.gnu.org/copyleft/gpl.html.
*
* Other Usage
* Alternatively, this file may be used in accordance with the terms
* and conditions contained in a signed written agreement between
* you and ecsec GmbH.
*
***************************************************************************/
package org.openecard.common;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.Locale;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.openecard.common.util.FileUtils;
/**
* An internationalization component similar to Java's {@link java.util.ResourceBundle}.
* It is capable of providing translated versions of key identified values as well as complete files.
* <p>
* All translation files must be located below the folder <pre>openecard_i18n</pre> plus a component name.
* The special file Messages.properties is used to provide translated key value pairs. Completely translated files can
* have any other name.<br/>
* All translation files follow the same scheme to identify their language. The language is written in the form of
* <a href="https://tools.ietf.org/html/bcp47">BCP 47</a> language tags. However instead of -, _ is used as a separator,
* which is the common practice. This implementation only supports a subset of the BCP 47 specification, meaning only
* language and country codes are allowed. The default language, which is English is described by C. The name of the
* file and its language is separated by _. The file ending is optional for arbitrary files.<br/>
* The following examples illustrate the scheme.
* <pre> Messages_C.properties
* Messages_de.properties
* Messages_de_DE.properties
* anyotherfile_C
* anyotherfile_C.html</pre>
*
* @author Tobias Wich <tobias.wich@ecsec.de>
*/
public class I18n {
private static final ConcurrentSkipListMap<String,I18n> translations;
static {
translations = new ConcurrentSkipListMap<String,I18n>();
// preload important components
getTranslation("ifd");
getTranslation("sal");
}
/**
* Load a translation for the specified component. If no translation for a
* component exists, a fallback method is used according to {@link java.util.ResourceBundle} and in case no
* translation exists at all, an empty I18n instance is returned.
*
* @param component String describing the component. This must also be the filename prefix of the translation.
* @return I18n instance responsible for specified component.
*/
public synchronized static I18n getTranslation(String component) {
if (translations.containsKey(component)) {
return translations.get(component);
} else {
I18n t = new I18n(component);
translations.put(component, t);
return t;
}
}
private final String component;
private final Properties translation;
private final TreeMap<String, URL> translatedFiles;
private I18n(String component) {
Locale userLocale = Locale.getDefault();
String lang = userLocale.getLanguage();
String country = userLocale.getCountry();
// load applicable language files
// the order is: C -> lang -> lang_country
Properties defaults = loadFile(component, "C");
if (!lang.isEmpty()) {
Properties target = loadFile(component, lang);
defaults = mergeProperties(defaults, target);
}
if (!lang.isEmpty() && !country.isEmpty()) {
Properties target = loadFile(component, lang + "_" + country);
defaults = mergeProperties(defaults, target);
}
this.component = component;
this.translation = defaults;
this.translatedFiles = new TreeMap<String, URL>();
}
private static Properties loadFile(String component, String locale) {
// load properties or die tryin'
try {
String fileName = "/openecard_i18n/" + component + "/Messages_" + locale + ".properties";
InputStream in = FileUtils.resolveResourceAsStream(I18n.class, fileName);
Properties props = new Properties();
Reader r = new InputStreamReader(in, "utf-8");
props.load(r);
return props;
} catch (IOException ex) {
return new Properties();
} catch (RuntimeException ex) { // no such file and stuff
return new Properties();
}
}
private static Properties mergeProperties(Properties defaults, Properties target) {
Properties result = new Properties(defaults);
result.putAll(target);
return result;
}
///
/// public non static api
///
/**
* @return Name of the component this I18n instance is responsible for.
*/
public String associatedComponent() {
return component;
}
/**
* Get the translated value for the given key.
* The implementation tries to find the key in the requested language, then the default language and if nothing is
* specified at all, a special string in the form of <No translation for key <requested.key>>
* is returned.
*
* @param key Key as defined in language properties file.
* @param parameters If any parameters are given here, the string is interpreted as a template and the parameters
* are applied. The template interpretation uses {@link String#format()} as the rendering method.
* @return Translation as specified in the translation, or default file.
*/
public String translationForKey(String key, Object ... parameters) {
String result = translation.getProperty(key.toLowerCase());
if (result == null) {
return "<<No translation for key <" + key + ">>";
} else if (parameters.length != 0) {
String formattedResult = String.format(result, parameters);
return formattedResult;
} else {
return result;
}
}
/**
* Calls {@link #translationForFile(java.lang.String, java.lang.String)} with the second parameter set to null.
*/
public URL translationForFile(String name) throws IOException {
return translationForFile(name, null);
}
/**
* Get translated version of a file depending on current locale.
* <p>The file's base path equals the component directory. The language definition is enclosed between the filename
* and the file ending plus a '.'.</p>
* <p>An example looks like this:<br/>
* <pre> I18n l = I18n.getTranslation("gui");
* l.translationForFile("about", "html");
* // this code in a german environment tries to load the following files until one is found
* // - openecard_i18n/gui/about_de_DE.html
* // - openecard_i18n/gui/about_de.html
* // - openecard_i18n/gui/about_C.html</pre>
* </p>
*
* @param name Name part of the file
* @param fileEnding File ending if available, null otherwise.
* @return URL pointing to the translated, or default file.
* @throws IOException Thrown in case no resource is available.
*/
public synchronized URL translationForFile(String name, String fileEnding) throws IOException {
// check if the url has already been found previously
fileEnding = fileEnding != null ? ("." + fileEnding) : "";
String mapKey = name + fileEnding;
if (translatedFiles.containsKey(mapKey)) {
URL url = translatedFiles.get(mapKey);
if (url == null) {
throw new IOException("No translation available for file '" + name + fileEnding + "'.");
} else {
return url;
}
}
Locale locale = Locale.getDefault();
String lang = locale.getLanguage();
String country = locale.getCountry();
String fnameBase = "/openecard_i18n/" + component + "/" + name;
// try to guess correct file to load
if (!lang.isEmpty() && !country.isEmpty()) {
String fileName = fnameBase + "_" + lang + "_" + country + fileEnding;
URL url = FileUtils.resolveResourceAsURL(I18n.class, fileName);
if (url != null) {
translatedFiles.put(mapKey, url);
return url;
}
}
if (!lang.isEmpty()) {
String fileName = fnameBase + "_" + lang + fileEnding;
URL url = FileUtils.resolveResourceAsURL(I18n.class, fileName);
if (url != null) {
translatedFiles.put(mapKey, url);
return url;
}
}
// else
String fileName = fnameBase + "_C" + fileEnding;
URL url = FileUtils.resolveResourceAsURL(I18n.class, fileName);
if (url != null) {
translatedFiles.put(mapKey, url);
return url;
}
// no file found
translatedFiles.put(mapKey, null);
throw new IOException("No translation available for file '" + name + fileEnding + "'.");
}
}