// // $Id: MessageBundle.java,v 1.19 2003/03/05 23:27:39 mdb Exp $ package com.threerings.util; import java.text.MessageFormat; import java.util.MissingResourceException; import java.util.ResourceBundle; import com.samskivert.util.StringUtil; /** * A message bundle provides an easy mechanism by which to obtain * translated message strings from a resource bundle. It uses the {@link * MessageFormat} class to substitute arguments into the translation * strings. Message bundles would generally be obtained via the {@link * MessageManager}, but could be constructed individually if so desired. */ public class MessageBundle { /** * Initializes the message bundle which will obtain localized messages * from the supplied resource bundle. The path is provided purely for * reporting purposes. */ public void init (MessageManager msgmgr, String path, ResourceBundle bundle, MessageBundle parent) { _msgmgr = msgmgr; _path = path; _bundle = bundle; _parent = parent; } /** * Obtains the translation for the specified message key. No arguments * are substituted into the translated string. If a translation * message does not exist for the specified key, an error is logged * and the key itself is returned so that the caller need not worry * about handling a null response. */ public String get (String key) { // if this string is tainted, we don't translate it, instead we // simply remove the taint character and return it to the caller if (key.startsWith(TAINT_CHAR)) { return key.substring(1); } String msg = getResourceString(key); return (msg != null) ? msg : key; } /** * Get a String from the resource bundle, or null if there was an error. */ protected String getResourceString (String key) { return getResourceString(key, true); } /** * Get a String from the resource bundle, or null if there was an * error. * * @param key the resource key. * @param reportMissing whether or not the method should log an error * if the resource didn't exist. */ protected String getResourceString (String key, boolean reportMissing) { try { if (_bundle != null) { return _bundle.getString(key); } } catch (MissingResourceException mre) { // if we have a parent, try getting the string from them if (_parent != null) { String value = _parent.getResourceString(key, false); if (value != null) { return value; } // if we didn't find it in our parent, we want to fall // through and report missing appropriately } if (reportMissing) { Log.warning("Missing translation message " + "[bundle=" + _path + ", key=" + key + "]."); Thread.dumpStack(); } } return null; } /** * Obtains the translation for the specified message key accounting * for plurality in the following manner. Assuming a message key of * m.widgets, the following translations should be * defined: *
     * m.widgets.0 = no widgets.
     * m.widgets.1 = {0} widget.
     * m.widgets.n = {0} widgets.
     * 
* * The specified argument is substituted into the translated string as * appropriate. Consider using: * *
     * m.widgets.n = {0,number,integer} widgets.
     * 
* * to obtain proper insertion of commas and dots as appropriate for * the locale. */ public String get (String key, int value) { // we could use ChoiceFormat for this, but there's no indication // that it does anything more useful than what we're doing here // when it comes to handling plurals String format; switch (value) { case 0: format = getResourceString(key + ".0"); break; case 1: format = getResourceString(key + ".1"); break; default: format = getResourceString(key + ".n"); break; } Object[] args = new Object[] { new Integer(value) }; return (format == null) ? (key + StringUtil.toString(args)) : MessageFormat.format(format, args); } /** * Obtains the translation for the specified message key. The * specified argument is substituted into the translated string. * *

See {@link MessageFormat} for more information on how the * substitution is performed. If a translation message does not exist * for the specified key, an error is logged and the key itself (plus * the argument) is returned so that the caller need not worry about * handling a null response. */ public String get (String key, Object arg1) { return get(key, new Object[] { arg1 }); } /** * Obtains the translation for the specified message key. The * specified arguments are substituted into the translated string. * *

See {@link MessageFormat} for more information on how the * substitution is performed. If a translation message does not exist * for the specified key, an error is logged and the key itself (plus * the arguments) is returned so that the caller need not worry about * handling a null response. */ public String get (String key, Object arg1, Object arg2) { return get(key, new Object[] { arg1, arg2 }); } /** * Obtains the translation for the specified message key. The * specified arguments are substituted into the translated string. * *

See {@link MessageFormat} for more information on how the * substitution is performed. If a translation message does not exist * for the specified key, an error is logged and the key itself (plus * the arguments) is returned so that the caller need not worry about * handling a null response. */ public String get (String key, Object arg1, Object arg2, Object arg3) { return get(key, new Object[] { arg1, arg2, arg3 }); } /** * Obtains the translation for the specified message key. The * specified arguments are substituted into the translated string. * *

See {@link MessageFormat} for more information on how the * substitution is performed. If a translation message does not exist * for the specified key, an error is logged and the key itself (plus * the arguments) is returned so that the caller need not worry about * handling a null response. */ public String get (String key, Object[] args) { // if this is a qualified key, we need to pass the buck to the // appropriate message bundle if (key.startsWith(QUAL_PREFIX)) { MessageBundle qbundle = _msgmgr.getBundle(getBundle(key)); return qbundle.get(getUnqualifiedKey(key), args); } String msg = getResourceString(key); return (msg != null) ? MessageFormat.format(msg, args) : (key + StringUtil.toString(args)); } /** * Obtains the translation for the specified compound message key. A * compound key contains the message key followed by a tab separated * list of message arguments which will be subsituted into the * translation string. * *

See {@link MessageFormat} for more information on how the * substitution is performed. If a translation message does not exist * for the specified key, an error is logged and the key itself (plus * the arguments) is returned so that the caller need not worry about * handling a null response. */ public String xlate (String compoundKey) { // if this is a qualified key, we need to pass the buck to the // appropriate message bundle; we have to do it here because we // want the compound arguments of this key to be translated in the // context of the containing message bundle qualification if (compoundKey.startsWith(QUAL_PREFIX)) { MessageBundle qbundle = _msgmgr.getBundle(getBundle(compoundKey)); return qbundle.xlate(getUnqualifiedKey(compoundKey)); } // to be more efficient about creating unnecessary objects, we // do some checking before splitting int tidx = compoundKey.indexOf('|'); if (tidx == -1) { return get(compoundKey); } else { String key = compoundKey.substring(0, tidx); String argstr = compoundKey.substring(tidx+1); String[] args = StringUtil.split(argstr, "|"); // unescape and translate the arguments for (int i = 0; i < args.length; i++) { // if the argument is tainted, do no further translation // (it might contain |s or other fun stuff) if (args[i].startsWith(TAINT_CHAR)) { args[i] = unescape(args[i].substring(1)); } else { args[i] = xlate(unescape(args[i])); } } return get(key, args); } } /** * Call this to "taint" any string that has been entered by an entity * outside the application so that the translation code knows not to * attempt to translate this string when doing recursive translations * (see {@link #xlate}). */ public static String taint (String text) { return TAINT_CHAR + text; } /** * Composes a message key with an array of arguments. The message can * subsequently be translated in a single call using {@link #xlate}. */ public static String compose (String key, String[] args) { StringBuffer buf = new StringBuffer(); buf.append(key); buf.append('|'); for (int i = 0; i < args.length; i++) { if (i > 0) { buf.append('|'); } // escape the string while adding to the buffer String arg = (args[i] == null) ? "" : args[i]; int alength = arg.length(); for (int p = 0; p < alength; p++) { char ch = arg.charAt(p); if (ch == '|') { buf.append("\\!"); } else if (ch == '\\') { buf.append("\\\\"); } else { buf.append(ch); } } } return buf.toString(); } /** * Unescapes characters that are escaped in a call to compose. */ protected static String unescape (String value) { int bsidx = value.indexOf('\\'); if (bsidx == -1) { return value; } StringBuffer buf = new StringBuffer(); int vlength = value.length(); for (int i = 0; i < vlength; i++) { char ch = value.charAt(i); if (ch != '\\') { buf.append(ch); } else if (i < vlength-1) { // look at the next character ch = value.charAt(++i); buf.append((ch == '!') ? '|' : ch); } else { buf.append(ch); } } return buf.toString(); } /** * A convenience method for calling {@link #compose(String,String[])} * with a single argument. */ public static String compose (String key, String arg) { return compose(key, new String[] { arg }); } /** * A convenience method for calling {@link #compose(String,String[])} * with two arguments. */ public static String compose (String key, String arg1, String arg2) { return compose(key, new String[] { arg1, arg2 }); } /** * A convenience method for calling {@link #compose(String,String[])} * with three arguments. */ public static String compose ( String key, String arg1, String arg2, String arg3) { return compose(key, new String[] { arg1, arg2, arg3 }); } /** * A convenience method for calling {@link #compose(String,String[])} * with a single argument that will be automatically tainted (see * {@link #taint}). */ public static String tcompose (String key, String arg) { return compose(key, new String[] { taint(arg) }); } /** * A convenience method for calling {@link #compose(String,String[])} * with two arguments that will be automatically tainted (see {@link * #taint}). */ public static String tcompose (String key, String arg1, String arg2) { return compose(key, new String[] { taint(arg1), taint(arg2) }); } /** * A convenience method for calling {@link #compose(String,String[])} * with three arguments that will be automatically tainted (see {@link * #taint}). */ public static String tcompose ( String key, String arg1, String arg2, String arg3) { return compose(key, new String[] { taint(arg1), taint(arg2), taint(arg3) }); } /** * A convenience method for calling {@link #compose(String,String[])} * with an array of arguments that will be automatically tainted (see * {@link #taint}). */ public static String tcompose (String key, String[] args) { int acount = args.length; String[] targs = new String[acount]; for (int ii = 0; ii < acount; ii++) { targs[ii] = taint(args[ii]); } return compose(key, targs); } /** * Returns a fully qualified message key which, when translated by * some other bundle, will know to resolve and utilize the supplied * bundle to translate this particular key. */ public static String qualify (String bundle, String key) { // sanity check if (bundle.indexOf(QUAL_PREFIX) != -1 || bundle.indexOf(QUAL_SEP) != -1 || key.indexOf(QUAL_PREFIX) != -1) { String errmsg = "Message bundle may not contain '" + QUAL_PREFIX + "' or '" + QUAL_SEP + "' and message key may not contain '" + QUAL_PREFIX + "' [bundle=" + bundle + ", key=" + key + "]"; throw new IllegalArgumentException(errmsg); } return QUAL_PREFIX + bundle + QUAL_SEP + key; } /** * Returns the bundle name from a fully qualified message key. * * @see #qualify */ public static String getBundle (String qualifiedKey) { if (!qualifiedKey.startsWith(QUAL_PREFIX)) { throw new IllegalArgumentException( qualifiedKey + " is not a fully qualified message key."); } int qsidx = qualifiedKey.indexOf(QUAL_SEP); if (qsidx == -1) { throw new IllegalArgumentException( qualifiedKey + " is not a valid fully qualified key."); } return qualifiedKey.substring(QUAL_PREFIX.length(), qsidx); } /** * Returns the unqualified portion of the key from a fully qualified * message key. * * @see #qualify */ public static String getUnqualifiedKey (String qualifiedKey) { if (!qualifiedKey.startsWith(QUAL_PREFIX)) { throw new IllegalArgumentException( qualifiedKey + " is not a fully qualified message key."); } int qsidx = qualifiedKey.indexOf(QUAL_SEP); if (qsidx == -1) { throw new IllegalArgumentException( qualifiedKey + " is not a valid fully qualified key."); } return qualifiedKey.substring(qsidx+1); } /** The message manager via whom we'll resolve fully qualified * translation strings. */ protected MessageManager _msgmgr; /** The path that identifies the resource bundle we are using to * obtain our messages. */ protected String _path; /** The resource bundle from which we obtain our messages. */ protected ResourceBundle _bundle; /** Our parent bundle if we're not the global bundle. */ protected MessageBundle _parent; /** Text prefixed by this character will be considered tainted when * doing recursive translations and won't be translated. */ protected static final String TAINT_CHAR = "~"; // protected static final String TAINT_CHAR = '~'; /** Used to mark fully qualified message keys. */ protected static final String QUAL_PREFIX = "%"; /** Used to separate the bundle qualifier from the message key in a * fully qualified message key. */ protected static final String QUAL_SEP = ":"; }