/******************************************************************************* * Copyright (c) 2011 Nokia Corporation * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Comarch team - initial API and implementation *******************************************************************************/ package org.ned.client; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Hashtable; import org.ned.client.utils.NedIOUtils; /** * @author krzysztof.glab */ public class Localization { /************************************************************************** **** **** Localization Support Begin **** **************************************************************************/ /** * Full path to the messages resource bundle. Feel free to change it if you don't * use the message bundle file generated by IDE. */ public static final String _MESSAGES_BUNDLE = "/org/ned/client/messages.properties"; /** * Error message used in the case there is any problem with initialization of * localization support. Please note, the error message should contain one * parameter sign - '{0}', which is used to fill in a reason of the failure. */ private static final String _INIT_LOCALIZATION_ERROR_MSG = "Error when initializing localization support, reason: {0}"; /** * Default String is returned from getMessage() methods when there is any problem * with finding the appropriate localized message or any part of it. */ public static final String _DEFAULT_STRING = "???"; /** * Initializes localization support based on currently set locale (obtained * from "microedition.locale" system property). The initialization method is called * automatically when a call to {@link #getMessage(java.lang.String)} method is attempted for the first time. * * * You can call this method explicitly to see whether there was any problem * with initialization of the localization support. Method returns a status * of the successfulness. If there was any problem with initialization, you can * get reason by using {@link #getErrorMessage()} method. * @return true if the intialization was succesfull, false if there was any problem. */ public static boolean initLocalizationSupport() { String locale = System.getProperty("microedition.locale"); if ( NedMidlet.getAccountManager() != null && NedMidlet.getAccountManager().getLanguage() != null) { locale = NedMidlet.getAccountManager().getLanguage().getLocale(); } return initLocalizationSupport(locale); // NOI18N } /** * Explicit initialization of the localization support. This method is usually * called when a particular locale used in the application. E.g. the application * contains only french messages (no default messages, only <CODE>messages_fr.properties</CODE> * files is available), you should initialize the localization support (by calling * <CODE>initLocalizationSupport("fr");</CODE>) before using {@link #getMessage(java.lang.String)} method for the first * time. * * Method returns a status of the successfulness. If there was any problem with * the initialization, you can get explanation by using {@link #getErrorMessage()} * method. * @param locale locale which will be used to determine which message file from bundle will be used * @return true if the intialization was succesfull, false if there was any problem. */ public static boolean initLocalizationSupport(String locale) { InputStream in = null; // need access to a class object - cannot use Object.class, because of MIDP1 bug Class clazz = Runtime.getRuntime().getClass(); try { // try to find localized resource first (in format <name>_locale.<suffix>) if ((locale != null) && (locale.length() > 1)) { int lastIndex = _MESSAGES_BUNDLE.lastIndexOf('.'); String prefix = _MESSAGES_BUNDLE.substring(0, lastIndex); String suffix = _MESSAGES_BUNDLE.substring(lastIndex); // replace '-' with '_', some phones returns locales with // '-' instead of '_'. For example Nokia or Motorola locale = locale.replace('-', '_'); in = NedIOUtils.loadFileAsStream( NedIOUtils.getLocalRoot() + "messages_" + locale + suffix ); if( in == null ) { in = clazz.getResourceAsStream(prefix + "_" + locale + suffix); } if (in == null) { // if no localized resource is found or localization is available // try broader???? locale (i.e. instead og en_US, try just en) in = clazz.getResourceAsStream(prefix + "_" + locale.substring(0, 2) + suffix); } } if (in == null) { // if not found or locale is not set, try default locale in = clazz.getResourceAsStream(_MESSAGES_BUNDLE); } if (in == null) { // no messages bundle was found - initialization failed _localizationErrorMessage = _processPattern(_INIT_LOCALIZATION_ERROR_MSG, new Object[]{"No messages found"}); // NOI18N } else { // load messages to _messageTable hashtable _messageTable = new Hashtable(); _loadMessages(in); // we are ok - return true as success ... return true; } } catch (Exception e) { // houston we have a problem _localizationErrorMessage = _processPattern(_INIT_LOCALIZATION_ERROR_MSG, new Object[]{e.getMessage()}); } finally { try { if( in!= null ) { in.close(); } } catch ( IOException ex ) { } } return false; } /** * Returns an error message if there was any problem with accessing the localized * text. The message also possibly explainins a reason of the failure. The message * is taken from <CODE>_INIT_LOCALIZATION_ERROR_MSG</CODE>. * @return error message if there was any failure or null when everything is OK. */ public static String getErrorMessage() { return _localizationErrorMessage; } /** * Finds a localized string in a message bundle. * @param key key of the localized string to look for * @return the localized string. If key is not found, then <CODE>_DEFAULT_STRING</CODE> string * is returned */ public static final String getMessage(String key) { return getMessage(key, null); } /** * Finds a localized string in a message bundle and formats the message by passing * requested parameters. * @param key key of the localized string to look for * @param args array of parameters to use for formatting the message * @return the localized string. If key is not found, then <CODE>_DEFAULT_STRING</CODE> string * is returned */ public static final String getMessage(String key, Object[] args) { if (_messageTable == null) { if (!initLocalizationSupport()) { return _DEFAULT_STRING; } } StringBuffer toAppendTo = new StringBuffer(); String s = (String) _messageTable.get(key); if (s == null) { return _DEFAULT_STRING; } int l = s.length(); int n = 0, lidx = -1, lastidx = 0; for (int i = 0; i < l; i++) { if (s.charAt(i) == '{') { n++; if (n == 1) { lidx = i; toAppendTo.append(s.substring(lastidx, i)); lastidx = i; } } if (s.charAt(i) == '}') { if (n == 1) { toAppendTo.append(_processPattern(s.substring(lidx + 1, i), args)); lidx = -1; lastidx = i + 1; } n--; } } if (n > 0) { toAppendTo.append(_processPattern(s.substring(lidx + 1), args)); } else { toAppendTo.append(s.substring(lastidx)); } return toAppendTo.toString(); } /* The rest is private to localization support. You shouldn't change anything * below this comment unless you really know what you are doing * Ideally, everyhthing below this should be collapsed. */ /** * Characters separating keys and values */ private static final String _KEY_VALUE_SEPARATORS = "=: \t\r\n\f"; /** * Characters strictly separating keys and values */ private static final String _STRICT_KEY_VALUE_SEPARTORS = "=:"; /** * white space characters understood by the support (these can be in the message file) */ private static final String _WHITESPACE_CHARS = " \t\r\n\f"; /** * Contains the parsed message bundle. */ private static Hashtable _messageTable; /** * Contains an error message if there was any problem with localization support. * If everything is OK, this field is null. */ private static String _localizationErrorMessage = null; /** * Loads messages from input stream to hash table. * @param inStream stream from which the messages are read * @throws IOException if there is any problem with reading the messages */ private static synchronized void _loadMessages(InputStream inStream) throws IOException { InputStreamReader in = new InputStreamReader(inStream); while (true) { // Get next line String line = _readLine(in); if (line == null) { in.close(); return; } if (line.length() > 0) { // Find start of key int len = line.length(); int keyStart; for (keyStart = 0; keyStart < len; keyStart++) { if (_WHITESPACE_CHARS.indexOf(line.charAt(keyStart)) == -1) { break; } } // Blank lines are ignored if (keyStart == len) { continue; } // Continue lines that end in slashes if they are not comments char firstChar = line.charAt(keyStart); if ((firstChar != '#') && (firstChar != '!')) { while (_continueLine(line)) { String nextLine = _readLine(in); if (nextLine == null) { nextLine = ""; } String loppedLine = line.substring(0, len - 1); // Advance beyond whitespace on new line int startIndex; for (startIndex = 0; startIndex < nextLine.length(); startIndex++) { if (_WHITESPACE_CHARS.indexOf(nextLine.charAt(startIndex)) == -1) { break; } } nextLine = nextLine.substring(startIndex, nextLine.length()); line = loppedLine + nextLine; len = line.length(); } // Find separation between key and value int separatorIndex; for (separatorIndex = keyStart; separatorIndex < len; separatorIndex++) { char currentChar = line.charAt(separatorIndex); if (currentChar == '\\') { separatorIndex++; } else if (_KEY_VALUE_SEPARATORS.indexOf(currentChar) != -1) { break; } } // Skip over whitespace after key if any int valueIndex; for (valueIndex = separatorIndex; valueIndex < len; valueIndex++) { if (_WHITESPACE_CHARS.indexOf(line.charAt(valueIndex)) == -1) { break; } } // Skip over one non whitespace key value separators if any if (valueIndex < len) { if (_STRICT_KEY_VALUE_SEPARTORS.indexOf(line.charAt(valueIndex)) != -1) { valueIndex++; } } // Skip over white space after other separators if any while (valueIndex < len) { if (_WHITESPACE_CHARS.indexOf(line.charAt(valueIndex)) == -1) { break; } valueIndex++; } String key = line.substring(keyStart, separatorIndex); String value = (separatorIndex < len) ? line.substring(valueIndex, len) : ""; // Convert then store key and value key = _convertString(key); value = _convertString(value); _messageTable.put(key, value); } } } } /** * reads a single line from InputStreamReader * @param in InputStreamReader used to read the line * @throws IOException if there is any problem with reading * @return the read line */ private static String _readLine(InputStreamReader in) throws IOException { StringBuffer strBuf = new StringBuffer(""); int i; while ((i = in.read()) != -1) { if ((char) i == '\r' || (char) i == '\n') { return strBuf.toString(); } strBuf.append((char) i); } return strBuf.length() > 0 ? strBuf.toString() : null; } /** * determines whether the line of the supplied string continues on the next line * @param line a line of String * @return true if the string contines on the next line, false otherwise */ private static boolean _continueLine(String line) { int slashCount = 0; int index = line.length() - 1; while ((index >= 0) && (line.charAt(index--) == '\\')) { slashCount++; } return (slashCount % 2 == 1); } /** * Decodes a String which uses unicode characters in \\uXXXX format. * @param theString String with \\uXXXX characters * @return resolved string */ private static String _convertString(String theString) { char aChar; int len = theString.length(); StringBuffer outBuffer = new StringBuffer(len); for (int x = 0; x < len;) { aChar = theString.charAt(x++); if (aChar == '\\') { aChar = theString.charAt(x++); if (aChar == 'u') { // Read the xxxx int value = 0; for (int i = 0; i < 4; i++) { aChar = theString.charAt(x++); switch (aChar) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': value = (value << 4) + aChar - '0'; break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': value = (value << 4) + 10 + aChar - 'a'; break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': value = (value << 4) + 10 + aChar - 'A'; break; default: // return DEFAULT STRING if there is any problem return _DEFAULT_STRING; } } outBuffer.append((char) value); } else { if (aChar == 't') { aChar = '\t'; } else if (aChar == 'r') { aChar = '\r'; } else if (aChar == 'n') { aChar = '\n'; } else if (aChar == 'f') { aChar = '\f'; } outBuffer.append(aChar); } } else { outBuffer.append(aChar); } } return outBuffer.toString(); } /** * Extracts N-th from an array of argumens. * @param indexString a String number * @param args array of arguments * @return the indexString-th parameter from the array */ private static String _processPattern(String indexString, Object[] args) { try { int index = Integer.parseInt(indexString); if ((args != null) && (index >= 0) && (index < args.length)) { if (args[index] != null) { return args[index].toString(); } } } catch (NumberFormatException nfe) { // NFE - nothing bad basically - the argument is not a number // swallow it for the time being and show default string } return _DEFAULT_STRING; } /************************************************************************** **** **** Localization Support End **** **************************************************************************/ }