/*
* FrontlineSMS <http://www.frontlinesms.com>
* Copyright 2007, 2008 kiwanja
*
* This file is part of FrontlineSMS.
*
* FrontlineSMS 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, either version 3 of the License, or (at
* your option) any later version.
*
* FrontlineSMS 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FrontlineSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package net.frontlinesms.ui.i18n;
import static net.frontlinesms.FrontlineSMSConstants.COMMON_FAILED;
import static net.frontlinesms.FrontlineSMSConstants.COMMON_OUTBOX;
import static net.frontlinesms.FrontlineSMSConstants.COMMON_PENDING;
import static net.frontlinesms.FrontlineSMSConstants.COMMON_RETRYING;
import static net.frontlinesms.FrontlineSMSConstants.COMMON_SENT;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Currency;
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.regex.Matcher;
import java.util.regex.Pattern;
import net.frontlinesms.AppProperties;
import net.frontlinesms.FrontlineSMSConstants;
import net.frontlinesms.FrontlineUtils;
import net.frontlinesms.data.domain.Email;
import net.frontlinesms.resources.ResourceUtils;
import net.frontlinesms.ui.FrontlineUI;
import net.frontlinesms.ui.UiProperties;
import org.apache.log4j.Logger;
import thinlet.Thinlet;
/**
* Utilities for helping internationalise text etc.
*
* @author Alex Anderson
* @author Gonçalo Silva
*/
public class InternationalisationUtils {
//> STATIC PROPERTIES
/**
* Name of the directory containing the languages files. This is located
* within the config directory.
*/
private static final String LANGUAGES_DIRECTORY_NAME = "languages";
/** The filename of the default language bundle. */
public static final String DEFAULT_LANGUAGE_BUNDLE_FILENAME = "frontlineSMS.properties";
/** The path to the default language bundle on the classpath. */
public static final String DEFAULT_LANGUAGE_BUNDLE_PATH = "/resources/languages/"
+ DEFAULT_LANGUAGE_BUNDLE_FILENAME;
/** Logging object for this class */
private static Logger LOG = FrontlineUtils
.getLogger(InternationalisationUtils.class);
//> GENERAL i18n HELP METHODS
/** The default characterset, UTF-8. This must be available for every JVM. */
public static final Charset CHARSET_UTF8 = Charset.forName("UTF-8");
//>
public static String getI18nString(Internationalised i) {
return getI18nString(i.getI18nKey());
}
/**
* Return an internationalised message for this key, with the current resource bundle
* This method tries to get the string for the current bundle and if it does
* not exist, it looks into the default bundle (English GB).
*
* @param key
* @return the internationalised text, or the english text if no
* internationalised text could be found
*/
public static String getI18nString(String key) {
if (FrontlineUI.currentResourceBundle != null) {
try {
return FrontlineUI.currentResourceBundle.getValue(key);
} catch (MissingResourceException ex) {
}
}
return Thinlet.DEFAULT_ENGLISH_BUNDLE.get(key);
}
/**
* Return an internationalised message for this key and the given resource bundle.
* <br> This method tries to get the string for the bundle given in parameter and looks into
* the default bundle (English GB) if <code>null</code>.
* @param key
* @param languageBundle
* @return the internationalised text, or the english text if no internationalised text could be found
*/
public static String getI18nString(String key, LanguageBundle languageBundle) {
if(languageBundle != null) {
try {
return languageBundle.getValue(key);
} catch(MissingResourceException ex) {}
}
return Thinlet.DEFAULT_ENGLISH_BUNDLE.get(key);
}
/**
* Return the list of internationalised message for this prefix. <br>
* This method tries to get the strings from the current bundle, and if it
* does not exist, it looks into the default bundle
*
* @param key
* @return the list internationalised text, or an empty list if no
* internationalised text could be found
*/
public static List<String> getI18nStrings(String key, String ... i18nValues) {
if(FrontlineUI.currentResourceBundle != null) {
try {
List<String> values = FrontlineUI.currentResourceBundle.getValues(key);
if (i18nValues.length == 0) {
return values;
} else {
List<String> formattedValues = new ArrayList<String>();
for (String value : values) {
formattedValues.add(formatString(value, i18nValues));
}
return formattedValues;
}
} catch(MissingResourceException ex) {}
}
return LanguageBundle.getValues(Thinlet.DEFAULT_ENGLISH_BUNDLE, key);
}
/**
* Return an internationalised message for this key. This calls
* {@link #getI18nString(String)} and then replaces any instance of
* {@link FrontlineSMSConstants#ARG_VALUE} with @param argValues
*
* @param key
* @param argValues
* @return an internationalised string with any substitution variables
* converted
*/
public static String getI18nString(String key, String... argValues) {
String string = getI18nString(key);
return formatString(string, argValues);
}
/**
* Return an internationalised message for this key. This calls
* {@link #getI18nString(String)} and then replaces any instance of
* {@link FrontlineSMSConstants#ARG_VALUE} with @param argValues
*
* @param key
* @param argValues
* @return an internationalised string with any substitution variables
* converted
*/
public static String formatString(String string, String... argValues) {
if (argValues != null) {
// Iterate backwards through the replacements and replace the
// arguments with the new values. Need
// to iterate backwards so e.g. %10 is replaced before %1
for (int i = argValues.length - 1; i >= 0; --i) {
String arg = argValues[i];
if (arg != null) {
if (LOG.isDebugEnabled())
LOG.debug("Subbing " + arg + " as "
+ (FrontlineSMSConstants.ARG_VALUE + i)
+ " into: " + string);
string = string.replace(
FrontlineSMSConstants.ARG_VALUE + i, arg);
}
}
}
return string;
}
/**
* Return an internationalised message for this key. This converts the
* integer to a {@link String} and then calls
* {@link #getI18nString(String, String...)} with this argument.
*
* @param key
* @param intValue
* @return the internationalised string with the supplied integer embedded
* at the appropriate place
*/
public static String getI18nString(String key, int intValue) {
return getI18nString(key, Integer.toString(intValue));
}
/**
* Parses a string representation of an amount of currency to an integer.
* This will handle cases where the string has separators, including non
* default separators, two or more separators, and different separators
* in the same string.
*
* @param currencyString
* @return the currency amount represented by the supplied string
* @throws {@link NumberFormatException}
*/
public static final double parseCurrency(String currencyString) throws NumberFormatException{
String regexPattern = "\\D";
Pattern pattern = Pattern.compile(regexPattern);
Matcher matcher = pattern.matcher(currencyString);
//Execute if currencyString has the specified pattern
if (matcher.find()) {
String[] splitValues = currencyString.split(regexPattern);
if (splitValues.length == 0) {
throw new NumberFormatException();
} else if (splitValues.length == 1) {
currencyString = splitValues[0];
} else if (splitValues.length == 2) {
// Only one separator - assume its for decimal places
currencyString = splitValues[0] + "." + splitValues[1];
} else {
int splitValuesLastBlock = splitValues.length - 1;
String[] separators = new String[splitValuesLastBlock];
matcher.reset();
// Find all separators and store them
for(int counter = 0; !matcher.hitEnd(); counter++){
if(matcher.find()){
separators [counter] = matcher.group();
}
}
// Check if the last two separators are the same - if true, assume no decimal places are present
if (separators[separators.length - 1].equals(separators[separators.length - 2])) {
currencyString = currencyString.replaceAll("\\D", "");
} else {
currencyString = "";
for (int i = 0; i < splitValuesLastBlock; i++) {
currencyString += splitValues[i];
}
currencyString += "." + splitValues[splitValuesLastBlock];
}
}
}
return Double.parseDouble(currencyString);
}
/**
* Returns a formatted value according to the defined currency format
*
* @param value
* @return formatted value
*/
public static final String formatCurrency(double value) {
if (UiProperties.getInstance().isCurrencyFormatCustom()) {
String currencyFormat = UiProperties.getInstance().getCustomCurrencyFormat();
return new CurrencyFormatter(currencyFormat).format(value);
} else {
return NumberFormat.getCurrencyInstance(getCurrentLocale()).format(value);
}
}
// > LANGUAGE BUNDLE LOADING METHODS
/**
* Loads the default, english {@link LanguageBundle} from the classpath
*
* @return the default English {@link LanguageBundle}
* @throws IOException
* If there was a problem loading the default language bundle.
* // TODO this should probably throw a runtimeexception of some
* sort
*/
public static final LanguageBundle getDefaultLanguageBundle()
throws IOException {
return ClasspathLanguageBundle.create(DEFAULT_LANGUAGE_BUNDLE_PATH);
}
/**
* @return {@link InputStream} to the default translation file on the
* classpath.
*/
public static InputStream getDefaultLanguageBundleInputStream() {
return ClasspathLanguageBundle.class
.getResourceAsStream(DEFAULT_LANGUAGE_BUNDLE_PATH);
}
/**
* Loads a {@link LanguageBundle} from a file. All files are encoded with
* UTF-8. TODO change this to use {@link Currency}, and put the ISO 4217
* currency code in the l10n file.
*
* @param file
* @return The loaded bundle, or NULL if the bundle could not be loaded.
*/
public static final FileLanguageBundle getLanguageBundle(File file) {
try {
FileLanguageBundle bundle = FileLanguageBundle.create(file);
LOG.info("Successfully loaded language bundle from file: "
+ file.getName());
LOG.info("Bundle reports filename as: "
+ bundle.getFile().getAbsolutePath());
LOG.info("Language Name : " + bundle.getLanguageName());
LOG.info("Language Code : " + bundle.getLanguageCode());
LOG.info("Country : " + bundle.getCountry());
LOG.info("Right-To-Left : " + bundle.isRightToLeft());
return bundle;
} catch (Exception ex) {
LOG.error("Problem reading language file: " + file.getName(), ex);
return null;
}
}
/**
* @param identifier
* ID used when logging problems while loading the text resource
* @param inputStream
* @return map containing map of key-value pairs of text resources
* @throws IOException
*/
public static final Map<String, String> loadTextResources(
String identifier, InputStream inputStream) throws IOException {
HashMap<String, String> i18nStrings = new HashMap<String, String>();
BufferedReader in = new BufferedReader(new InputStreamReader(
inputStream, CHARSET_UTF8));
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.length() > 0 && line.charAt(0) != '#') {
int splitChar = line.indexOf('=');
if (splitChar <= 0) {
// there's no "key=value" pair on this line, but it does
// have text on it. That's
// not strictly legal, so we'll log a warning and carry on.
LOG.warn("Bad line in language file '" + identifier
+ "': '" + line + "'");
} else {
String key = line.substring(0, splitChar).trim();
if (i18nStrings.containsKey(key)) {
// This key has already been read from the language
// file. Ignore the new value.
LOG.warn("Duplicate key in language file '': ''");
} else {
String value = line.substring(splitChar + 1).trim();
if (value.length() > 0) {
i18nStrings.put(key, value);
}
}
}
}
}
return i18nStrings;
}
/**
* Loads all language bundles from within and without the JAR
*
* @return all language bundles from within and without the JAR
*/
public static Collection<FileLanguageBundle> getLanguageBundles() {
ArrayList<FileLanguageBundle> bundles = new ArrayList<FileLanguageBundle>();
File langDir = new File(getLanguageDirectoryPath());
if (!langDir.exists() || !langDir.isDirectory())
throw new IllegalArgumentException(
"Could not find resources directory: "
+ langDir.getAbsolutePath());
for (File file : langDir.listFiles()) {
FileLanguageBundle bungle = getLanguageBundle(file);
if (bungle != null) {
bundles.add(bungle);
}
}
return bundles;
}
/** @return path of the directory in which language bundles are located. */
private static final String getLanguageDirectoryPath() {
return ResourceUtils.getConfigDirectoryPath()
+ LANGUAGES_DIRECTORY_NAME + File.separatorChar;
}
/** @return path of the directory in which language bundles are located. */
public static final File getLanguageDirectory() {
return new File(ResourceUtils.getConfigDirectoryPath(),
LANGUAGES_DIRECTORY_NAME);
}
// > DATE FORMAT GETTERS
/**
* N.B. This {@link DateFormat} may be used for parsing user-entered data.
*
* @return date format for displaying and entering year (4 digits), month
* and day.
*/
public static DateFormat getDateFormat() {
return new SimpleDateFormat(
getI18nString(FrontlineSMSConstants.DATEFORMAT_YMD));
}
/**
* This is not used for parsing user-entered data.
*
* @return date format for displaying date and time.#
*/
public static DateFormat getDatetimeFormat() {
return new SimpleDateFormat(
getI18nString(FrontlineSMSConstants.DATEFORMAT_YMD_HMS));
}
/**
* TODO what is this method used for? This value seems completely
* nonsensical - why wouldn't you just use the timestamp itself? When do you
* ever need the date as an actual string?
*
* @return current time as a formatted date string
*/
public static String getDefaultStartDate() {
return getDateFormat().format(new Date());
}
/**
* Parse the supplied {@link String} into a {@link Date}. This method
* assumes that the supplied date is in the same format as
* {@link #getDateFormat()}.
*
* @param date
* A date {@link String} formatted with {@link #getDateFormat()}
* @return a java {@link Date} object describing the supplied date
* @throws ParseException
*/
public static Date parseDate(String date) throws ParseException {
return getDateFormat().parse(date);
}
/**
* <p>
* Merges the source map into the destination. Values in the destination
* take precedence - they will not be overridden if the same key occurs in
* both destination and source.
* </p>
* <p>
* If a <code>null</code> source is provided, this method does nothing; if a
* <code>null</code> destination is provided, a {@link NullPointerException}
* will be thrown.
*
* @param destination
* @param source
*/
public static void mergeMaps(Map<String, String> destination,
Map<String, String> source) {
assert (destination != null) : "You must provide a destination map to merge into.";
// If there is nothing to merge, just return.
if (source == null)
return;
for (String key : source.keySet()) {
if (destination.get(key) != null) {
// key already present in language bundle - ignoring
} else {
// this key does not appear in the language bundle, so add it
// with the value from the map
destination.put(key, source.get(key));
}
}
}
/**
* Get the status of a {@link Email} as a {@link String}.
*
* @param email
* @return {@link String} representation of the status.
*/
public static final String getEmailStatusAsString(Email email) {
switch (email.getStatus()) {
case OUTBOX:
return getI18nString(COMMON_OUTBOX);
case PENDING:
return getI18nString(COMMON_PENDING);
case SENT:
return getI18nString(COMMON_SENT);
case RETRYING:
return getI18nString(COMMON_RETRYING);
case FAILED:
return getI18nString(COMMON_FAILED);
default:
return "(unknown)";
}
}
/**
* @return the current locale, specified by which language is currently
* selected
*/
public static Locale getCurrentLocale() {
return FrontlineUI.currentResourceBundle != null ? FrontlineUI.currentResourceBundle
.getLocale()
: new Locale("en", "gb");
}
public static String getInternationalPhoneNumber(String phoneNumber) {
return CountryCallingCode.format(phoneNumber, AppProperties.getInstance().getUserCountry());
}
}