diff --git a/projects/samskivert/src/java/com/samskivert/text/MessageUtil.java b/projects/samskivert/src/java/com/samskivert/text/MessageUtil.java
new file mode 100644
index 00000000..d47031cd
--- /dev/null
+++ b/projects/samskivert/src/java/com/samskivert/text/MessageUtil.java
@@ -0,0 +1,241 @@
+//
+// $Id: MessageUtil.java,v 1.1 2003/12/11 06:32:18 mdb Exp $
+
+package com.samskivert.text;
+
+import com.samskivert.util.StringUtil;
+
+/**
+ * Utility functions for translation string handling.
+ */
+public class MessageUtil
+{
+ /** Text prefixed by this character will be considered tainted when
+ * doing recursive translations and won't be translated. */
+ public static final String TAINT_CHAR = "~";
+
+ /** Used to mark fully qualified message keys. */
+ public static final String QUAL_PREFIX = "%";
+
+ /** Used to separate the bundle qualifier from the message key in a
+ * fully qualified message key. */
+ public static final String QUAL_SEP = ":";
+
+ /**
+ * 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.
+ */
+ public 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) {
+ String errmsg = "Message bundle may not contain '" + QUAL_PREFIX +
+ "' or '" + QUAL_SEP + "' [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);
+ }
+
+ /**
+ * Used to escape single quotes so that they are not interpreted by
+ * {@link MessageFormat}. As we assume all single quotes are to be
+ * escaped, we cannot use the characters { and
+ * } in our translation strings, but this is a small
+ * price to pay to have to differentiate between messages that will
+ * and won't eventually be parsed by a {@link MessageFormat} instance.
+ */
+ protected static String escape (String message)
+ {
+ return StringUtil.replace(message, "'", "''");
+ }
+}