Add support for selecting messages based on arbitrary values, such
as gender of a placeholder.  Also, add support for more than one
@PluralCount / @Select on a given message, in any mix.

Patch by: jat
Review by: pdr

Review at http://gwt-code-reviews.appspot.com/1246801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9483 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/i18n/client/Messages.java b/user/src/com/google/gwt/i18n/client/Messages.java
index f7478ca..b0273cf 100644
--- a/user/src/com/google/gwt/i18n/client/Messages.java
+++ b/user/src/com/google/gwt/i18n/client/Messages.java
@@ -173,6 +173,84 @@
 public interface Messages extends LocalizableResource {
 
   /**
+   * Provides alternate forms of a message, such as are needed when plural
+   * forms are used or a placeholder has known gender. The selection of which
+   * form to use is based on the value of the arguments marked
+   * PluralCount and/or Select.
+   * 
+   * <p>Example:
+   * <code><pre>
+   *   &#64;DefaultMessage("You have {0} widgets.")
+   *   &#64;AlternateMessage({"one", "You have one widget.")
+   *   String example(&#64;PluralCount int count);
+   * </pre></code>
+   * </p>
+   * 
+   * <p>If multiple {@link PluralCount} or {@link Select} parameters are
+   * supplied, the forms for each, in the order they appear in the parameter
+   * list, are supplied separated by a vertical bar ("|").  Example:
+   * <code><pre>
+   *   &#64;DefaultMessage("You have {0} messages and {1} notifications.")
+   *   &#64;AlternateMessage({
+   *       "=0|=0", "You have no messages or notifications."
+   *       "=0|one", "You have a notification."
+   *       "one|=0", "You have a message."
+   *       "one|one", "You have one message and one notification."
+   *       "other|one", "You have {0} messages and one notification."
+   *       "one|other", "You have one message and {1} notifications."
+   *   })
+   *   String messages(&#64;PluralCount int msgCount,
+   *       &#64;PluralCount int notifyCount);
+   * </pre></code>
+   * 
+   * Note that the number of permutations can grow quickly, and that the default
+   * message is used when every {@link PluralCount} or {@link Select} would use
+   * the "other" value.
+   * </p>
+   */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.METHOD)
+  @Documented
+  public @interface AlternateMessage {
+
+    /**
+     * An array of pairs of strings containing the strings for different forms.
+     * 
+     * Each pair is the name of a form followed by the string in the source
+     * locale for that form.  Each form name is the name of a plural form if
+     * {@link PluralCount} is used, or the matching value if {@link Select} is
+     * used.  An example for a locale that has "none", "one", and "other" plural
+     * forms:
+     * 
+     * <code><pre>
+     * &#64;DefaultMessage("{0} widgets")
+     * &#64;AlternateMessage({"none", "No widgets", "one", "One widget"})
+     * </pre>
+     * 
+     * Note that the plural form "other" gets the translation specified in
+     * {@code &#64;DefaultMessage}, as does any {@code &#64;Select} value not
+     * listed. 
+     * 
+     * If more than one way of selecting a translation exists, they will be
+     * combined, separated with {@code |}, in the order they are supplied as
+     * arguments in the method.  For example:
+     * <code><pre>
+     *   &#64;DefaultMessage("{0} gave away their {2} widgets")
+     *   &#64;AlternateMesssage({
+     *     "MALE|other", "{0} gave away his {2} widgets",
+     *     "FEMALE|other", "{0} gave away her {2} widgets",
+     *     "MALE|one", "{0} gave away his widget",
+     *     "FEMALE|one", "{0} gave away her widget",
+     *     "other|one", "{0} gave away their widget",
+     *   })
+     *   String giveAway(String name, &#64;Select Gender gender,
+     *       &#64;PluralCount int count);
+     * </pre></code>
+     */
+    String[] value();
+  }
+
+  /**
    * Default text to be used if no translation is found (and also used as the
    * source for translation). Format should be that expected by
    * {@link java.text.MessageFormat}.
@@ -265,7 +343,7 @@
    * <p>Example:
    * <code><pre>
    *   &#64;DefaultMessage("You have {0} widgets.")
-   *   &#64;PluralText({"one", "You have one widget.")
+   *   &#64;AlternateMessage({"one", "You have one widget."})
    *   String example(&#64;PluralCount int count)
    * </pre></code>
    * </p>
@@ -297,10 +375,13 @@
    *   String example(&#64;PluralCount int count)
    * </pre></code>
    * </p>
+   * 
+   * @deprecated use {@link AlternateMessage} instead
    */
   @Retention(RetentionPolicy.RUNTIME)
   @Target(ElementType.METHOD)
   @Documented
+  @Deprecated
   public @interface PluralText {
 
     /**
@@ -321,4 +402,30 @@
      */
     String[] value();
   }
+
+  /**
+   * Provides multiple forms based on a dynamic parameter.
+   * 
+   * This annotation is applied to a single parameter of a Messages subinterface
+   * and indicates that parameter is to be used to choose the proper form of the
+   * message. The parameter chosen must be of type Enum, String, or a primitive
+   * numeric type.  This is frequently used to get proper gender for
+   * translations to languages where surrounding words depend on the gender of
+   * a person or noun.  This also marks the parameter as {@link Optional}.
+   * 
+   * <p>Example:
+   * <code><pre>
+   *   &#64;DefaultMessage("{0} likes their widgets.")
+   *   &#64;AlternateMessage({
+   *       "FEMALE", "{0} likes her widgets.",
+   *       "MALE", "{0} likes his widgets.",
+   *   })
+   *   String example(String name, &#64;Select Gender gender)
+   * </pre></code>
+   * </p>
+   */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  public @interface Select {
+  }
 }
diff --git a/user/src/com/google/gwt/i18n/rebind/AnnotationsResource.java b/user/src/com/google/gwt/i18n/rebind/AnnotationsResource.java
index 37f8218..037ac37 100644
--- a/user/src/com/google/gwt/i18n/rebind/AnnotationsResource.java
+++ b/user/src/com/google/gwt/i18n/rebind/AnnotationsResource.java
@@ -26,6 +26,8 @@
 import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
 import com.google.gwt.core.ext.typeinfo.JRawType;
 import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.i18n.client.Messages.AlternateMessage;
+import com.google.gwt.i18n.client.Messages.Select;
 import com.google.gwt.i18n.client.PluralRule;
 import com.google.gwt.i18n.client.Constants.DefaultBooleanValue;
 import com.google.gwt.i18n.client.Constants.DefaultDoubleValue;
@@ -82,6 +84,7 @@
     public String name;
     public boolean optional;
     public Class<? extends PluralRule> pluralRuleClass;
+    public boolean isSelect;
 
     public ArgumentInfo(String name) {
       this.name = name;
@@ -99,11 +102,11 @@
     }
 
     public String getForm(String form) {
-      return form == null ? entry.text : entry.pluralText.get(form);
+      return form == null ? entry.text : entry.altText.get(form);
     }
 
     public Collection<String> getForms() {
-      return entry.pluralText.keySet();
+      return entry.altText.keySet();
     }
 
     public String getKey() {
@@ -123,26 +126,26 @@
     public ArrayList<ArgumentInfo> arguments;
     public String description;
     public String meaning;
-    public Map<String, String> pluralText;
+    public Map<String, String> altText;
     public String text;
 
     public MethodEntry(String text, String meaning) {
       this.text = text;
       this.meaning = meaning;
-      pluralText = new HashMap<String, String>();
+      altText = new HashMap<String, String>();
       arguments = new ArrayList<ArgumentInfo>();
     }
 
+    public void addAlternateText(String altForm, String altMessage) {
+      altText.put(altForm, altMessage);
+    }
+
     public ArgumentInfo addArgument(String argName) {
       ArgumentInfo argInfo = new ArgumentInfo(argName);
       arguments.add(argInfo);
       return argInfo;
     }
 
-    public void addPluralText(String form, String pluralFormText) {
-      pluralText.put(form, pluralFormText);
-    }
-
     @Override
     public String toString() {
       StringBuilder buf = new StringBuilder();
@@ -497,10 +500,26 @@
         if ((pluralForms.length & 1) != 0) {
           throw new AnnotationsError(
               "Odd number of strings supplied to @PluralText: must be"
-              + " pairs of form names and strings");
+              + " pairs of form names and messages");
         }
         for (int i = 0; i + 1 < pluralForms.length; i += 2) {
-          entry.addPluralText(pluralForms[i], pluralForms[i + 1]);
+          entry.addAlternateText(pluralForms[i], pluralForms[i + 1]);
+        }
+      }
+      AlternateMessage altMsg = method.getAnnotation(AlternateMessage.class);
+      if (altMsg != null) {
+        if (pluralText != null) {
+          throw new AnnotationsError("May not have both @AlternateMessage"
+              + " and @PluralText");
+        }
+        String[] altForms = altMsg.value();
+        if ((altForms.length & 1) != 0) {
+          throw new AnnotationsError(
+              "Odd number of strings supplied to @AlternateMessage: must be"
+              + " pairs of values and messages");
+        }
+        for (int i = 0; i + 1 < altForms.length; i += 2) {
+          entry.addAlternateText(altForms[i], altForms[i + 1]);
         }
       }
       for (JParameter param : method.getParameters()) {
@@ -517,6 +536,10 @@
         if (example != null) {
           argInfo.example = example.value();
         }
+        Select select = param.getAnnotation(Select.class);
+        if (select != null) {
+          argInfo.isSelect = true;
+        }
       }
     }
   }
@@ -545,7 +568,7 @@
   @Override
   public Collection<String> getExtensions(String key) {
     MethodEntry entry = map.get(key);
-    return entry == null ? new ArrayList<String>() : entry.pluralText.keySet();
+    return entry == null ? new ArrayList<String>() : entry.altText.keySet();
   }
 
   public String getMeaning(String key) {
@@ -560,7 +583,7 @@
       return null;
     }
     if (extension != null) {
-      return entry.pluralText.get(extension);
+      return entry.altText.get(extension);
     } else {
       return entry.text;
     }
diff --git a/user/src/com/google/gwt/i18n/rebind/MessageFormatParser.java b/user/src/com/google/gwt/i18n/rebind/MessageFormatParser.java
index 663bba0..b59afb4 100644
--- a/user/src/com/google/gwt/i18n/rebind/MessageFormatParser.java
+++ b/user/src/com/google/gwt/i18n/rebind/MessageFormatParser.java
@@ -470,7 +470,7 @@
 
   /**
    * Parse any arguments appended to a format. The syntax is:
-   * format[:tag[=value][,tag[=value]]... for example: "date:tz=EST,showoffset"
+   * format[:tag[=value][:tag[=value]]... for example: "date:tz=EST:showoffset"
    *
    * @param format format value to parse
    * @param formatArgs map to add tag/value pairs to
diff --git a/user/src/com/google/gwt/i18n/rebind/MessagesImplCreator.java b/user/src/com/google/gwt/i18n/rebind/MessagesImplCreator.java
index 1e56586..1aa72ab 100644
--- a/user/src/com/google/gwt/i18n/rebind/MessagesImplCreator.java
+++ b/user/src/com/google/gwt/i18n/rebind/MessagesImplCreator.java
@@ -47,7 +47,7 @@
       throws UnableToCompleteException {
     super(logger, writer, localizableClass, resourceList, false);
     try {
-      MessagesMethodCreator creator = new MessagesMethodCreator(this);
+      MessagesMethodCreator creator = new MessagesMethodCreator(this, writer);
       JClassType stringClass = oracle.getType(String.class.getName());
       register(stringClass, creator);
       JClassType safeHtmlClass = oracle.getType(SafeHtml.class.getName());
diff --git a/user/src/com/google/gwt/i18n/rebind/MessagesMethodCreator.java b/user/src/com/google/gwt/i18n/rebind/MessagesMethodCreator.java
index 0168f07..19c27e7 100644
--- a/user/src/com/google/gwt/i18n/rebind/MessagesMethodCreator.java
+++ b/user/src/com/google/gwt/i18n/rebind/MessagesMethodCreator.java
@@ -19,6 +19,9 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JArrayType;
 import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JEnumConstant;
+import com.google.gwt.core.ext.typeinfo.JEnumType;
+import com.google.gwt.core.ext.typeinfo.JField;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.JParameter;
 import com.google.gwt.core.ext.typeinfo.JParameterizedType;
@@ -27,10 +30,12 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.i18n.client.DateTimeFormat;
 import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
+import com.google.gwt.i18n.client.Messages.AlternateMessage;
 import com.google.gwt.i18n.client.Messages.Offset;
 import com.google.gwt.i18n.client.Messages.Optional;
 import com.google.gwt.i18n.client.Messages.PluralCount;
 import com.google.gwt.i18n.client.Messages.PluralText;
+import com.google.gwt.i18n.client.Messages.Select;
 import com.google.gwt.i18n.client.NumberFormat;
 import com.google.gwt.i18n.client.PluralRule;
 import com.google.gwt.i18n.client.PluralRule.PluralForm;
@@ -49,21 +54,60 @@
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
 import com.google.gwt.user.rebind.AbstractMethodCreator;
+import com.google.gwt.user.rebind.SourceWriter;
 
 import org.apache.tapestry.util.text.LocalizedPropertiesLoader;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Creator for methods of the Messages interface.
  */
+@SuppressWarnings("deprecation") // for @PluralText
 class MessagesMethodCreator extends AbstractMethodCreator {
 
+  private abstract static class AlternateFormSelector {
+    protected final int argNumber;
+    protected final JType argType;
+    
+    public AlternateFormSelector(TreeLogger logger, int argNumber, JParameter[] params) {
+      this.argNumber = argNumber;
+      this.argType = params[argNumber].getType();
+    }
+
+    public abstract void generatePrepCode(SourceWriter out);
+
+    public abstract void generateSelectEnd(SourceWriter out);
+
+    public abstract void generateSelectMatchEnd(SourceWriter out, String value);
+
+    /**
+     * @param out
+     * @param logger
+     * @param value
+     * @throws UnableToCompleteException
+     */
+    public abstract void generateSelectMatchStart(SourceWriter out,
+        TreeLogger logger, String value) throws UnableToCompleteException;
+    
+    public abstract void generateSelectStart(SourceWriter out,
+        boolean exactMatches);
+
+    public abstract void issueWarnings(TreeLogger logger, JMethod m,
+        GwtLocale locale);
+  }
+
   /**
    * Implements {x,date...} references in MessageFormat.
    */
@@ -72,8 +116,7 @@
         StringGenerator out, Map<String, String> formatArgs, String subformat,
         String argName, JType argType, Parameters params) {
       if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
-        logger.log(
-            TreeLogger.ERROR, "Only java.util.Date acceptable for date format");
+        logger.log(TreeLogger.ERROR, "Only java.util.Date acceptable for date format");
         return true;
       }
       String tzParam = "";
@@ -82,49 +125,213 @@
         if (tzArg.startsWith("$")) {
           int paramNum = params.getParameterIndex(tzArg.substring(1));
           if (paramNum < 0) {
-            logger.log(
-                TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
+            logger.log(TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
             return true;
           } else if (!"com.google.gwt.i18n.client.TimeZone".equals(
               params.getParameter(paramNum).getType().getQualifiedSourceName())) {
-            logger.log(
-                TreeLogger.ERROR, "Currency code parameter must be TimeZone");
+            logger.log(TreeLogger.ERROR, "Currency code parameter must be TimeZone");
             return true;
           } else {
             tzParam = ", arg" + paramNum;
           }
         } else {
-          tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone("
-              + tzArg + ")";
+          tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone(" + tzArg + ")";
         }
       }
       if (subformat == null || "medium".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getMediumDateFormat()" + ".format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getMediumDateFormat()" + ".format(" + argName + tzParam + ")");
       } else if ("full".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getFullDateFormat().format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getFullDateFormat().format(" + argName + tzParam + ")");
       } else if ("long".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getLongDateFormat().format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getLongDateFormat().format(" + argName + tzParam + ")");
       } else if ("short".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getShortDateFormat()" + ".format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getShortDateFormat()" + ".format(" + argName + tzParam + ")");
       } else {
         logger.log(TreeLogger.WARN, "Use localdatetime format instead");
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getFormat(" + wrap(subformat) + ").format("
-                + argName + tzParam + ")");
+            dtFormatClassName + ".getFormat(" + wrap(subformat) + ").format(" + argName + tzParam
+                + ")");
       }
       return false;
     }
   }
 
   /**
+   * Comparator that ensures all exact value matches (=N) strings come before
+   * all non-exact matches.
+   */
+  private static class ExactValueComparator implements Comparator<String> {
+    
+    private static int compareOne(String a, String b) {
+      boolean aExact = a.startsWith("=");
+      boolean bExact = a.startsWith("=");
+      if (aExact != bExact) {
+        return aExact ? -1 : 1;
+      }
+      if (aExact) {
+        return a.substring(1).compareTo(b.substring(1));
+      } else {
+        return a.compareTo(b);
+      }
+    }
+
+    public int compare(String a, String b) {
+      String[] aSplit = a.split("\\|");
+      String[] bSplit = b.split("\\|");
+      int c = 0;
+      for (int i = 0; c == 0 && i < aSplit.length && i < bSplit.length; ++i) {
+        c = compareOne(aSplit[i], bSplit[i]);
+      }
+      if (c == 0 && aSplit.length != bSplit.length) {
+        c = aSplit.length < bSplit.length ? -1 : 1;
+      }
+      return c;
+    }
+  }
+
+  /**
+   * An {@link AlternateFormSelector} used with {@link Select}.
+   */
+  private static class GenericSelector extends AlternateFormSelector {
+
+    private final JEnumType enumType;
+    private final boolean isString;
+    private boolean startedIfChain;
+
+    /**
+     * @param logger
+     * @param m
+     * @param i
+     * @param params
+     * @throws UnableToCompleteException 
+     */
+    public GenericSelector(TreeLogger logger, JMethod m, int i,
+        JParameter[] params) throws UnableToCompleteException {
+      super(logger, i, params);
+      JPrimitiveType primType = argType.isPrimitive();
+      JClassType classType = argType.isClass();
+      JEnumType tempEnumType = null;
+      boolean tempIsString = false;
+      if (primType != null) {
+        if (primType == JPrimitiveType.DOUBLE
+            || primType == JPrimitiveType.FLOAT) {
+          throw error(logger, m.getName() + ": @Select arguments may only be"
+              + " integral primitives, boolean, enums, or String");
+        }
+      } else if (classType != null) {
+        tempEnumType = classType.isEnum();
+        tempIsString = "java.lang.String".equals(classType.getQualifiedSourceName());
+        if (tempEnumType == null && !tempIsString) {
+          throw error(logger, m.getName() + ": @Select arguments may only be"
+              + " integral primitives, boolean, enums, or String");
+        }
+      } else {
+        throw error(logger, m.getName() + ": @Select arguments may only be"
+            + " integral primitives, boolean, enums, or String");
+      }
+      enumType = tempEnumType;
+      isString = tempIsString;
+    }
+
+    @Override
+    public void generatePrepCode(SourceWriter out) {
+      if (enumType != null) {
+        out.println("int arg" + argNumber + "_ordinal = -1;");
+        out.println("if (arg" + argNumber + " != null) {");
+        out.indent();
+        out.println("arg" + argNumber + "_ordinal = arg" + argNumber
+            + ".ordinal();");
+        out.outdent();
+        out.println("}");
+      }
+    }
+
+    @Override
+    public void generateSelectEnd(SourceWriter out) {
+      if (!startedIfChain) {
+        out.outdent();
+      }
+      out.println("}");
+    }
+    
+    @Override
+    public void generateSelectMatchEnd(SourceWriter out, String value) {
+      if (!startedIfChain) {
+        out.println("break;");
+      }
+      out.outdent();
+    }
+
+    @Override
+    public void generateSelectMatchStart(SourceWriter out, TreeLogger logger,
+        String value) throws UnableToCompleteException {
+      if (isString) {
+        if (startedIfChain) {
+          out.print("} else ");
+        } else {
+          startedIfChain = true;
+        }
+        if ("other".equals(value)) {
+          out.println("{  // other");
+        } else {
+          value = value.replace("\"", "\\\"");
+          out.println("if (\"" + value + "\".equals(arg" + argNumber
+              + ")) {");
+        }
+      } else {
+        if ("other".equals(value)) {
+          out.println("default:  // other");
+        } else if (enumType != null) {
+          JField field = enumType.findField(value);
+          JEnumConstant enumConstant = null;
+          if (field != null) {
+            enumConstant = field.isEnumConstant();
+          }
+          if (field == null || enumConstant == null) {
+            throw error(logger, "'" + value + "' is not a valid value of "
+                + enumType.getQualifiedSourceName() + " or 'other'");
+          }
+          out.println("case " + enumConstant.getOrdinal() + ":  // " + value);
+        } else {
+          long longVal;
+          try {
+            longVal = Long.parseLong(value);
+          } catch (NumberFormatException e) {
+            throw error(logger, "'" + value + "' is not a valid numeric value",
+                e);
+          }
+          out.println("case " + longVal + ":");
+        }
+      }
+      out.indent();
+    }
+
+    @Override
+    public void generateSelectStart(SourceWriter out, boolean exactMatches) {
+      // ignore exactMatches, so "=VALUE" is the same as "VALUE"
+      if (isString) {
+        startedIfChain = false;
+        return;
+      }
+      String suffix = "";
+      if (enumType != null) {
+        suffix = "_ordinal";
+      }
+      out.println("switch (arg" + argNumber + suffix + ") {");
+      out.indent();
+    }
+
+    @Override
+    public void issueWarnings(TreeLogger logger, JMethod m, GwtLocale locale) {
+      // nothing to warn about
+    }
+  }
+
+  /**
    * Interface used to abstract away differences between accessing an array and
    * a list.
    */
@@ -181,17 +388,20 @@
   private static class LocalDateTimeFormatter implements ValueFormatter {
     private static final String PREDEF = "predef:";
 
-    public boolean format(TreeLogger logger, GwtLocale locale,
-        StringGenerator out, Map<String, String> formatArgs, String subformat,
-        String argName, JType argType, Parameters params) {
+    public boolean format(TreeLogger logger,
+        GwtLocale locale,
+        StringGenerator out,
+        Map<String, String> formatArgs,
+        String subformat,
+        String argName,
+        JType argType,
+        Parameters params) {
       if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
-        logger.log(TreeLogger.ERROR,
-            "Only java.util.Date acceptable for localdatetime format");
+        logger.log(TreeLogger.ERROR, "Only java.util.Date acceptable for localdatetime format");
         return true;
       }
       if (subformat == null || subformat.length() == 0) {
-        logger.log(TreeLogger.ERROR,
-            "localdatetime format requires a skeleton pattern");
+        logger.log(TreeLogger.ERROR, "localdatetime format requires a skeleton pattern");
         return true;
       }
       String tzParam = "";
@@ -200,53 +410,46 @@
         if (tzArg.startsWith("$")) {
           int paramNum = params.getParameterIndex(tzArg.substring(1));
           if (paramNum < 0) {
-            logger.log(
-                TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
+            logger.log(TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
             return true;
           } else if (!"com.google.gwt.i18n.client.TimeZone".equals(
               params.getParameter(paramNum).getType().getQualifiedSourceName())) {
-            logger.log(
-                TreeLogger.ERROR, "tz parameter must be of type TimeZone");
+            logger.log(TreeLogger.ERROR, "tz parameter must be of type TimeZone");
             return true;
           } else {
             tzParam = ", arg" + paramNum;
           }
         } else {
-          tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone("
-              + tzArg + ")";
+          tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone(" + tzArg + ")";
         }
       }
       if (subformat.startsWith(PREDEF)) {
         // TODO(jat): error checking/logging
         PredefinedFormat predef;
         try {
-          predef = PredefinedFormat.valueOf(
-              subformat.substring(PREDEF.length()));
+          predef = PredefinedFormat.valueOf(subformat.substring(PREDEF.length()));
         } catch (IllegalArgumentException e) {
-          logger.log(TreeLogger.ERROR,
-              "Unrecognized predefined format '" + subformat + "'");
+          logger.log(TreeLogger.ERROR, "Unrecognized predefined format '" + subformat + "'");
           return true;
         }
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getFormat(" + PredefinedFormat.class.getName()
-                + "." + predef.toString() + ").format(" + argName + tzParam
-                + ")");
+            dtFormatClassName + ".getFormat(" + PredefinedFormat.class.getName() + "."
+                + predef.toString() + ").format(" + argName + tzParam + ")");
         return false;
       }
       DateTimePatternGenerator dtpg = new DateTimePatternGenerator(locale);
       try {
         String pattern = dtpg.getBestPattern(subformat);
         if (pattern == null) {
-          logger.log(TreeLogger.ERROR,
-              "Invalid localdatetime skeleton pattern \"" + subformat + "\"");
+          logger.log(
+              TreeLogger.ERROR, "Invalid localdatetime skeleton pattern \"" + subformat + "\"");
           return true;
         }
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getFormat(" + wrap(pattern) + ").format("
-                + argName + tzParam + ")");
+            dtFormatClassName + ".getFormat(" + wrap(pattern) + ").format(" + argName + tzParam
+                + ")");
       } catch (IllegalArgumentException e) {
-        logger.log(TreeLogger.ERROR,
-            "Unable to parse '" + subformat + ": " + e.getMessage());
+        logger.log(TreeLogger.ERROR, "Unable to parse '" + subformat + ": " + e.getMessage());
         return true;
       }
       return false;
@@ -258,29 +461,30 @@
    */
   private static class NumberFormatter implements ValueFormatter {
 
-    public boolean format(TreeLogger logger, GwtLocale locale,
-        StringGenerator out, Map<String, String> formatArgs, String subformat,
-        String argName, JType argType, Parameters params) {
+    public boolean format(TreeLogger logger,
+        GwtLocale locale,
+        StringGenerator out,
+        Map<String, String> formatArgs,
+        String subformat,
+        String argName,
+        JType argType,
+        Parameters params) {
       JPrimitiveType argPrimType = argType.isPrimitive();
       if (argPrimType != null) {
-        if (argPrimType == JPrimitiveType.BOOLEAN
-            || argPrimType == JPrimitiveType.VOID) {
-          logger.log(
-              TreeLogger.ERROR, "Illegal argument type for number format");
+        if (argPrimType == JPrimitiveType.BOOLEAN || argPrimType == JPrimitiveType.VOID) {
+          logger.log(TreeLogger.ERROR, "Illegal argument type for number format");
           return true;
         }
       } else {
         JClassType classType = argType.isClass();
         if (classType == null) {
-          logger.log(
-              TreeLogger.ERROR, "Unexpected argument type for number format");
+          logger.log(TreeLogger.ERROR, "Unexpected argument type for number format");
           return true;
         }
         TypeOracle oracle = classType.getOracle();
         JClassType numberType = oracle.findType("java.lang.Number");
         if (!classType.isAssignableTo(numberType)) {
-          logger.log(TreeLogger.ERROR,
-              "Only Number subclasses may be formatted as a number");
+          logger.log(TreeLogger.ERROR, "Only Number subclasses may be formatted as a number");
           return true;
         }
       }
@@ -290,13 +494,11 @@
         if (curCode.startsWith("$")) {
           int paramNum = params.getParameterIndex(curCode.substring(1));
           if (paramNum < 0) {
-            logger.log(TreeLogger.ERROR,
-                "Unable to resolve curcode argument " + curCode);
+            logger.log(TreeLogger.ERROR, "Unable to resolve curcode argument " + curCode);
             return true;
           } else if (!"java.lang.String".equals(
               params.getParameter(paramNum).getType().getQualifiedSourceName())) {
-            logger.log(
-                TreeLogger.ERROR, "Currency code parameter must be String");
+            logger.log(TreeLogger.ERROR, "Currency code parameter must be String");
             return true;
           } else {
             curCodeParam = "arg" + paramNum;
@@ -313,8 +515,8 @@
             numFormatClassName + ".getIntegerFormat().format(" + argName + ")");
       } else if ("currency".equals(subformat)) {
         out.appendStringValuedExpression(
-            numFormatClassName + ".getCurrencyFormat(" + curCodeParam
-                + ").format(" + argName + ")");
+            numFormatClassName + ".getCurrencyFormat(" + curCodeParam + ").format(" + argName
+                + ")");
       } else if ("percent".equals(subformat)) {
         out.appendStringValuedExpression(
             numFormatClassName + ".getPercentFormat().format(" + argName + ")");
@@ -323,8 +525,8 @@
           curCodeParam = ", " + curCodeParam;
         }
         out.appendStringValuedExpression(
-            numFormatClassName + ".getFormat(" + wrap(subformat) + curCodeParam
-                + ").format(" + argName + ")");
+            numFormatClassName + ".getFormat(" + wrap(subformat) + curCodeParam + ").format("
+                + argName + ")");
       }
       return false;
     }
@@ -337,7 +539,14 @@
   private interface Parameters {
 
     /**
-     * Returns the count of parameters.
+     * Allow generated code to take advantage of plural offsets (see
+     * {@link Offset}).
+     */
+    void enablePluralOffsets();
+
+    /**
+     * Return the count of parameters.
+     * @return the count of parameters 
      */
     int getCount();
 
@@ -358,6 +567,16 @@
     JParameter getParameter(String name);
 
     /**
+     * Return an expression to get the value of the requested parameter.  Note
+     * that for arrays or lists this will return an expression giving the count
+     * of items in the array or list.
+     *  
+     * @param i index of the paramter, 0 .. getCount() - 1
+     * @return the source of code to access the parameter value
+     */
+    String getParameterExpression(int i);
+
+    /**
      * Find the index of a parameter by name.
      *
      * @param name
@@ -370,10 +589,37 @@
 
     private JParameter[] params;
     private boolean[] seenFlag;
+    private int[] offset;
+    private boolean[] isList;
+    private boolean[] isArray;
+    private boolean enablePluralOffsets;
 
     public ParametersImpl(JParameter[] params, boolean[] seenFlag) {
       this.params = params;
       this.seenFlag = seenFlag;
+      int n = params.length;
+      offset = new int[n];
+      isList = new boolean[n];
+      isArray = new boolean[n];
+      for (int i = 0; i < n; ++i) {
+        Offset offsetAnnot = params[i].getAnnotation(Offset.class);
+        if (offsetAnnot != null) {
+          offset[i] = offsetAnnot.value();
+        }
+        JType type = params[i].getType();
+        if (type.isArray() != null) {
+          isArray[i] = true;
+        } else if (type.isInterface() != null) {
+          JClassType rawType = type.isInterface().getErasedType();
+          if ("java.util.List".equals(rawType.getQualifiedSourceName())) {
+            isList[i] = true;
+          }
+        }
+      }
+    }
+
+    public void enablePluralOffsets() {
+      enablePluralOffsets = true;
     }
 
     public int getCount() {
@@ -392,6 +638,24 @@
       return getParameter(getParameterIndex(name));
     }
 
+    public String getParameterExpression(int i) {
+      if (i < 0 || i >= params.length) {
+        return null;
+      }
+      String argName = "arg" + i;
+      seenFlag[i] = true;
+      if (enablePluralOffsets && offset[i] != 0) {
+        return argName + "_count";
+      }
+      if (isArray[i]) {
+        return argName + ".length"; 
+      }
+      if (isList[i]) {
+        return argName + ".size()";
+      }
+      return argName;
+    }
+
     public int getParameterIndex(String name) {
       for (int i = 0; i < params.length; ++i) {
         if (params[i].getName().equals(name)) {
@@ -403,16 +667,209 @@
   }
 
   /**
+   * An {@link AlternateFormSelector} used with {@link PluralCount}.
+   */
+  private static class PluralFormSelector extends AlternateFormSelector {
+    protected final String countSuffix;
+    protected final String listSuffix;
+    protected final Set<String> missingPluralForms;
+    protected final int pluralOffset;
+    protected final PluralRule pluralRule;
+    private boolean hasExactMatches;
+    private boolean inExactMatches;
+    
+    // used to generate unique case values for bogus plural forms
+    private int bogusCaseValue = 1000;
+    
+    public PluralFormSelector(TreeLogger logger, JMethod method, int argNumber,
+        JParameter[] params, GwtLocale locale)
+        throws UnableToCompleteException {
+      super(logger, argNumber, params);
+      PluralCount pluralCount = params[argNumber].getAnnotation(
+          PluralCount.class);
+      Class<? extends PluralRule> ruleClass = pluralCount.value();
+      if (ruleClass == PluralRule.class) {
+        ruleClass = DefaultRule.class;
+      }
+      pluralRule = createLocalizedPluralRule(logger,
+          method.getEnclosingType().getOracle(), ruleClass, locale);
+      missingPluralForms = new HashSet<String>();
+      for (PluralForm form : pluralRule.pluralForms()) {
+        if (form.getWarnIfMissing() && !"other".equals(form.getName())) {
+          missingPluralForms.add(form.getName());
+        }
+      }
+      
+      Offset offsetAnnot = params[argNumber].getAnnotation(Offset.class);
+      int offset = 0;
+      if (offsetAnnot != null) {
+        offset = offsetAnnot.value();
+      }
+      this.pluralOffset = offset;
+      boolean isArray = false;
+      boolean isList = false;
+      JPrimitiveType primType = argType.isPrimitive();
+      JClassType classType = argType.isInterface();
+      if (classType != null) {
+        classType = classType.getErasedType();
+        if ("java.util.List".equals(classType.getQualifiedSourceName())) {
+          isList = true;
+        } else {
+          classType = null;
+        }
+      }
+
+      JArrayType arrayType = argType.isArray();
+      if (arrayType != null) {
+        isArray = true;
+      }
+      if (!isList && !isArray && (primType == null
+          || (primType != JPrimitiveType.INT
+              && primType != JPrimitiveType.SHORT))) {
+        throw error(logger, method.getName()
+            + ": PluralCount parameter must be int, short, array, or List");
+      }
+      String tempListSuffix = "";
+      if (isList) {
+        tempListSuffix = ".size()";
+      } else if (isArray) {
+        tempListSuffix = ".length";
+      }
+      String tempCountSuffix = tempListSuffix;
+      if (isList || isArray || offset != 0) {
+        tempCountSuffix = "_count";
+      }
+      listSuffix = tempListSuffix;
+      countSuffix = tempCountSuffix;
+    }
+
+    @Override
+    public void generatePrepCode(SourceWriter out) {
+      // save a value with the count value, applying an offset if present
+      if (countSuffix.length() > 0) {
+        out.print("int arg" + argNumber + countSuffix + " = arg" + argNumber
+            + listSuffix);
+        if (pluralOffset != 0) {
+          out.print(" - " + pluralOffset);
+        }
+        out.println(";");
+      }
+      // save the selected plural form
+      // TODO(jat): cache instances of the same plural rule?
+      out.println("int arg" + argNumber + "_form = new "
+          + pluralRule.getClass().getCanonicalName()
+          + "().select(arg" + argNumber + countSuffix + ");");
+    }
+
+    @Override
+    public void generateSelectEnd(SourceWriter out) {
+      if (hasExactMatches && !inExactMatches) {
+        // undo extra nesting level
+        out.outdent();
+        out.println("}");
+        out.println("break;");
+        out.outdent();
+      }
+      out.outdent();
+      out.println("}");
+    }
+    
+    @Override
+    public void generateSelectMatchEnd(SourceWriter out, String value) {
+      out.println("break;");
+      out.outdent();
+    }
+
+    @Override
+    public void generateSelectMatchStart(SourceWriter out, TreeLogger logger,
+        String value) throws UnableToCompleteException {
+      missingPluralForms.remove(value);
+      if (value.startsWith("=")) {
+        try {
+          long val = Long.parseLong(value.substring(1));
+          out.println("case " + val + ":  // " + value);
+        } catch (NumberFormatException e) {
+          throw error(logger, "Exact match value '" + value
+              + "' must be integral", e);
+        }
+        out.indent();
+        return;
+      }
+      if (inExactMatches) {
+        /*
+         * If this is the first non-exact value, create a nested select that
+         * chooses the message based on the plural form only if no exact values
+         * matched.
+         */
+        inExactMatches = false;
+        out.println("default: // non-exact matches");
+        out.indent();
+        out.println("switch (arg" + argNumber + "_form) {");
+        out.indent();
+      }
+      if ("other".equals(value)) {
+        out.println("default: // other");
+        out.indent();
+        return;
+      }
+      PluralForm[] pluralForms = pluralRule.pluralForms();
+      for (int i = 0; i < pluralForms.length; ++i) {
+        if (pluralForms[i].getName().equals(value)) {
+          out.println("case " + i + ":  // " + value);
+          out.indent();
+          return;
+        }
+      }
+      logger.log(TreeLogger.WARN, "Plural form '" + value + "' unknown in "
+          + pluralRule.getClass().getCanonicalName() + ": ignoring");
+      // TODO(jat): perhaps return a failure instead, and let the called skip
+      // the nested selector code?  It gets complicated really quick though.
+      out.println("case " + (bogusCaseValue++) + ": // unknown plural form '"
+          + value + "'");
+      out.indent();
+    }
+
+    @Override
+    public void generateSelectStart(SourceWriter out, boolean hasExactMatches) {
+      this.hasExactMatches = hasExactMatches;
+      inExactMatches = hasExactMatches;
+      String suffix = hasExactMatches ? listSuffix : "_form";
+      out.println("switch (arg" + argNumber + suffix + ") {");
+      out.indent();
+    }
+
+    public PluralForm[] getPluralForms() {
+      return pluralRule.pluralForms();
+    }
+    
+    @Override
+    public void issueWarnings(TreeLogger logger, JMethod m, GwtLocale locale) {
+      if (!missingPluralForms.isEmpty()) {
+        // TODO(jat): avoid giving warnings for values that are not necessary
+        // due to exact value matches.  For example, in English there is no need
+        // for ONE if the =1 value was given, and it may be important to have
+        // the =1 value across all locales.
+        logger.log(TreeLogger.WARN, "In locale '" + locale
+            + "', required plural forms are missing: " + missingPluralForms);
+      }
+    }
+  }
+
+  /**
    * Implements {x,time...} references in MessageFormat.
    */
   private static class TimeFormatter implements ValueFormatter {
 
-    public boolean format(TreeLogger logger, GwtLocale locale,
-        StringGenerator out, Map<String, String> formatArgs, String subformat,
-        String argName, JType argType, Parameters params) {
+    public boolean format(TreeLogger logger,
+        GwtLocale locale,
+        StringGenerator out,
+        Map<String, String> formatArgs,
+        String subformat,
+        String argName,
+        JType argType,
+        Parameters params) {
       if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
-        logger.log(
-            TreeLogger.ERROR, "Only java.util.Date acceptable for date format");
+        logger.log(TreeLogger.ERROR, "Only java.util.Date acceptable for date format");
         return true;
       }
       String tzParam = "";
@@ -421,43 +878,36 @@
         if (tzArg.startsWith("$")) {
           int paramNum = params.getParameterIndex(tzArg.substring(1));
           if (paramNum < 0) {
-            logger.log(
-                TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
+            logger.log(TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
             return true;
           } else if (!"com.google.gwt.i18n.client.TimeZone".equals(
               params.getParameter(paramNum).getType().getQualifiedSourceName())) {
-            logger.log(
-                TreeLogger.ERROR, "Currency code parameter must be TimeZone");
+            logger.log(TreeLogger.ERROR, "Currency code parameter must be TimeZone");
             return true;
           } else {
             tzParam = ", arg" + paramNum;
           }
         } else {
-          tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone("
-              + tzArg + ")";
+          tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone(" + tzArg + ")";
         }
       }
       if (subformat == null || "medium".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getMediumTimeFormat().format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getMediumTimeFormat().format(" + argName + tzParam + ")");
       } else if ("full".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getFullTimeFormat().format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getFullTimeFormat().format(" + argName + tzParam + ")");
       } else if ("long".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getLongTimeFormat().format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getLongTimeFormat().format(" + argName + tzParam + ")");
       } else if ("short".equals(subformat)) {
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getShortTimeFormat().format(" + argName
-                + tzParam + ")");
+            dtFormatClassName + ".getShortTimeFormat().format(" + argName + tzParam + ")");
       } else {
         logger.log(TreeLogger.WARN, "Use localdatetime format instead");
         out.appendStringValuedExpression(
-            dtFormatClassName + ".getFormat(" + wrap(subformat) + ").format("
-                + argName + tzParam + ")");
+            dtFormatClassName + ".getFormat(" + wrap(subformat) + ").format(" + argName + tzParam
+                + ")");
       }
       return false;
     }
@@ -478,16 +928,20 @@
      * @param params argument list or null
      * @return true if a fatal error occurred (which will already be logged)
      */
-    boolean format(TreeLogger logger, GwtLocale locale, StringGenerator out,
-        Map<String, String> formatArgs, String subformat, String argName,
-        JType argType, Parameters params);
+    boolean format(TreeLogger logger,
+        GwtLocale locale,
+        StringGenerator out,
+        Map<String, String> formatArgs,
+        String subformat,
+        String argName,
+        JType argType,
+        Parameters params);
   }
 
   /**
    * Class names, in a refactor-friendly manner.
    */
-  private static final String dtFormatClassName =
-      DateTimeFormat.class.getCanonicalName();
+  private static final String dtFormatClassName = DateTimeFormat.class.getCanonicalName();
 
   /**
    * Fully-qualified class name of the SafeHtml interface.
@@ -497,17 +951,14 @@
   /**
    * Fully-qualified class name of the SafeHtmlBuilder class.
    */
-  public static final String SAFE_HTML_BUILDER_FQCN =
-      SafeHtmlBuilder.class.getCanonicalName();
+  public static final String SAFE_HTML_BUILDER_FQCN = SafeHtmlBuilder.class.getCanonicalName();
 
   /**
    * Map of supported formats.
    */
-  private static Map<String, ValueFormatter> formatters = new HashMap<
-      String, ValueFormatter>();
+  private static Map<String, ValueFormatter> formatters = new HashMap<String, ValueFormatter>();
 
-  private static final String numFormatClassName =
-      NumberFormat.class.getCanonicalName();
+  private static final String numFormatClassName = NumberFormat.class.getCanonicalName();
 
   /*
    * Register supported formats.
@@ -519,252 +970,6 @@
     formatters.put("localdatetime", new LocalDateTimeFormatter());
   }
 
-  private final Map<GwtLocale, Map<String, String>> listPatternCache;
-
-  /**
-   * Constructor for <code>MessagesMethodCreator</code>.
-   *
-   * @param classCreator associated class creator
-   */
-  public MessagesMethodCreator(AbstractGeneratorClassCreator classCreator) {
-    super(classCreator);
-    listPatternCache = new HashMap<GwtLocale, Map<String, String>>();
-  }
-
-  @Override
-  public void createMethodFor(TreeLogger logger, JMethod m, String key,
-      ResourceList resourceList, GwtLocale locale)
-      throws UnableToCompleteException {
-    ResourceEntry resourceEntry = resourceList.getEntry(key);
-    if (resourceEntry == null) {
-      throw new MissingResourceException(key, resourceList);
-    }
-    JParameter[] params = m.getParameters();
-    int pluralParamIndex = -1;
-    Class<? extends PluralRule> ruleClass = null;
-    int numParams = params.length;
-    boolean[] seenFlags = new boolean[numParams];
-    final Parameters paramsAccessor = new ParametersImpl(params, seenFlags);
-
-    int pluralOffset = 0;
-    String pluralSuffix = "";
-    // See if any parameter is tagged as a PluralCount parameter.
-    for (int i = 0; i < numParams; ++i) {
-      PluralCount pluralCount = params[i].getAnnotation(PluralCount.class);
-      if (pluralCount != null) {
-        if (pluralParamIndex >= 0) {
-          throw error(logger,
-              m.getName() + ": there can only be one PluralCount parameter");
-        }
-        JType paramType = params[i].getType();
-        boolean isArray = false;
-        boolean isList = false;
-        JPrimitiveType primType = paramType.isPrimitive();
-        JClassType classType = paramType.isInterface();
-        if (classType != null) {
-          classType = classType.getErasedType();
-          if ("java.util.List".equals(classType.getQualifiedSourceName())) {
-            isList = true;
-          } else {
-            classType = null;
-          }
-        }
-
-        JArrayType arrayType = paramType.isArray();
-        if (arrayType != null) {
-          isArray = true;
-        }
-        if (!isList && !isArray
-            && (primType == null || (primType != JPrimitiveType.INT
-                && primType != JPrimitiveType.SHORT))) {
-          throw error(logger, m.getName()
-              + ": PluralCount parameter must be int, short, array, or List");
-        }
-        if (isList) {
-          pluralSuffix = ".size()";
-        } else if (isArray) {
-          pluralSuffix = ".length";
-        }
-        pluralParamIndex = i;
-        ruleClass = pluralCount.value();
-        Offset offset = params[i].getAnnotation(Offset.class);
-        if (offset != null) {
-          pluralOffset = offset.value();
-        }
-      }
-    }
-
-    boolean isSafeHtml = m.getReturnType().getQualifiedSourceName().equals(
-        SAFE_HTML_FQCN);
-
-    String template = resourceEntry.getForm(null);
-    if (template == null) {
-      logger.log(TreeLogger.ERROR,
-          "No default form for method " + m.getName() + "' in "
-              + m.getEnclosingType() + " for locale " + locale, null);
-      throw new UnableToCompleteException();
-    }
-    StringBuffer generated = new StringBuffer();
-    ArgumentChunk listArg = null;
-    JType elemType = null;
-    ListAccessor listAccessor = null;
-    try {
-      for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
-        if (chunk instanceof ArgumentChunk) {
-          ArgumentChunk argChunk = (ArgumentChunk) chunk;
-          if (argChunk.isList()) {
-            if (listArg != null) {
-              logger.log(TreeLogger.ERROR,
-                  "Only one list parameter supported in "
-                      + m.getEnclosingType().getSimpleSourceName() + "."
-                      + m.getName());
-              throw new UnableToCompleteException();
-            } else {
-              listArg = argChunk;
-              int listArgNum = argChunk.getArgumentNumber();
-              JType listType = params[listArgNum].getType();
-              JClassType classType = listType.isInterface();
-              if (classType != null) {
-                if ("java.util.List".equals(
-                    classType.getErasedType().getQualifiedSourceName())) {
-                  listAccessor = new ListAccessorList(listArgNum);
-                } else {
-                  logger.log(TreeLogger.ERROR,
-                          "Parameters formatted as lists must be declared as java.util.List or arrays in " + m.getEnclosingType().getSimpleSourceName() + "." + m.getName());
-                  throw new UnableToCompleteException();
-                }
-                JParameterizedType paramType = classType.isParameterized();
-                if (paramType != null) {
-                  elemType = paramType.getTypeArgs()[0];
-                } else {
-                  elemType = classType.getOracle().getJavaLangObject();
-                }
-              } else {
-                JArrayType arrayType = listType.isArray();
-                if (arrayType != null) {
-                  elemType = arrayType.getComponentType();
-                  listAccessor = new ListAccessorArray(listArgNum);
-                }
-              }
-            }
-          }
-        }
-      }
-    } catch (ParseException pe) {
-      logger.log(TreeLogger.ERROR, "Error parsing '" + template + "'", pe);
-      throw new UnableToCompleteException();
-    }
-
-    if (listArg != null) {
-      generateListFormattingCode(logger, locale, generated, listArg, elemType,
-          isSafeHtml, listAccessor, paramsAccessor);
-    }
-    if (ruleClass == null) {
-      if (m.getAnnotation(PluralText.class) != null) {
-        logger.log(TreeLogger.WARN,
-            "Unused @PluralText on "
-                + m.getEnclosingType().getSimpleSourceName() + "." + m.getName()
-                + "; did you intend to mark a @PluralCount parameter?", null);
-      }
-    } else {
-      if (ruleClass == PluralRule.class) {
-        ruleClass = DefaultRule.class;
-      }
-      PluralRule rule = createLocalizedPluralRule(
-          logger, m.getEnclosingType().getOracle(), ruleClass, locale);
-      logger.log(TreeLogger.TRACE,
-          "Using plural rule " + rule.getClass() + " for locale '" + locale
-              + "'", null);
-      boolean seenEqualsValue = false;
-      for (String form : resourceEntry.getForms()) {
-        if (form.startsWith("=")) {
-          int value = 0;
-          try {
-            value = Integer.parseInt(form.substring(1));
-          } catch (NumberFormatException e) {
-            logger.log(TreeLogger.WARN,
-                "Ignoring invalid value in plural form '" + form + "'", e);
-            continue;
-          }
-          if (!seenEqualsValue) {
-            generated.append(
-                "switch (arg" + pluralParamIndex + pluralSuffix + ") {\n");
-            seenEqualsValue = true;
-          }
-          generated.append("  case " + value + ": return ");
-          String pluralTemplate = resourceEntry.getForm(form);
-          generateString(logger, locale, pluralTemplate, paramsAccessor,
-              generated, isSafeHtml);
-          generated.append(";\n");
-        }
-      }
-      if (seenEqualsValue) {
-        generated.append("}\n");
-      }
-      boolean seenPluralForm = false;
-      StringBuilder pluralHeader = new StringBuilder();
-      pluralHeader.append(PluralRule.class.getCanonicalName());
-      pluralHeader.append(
-          " rule = new " + rule.getClass().getCanonicalName() + "();\n");
-      if (pluralOffset != 0) {
-        pluralHeader.append(
-            "arg" + pluralParamIndex + " -= " + pluralOffset + ";\n");
-      }
-      pluralHeader.append(
-          "switch (rule.select(arg" + pluralParamIndex + pluralSuffix
-              + ")) {\n");
-      PluralForm[] pluralForms = rule.pluralForms();
-      resourceList.setPluralForms(key, pluralForms);
-      // Skip default plural form (index 0); the fall-through case will handle
-      // it.
-      for (int i = 1; i < pluralForms.length; ++i) {
-        String pluralTemplate = resourceEntry.getForm(pluralForms[i].getName());
-        if (pluralTemplate != null) {
-          if (!seenPluralForm) {
-            generated.append(pluralHeader);
-            seenPluralForm = true;
-          }
-          generated.append("  // " + pluralForms[i].getName() + " - "
-              + pluralForms[i].getDescription() + "\n");
-          generated.append("  case " + i + ": return ");
-          generateString(logger, locale, pluralTemplate, paramsAccessor,
-              generated, isSafeHtml);
-          generated.append(";\n");
-        } else if (pluralForms[i].getWarnIfMissing()) {
-          if (!seenEqualsValue) {
-            // If we have seen a form "=n", assume the developer knows what
-            // they are doing and don't warn about plural forms that aren't
-            // used.
-            logger.log(TreeLogger.WARN,
-                "No plural form '" + pluralForms[i].getName()
-                    + "' defined for method '" + m.getName() + "' in "
-                    + m.getEnclosingType() + " for locale " + locale, null);
-          }
-        }
-      }
-      if (seenPluralForm) {
-        generated.append("}\n");
-      }
-    }
-    generated.append("return ");
-    generateString(
-        logger, locale, template, paramsAccessor, generated, isSafeHtml);
-
-    // Generate an error if any required parameter was not used somewhere.
-    for (int i = 0; i < numParams; ++i) {
-      if (!seenFlags[i]) {
-        Optional optional = params[i].getAnnotation(Optional.class);
-        if (optional == null) {
-          throw error(
-              logger, "Required argument " + i + " not present: " + template);
-        }
-      }
-    }
-
-    generated.append(';');
-    println(generated.toString());
-  }
-
   /**
    * Creates an instance of a locale-specific plural rule implementation.
    *
@@ -782,21 +987,21 @@
    *         of DefaultRule is used instead as a default of last resort.
    * @throws UnableToCompleteException if findDerivedClasses fails
    *
-   *           TODO: consider impact of possibly having multiple TypeOracles
+   *         TODO: consider impact of possibly having multiple TypeOracles
    */
-  private PluralRule createLocalizedPluralRule(TreeLogger logger,
-      TypeOracle oracle, Class<? extends PluralRule> ruleClass,
-      GwtLocale locale) throws UnableToCompleteException {
+  private static PluralRule createLocalizedPluralRule(
+      TreeLogger logger, TypeOracle oracle, Class<? extends PluralRule> ruleClass, GwtLocale locale)
+      throws UnableToCompleteException {
     String baseName = ruleClass.getCanonicalName();
     JClassType ruleJClassType = oracle.findType(baseName);
-        Map<String, JClassType> matchingClasses = LocalizableLinkageCreator.findDerivedClasses(logger, ruleJClassType);
+    Map<String, JClassType> matchingClasses =
+        LocalizableLinkageCreator.findDerivedClasses(logger, ruleJClassType);
     for (GwtLocale search : locale.getCompleteSearchList()) {
       JClassType localizedType = matchingClasses.get(search.toString());
       if (localizedType != null) {
         try {
           Class<?> testClass = Class.forName(
-              localizedType.getQualifiedBinaryName(), false,
-              PluralRule.class.getClassLoader());
+              localizedType.getQualifiedBinaryName(), false, PluralRule.class.getClassLoader());
           if (PluralRule.class.isAssignableFrom(testClass)) {
             return (PluralRule) testClass.newInstance();
           }
@@ -815,6 +1020,172 @@
     return new DefaultRule();
   }
 
+  private final Map<GwtLocale, Map<String, String>> listPatternCache;
+
+  private SourceWriter writer;
+
+  /**
+   * Constructor for <code>MessagesMethodCreator</code>.
+   *
+   * @param classCreator associated class creator
+   * @param writer 
+   */
+  public MessagesMethodCreator(AbstractGeneratorClassCreator classCreator,
+      SourceWriter writer) {
+    super(classCreator);
+    listPatternCache = new HashMap<GwtLocale, Map<String, String>>();
+    this.writer = writer;
+  }
+
+  @Override
+  public void createMethodFor(TreeLogger logger, JMethod m, String key,
+      ResourceList resourceList, GwtLocale locale)
+      throws UnableToCompleteException {
+    ResourceEntry resourceEntry = resourceList.getEntry(key);
+    if (resourceEntry == null) {
+      throw new MissingResourceException(key, resourceList);
+    }
+    JParameter[] params = m.getParameters();
+    boolean seenPluralCount = false;
+    boolean seenSelect = false;
+
+    int numParams = params.length;
+    List<AlternateFormSelector> selectors = new ArrayList<AlternateFormSelector>();
+    // See if any parameter is tagged as a PluralCount or Select parameter.
+    for (int i = 0; i < numParams; ++i) {
+      PluralCount pluralCount = params[i].getAnnotation(PluralCount.class);
+      Select select = params[i].getAnnotation(Select.class);
+      if (pluralCount != null && select != null) {
+        throw error(logger, params[i].getName() + " cannot be both @PluralCount"
+            + " and @Select");
+      }
+      AlternateFormSelector selector = null;
+      if (select != null) {
+        selector = new GenericSelector(logger, m, i, params);
+        seenSelect = true;
+      } else if (pluralCount != null) {
+        PluralFormSelector pluralSelector = new PluralFormSelector(logger, m, i,
+            params, locale);
+        selector = pluralSelector;
+        if (!seenPluralCount) {
+          // TODO(jat): what if we have different plural rules on the different
+          // forms?
+          resourceList.setPluralForms(key, pluralSelector.getPluralForms());
+        }
+        seenPluralCount = true;
+      }
+      if (selector != null) {
+        selectors.add(selector);
+      }
+    }
+
+    boolean[] seenFlags = new boolean[numParams];
+    final Parameters paramsAccessor = new ParametersImpl(params, seenFlags);
+    boolean isSafeHtml = m.getReturnType().getQualifiedSourceName().equals(
+        SAFE_HTML_FQCN);
+
+    String template = resourceEntry.getForm(null);
+    if (template == null) {
+      logger.log(TreeLogger.ERROR,"No default form for method " + m.getName()
+          + "' in " + m.getEnclosingType() + " for locale " + locale, null);
+      throw new UnableToCompleteException();
+    }
+
+    // Generate code to format any lists
+    // TODO(jat): handle messages with different list formats in alternate forms 
+    try {
+      for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
+        if (chunk instanceof ArgumentChunk) {
+          ArgumentChunk argChunk = (ArgumentChunk) chunk;
+          if (argChunk.isList()) {
+            ListAccessor listAccessor = null;
+            int listArgNum = argChunk.getArgumentNumber();
+            JType listType = params[listArgNum].getType();
+            JClassType classType = listType.isInterface();
+            JType elemType = null;
+            if (classType != null) {
+              if ("java.util.List".equals(
+                  classType.getErasedType().getQualifiedSourceName())) {
+                listAccessor = new ListAccessorList(listArgNum);
+              } else {
+                logger.log(TreeLogger.ERROR, "Parameters formatted as lists "
+                    + "must be declared as java.util.List or arrays in "
+                    + m.getEnclosingType().getSimpleSourceName() + "."
+                    + m.getName());
+                throw new UnableToCompleteException();
+              }
+              JParameterizedType paramType = classType.isParameterized();
+              if (paramType != null) {
+                elemType = paramType.getTypeArgs()[0];
+              } else {
+                elemType = classType.getOracle().getJavaLangObject();
+              }
+            } else {
+              JArrayType arrayType = listType.isArray();
+              if (arrayType != null) {
+                elemType = arrayType.getComponentType();
+                listAccessor = new ListAccessorArray(listArgNum);
+              }
+            }
+            generateListFormattingCode(logger, locale, argChunk,
+                elemType, isSafeHtml, listAccessor, paramsAccessor);
+          }
+        }
+      }
+    } catch (ParseException pe) {
+      throw error(logger, "Error parsing '" + template + "'", pe);
+    }
+
+    if (!seenPluralCount && !seenSelect 
+        && (m.getAnnotation(AlternateMessage.class) != null
+        || m.getAnnotation(PluralText.class) != null)) {
+      logger.log(TreeLogger.WARN, "Unused @AlternateMessage or @PluralText on "
+          + m.getEnclosingType().getSimpleSourceName() + "." + m.getName()
+          + "; did you intend to mark a @Select or @PluralCount parameter?",
+          null);
+    }
+    Collection<String> resourceForms = resourceEntry.getForms();
+    if (seenPluralCount) {
+      paramsAccessor.enablePluralOffsets();
+      writer.println(m.getReturnType().getParameterizedQualifiedSourceName()
+          + " returnVal = null;");
+      for (AlternateFormSelector selector : selectors) {
+        selector.generatePrepCode(writer);
+      }
+      
+      // sort forms so that all exact-value forms come first
+      String[] forms = resourceForms.toArray(new String[resourceForms.size()]);
+      Arrays.sort(forms, new ExactValueComparator());
+
+      generateMessageSelectors(logger, m, locale,
+          resourceEntry, selectors, paramsAccessor, isSafeHtml, forms);
+      for (AlternateFormSelector selector : selectors) {
+        selector.issueWarnings(logger, m, locale);
+      }
+      writer.println("if (returnVal != null) {");
+      writer.indent();
+      writer.println("return returnVal;");
+      writer.outdent();
+      writer.println("}");
+    }
+    writer.print("return ");
+    generateString(logger, locale, template, paramsAccessor, writer,
+        isSafeHtml);
+    writer.println(";");
+
+    // Generate an error if any required parameter was not used somewhere.
+    for (int i = 0; i < numParams; ++i) {
+      if (!seenFlags[i]) {
+        Optional optional = params[i].getAnnotation(Optional.class);
+        Select select = params[i].getAnnotation(Select.class);
+        if (optional == null && select == null) {
+          throw error(logger, "Required argument " + i + " not present: "
+              + template);
+        }
+      }
+    }
+  }
+
   private void formatArg(TreeLogger logger, GwtLocale locale,
       StringGenerator buf, ArgumentChunk argChunk, String argExpr,
       JType paramType, Parameters params) throws UnableToCompleteException {
@@ -832,13 +1203,11 @@
     }
     // no format specified or unknown format
     // have to ensure that the result is stringified if necessary
-    boolean isSafeHtmlTyped = SAFE_HTML_FQCN.equals(
-        paramType.getQualifiedSourceName());
+    boolean isSafeHtmlTyped = SAFE_HTML_FQCN.equals(paramType.getQualifiedSourceName());
     boolean isPrimitiveType = (paramType.isPrimitive() != null);
-    boolean needsConversionToString = !("java.lang.String".equals(
-        paramType.getQualifiedSourceName()));
-    buf.appendExpression(
-        argExpr, isSafeHtmlTyped, isPrimitiveType, needsConversionToString);
+    boolean needsConversionToString =
+        !("java.lang.String".equals(paramType.getQualifiedSourceName()));
+    buf.appendExpression(argExpr, isSafeHtmlTyped, isPrimitiveType, needsConversionToString);
   }
 
   /**
@@ -849,12 +1218,13 @@
    * @param listArg the {n,list,...} argument in the original format pattern
    * @param val0 the expression defining the {0} argument in the list pattern
    * @param val1 the expression defining the {1} argument in the list pattern
-   * @param elemType the element type of the list/array being rendered as a list   * @param isSafeHtml true if the resulting string is SafeHtml
+   * @param elemType the element type of the list/array being rendered as a list
+   *        * @param isSafeHtml true if the resulting string is SafeHtml
    * @param listPattern the list pattern to generate code for, ie "{0}, {1}"
    * @param formatSecond true if the {1} parameter needs to be formatted
    * @param params parameters passed to the Messages method call
    * @return a constructed string containing the code to implement the given
-   *     list pattern
+   *         list pattern
    * @throws UnableToCompleteException
    */
   private CharSequence formatListPattern(final TreeLogger logger,
@@ -869,8 +1239,7 @@
       for (TemplateChunk chunk : chunks) {
         chunk.accept(new DefaultTemplateChunkVisitor() {
           @Override
-          public void visit(ArgumentChunk argChunk)
-              throws UnableToCompleteException {
+          public void visit(ArgumentChunk argChunk) throws UnableToCompleteException {
             // The {0} argument in the list pattern always needs formatting,
             // but the {1} argument is the part of the list already rendered
             // (in either String of SafeHtml form) unless formatSecond is true.
@@ -885,7 +1254,7 @@
 
           @Override
           public void visit(StringChunk stringChunk)
-              throws UnableToCompleteException {
+          throws UnableToCompleteException {
             gen.appendStringLiteral(stringChunk.getString());
           }
         });
@@ -893,7 +1262,7 @@
     } catch (ParseException e) {
       logger.log(TreeLogger.ERROR,
           "Internal error: can't parse list pattern '" + listPattern
-              + "' for locale " + locale, e);
+          + "' for locale " + locale, e);
       throw new UnableToCompleteException();
     }
     gen.completeString();
@@ -905,79 +1274,166 @@
    *
    * @param logger logger to use for error/warning messages
    * @param locale locale we are generating code for
-   * @param generated a StringBuffer holding the generated code
    * @param listArg the {n,list,...} argument in the original format pattern
    * @param elemType the element type of the list/array being rendered as a list
    * @param isSafeHtml true if the resulting string is SafeHtml
    * @param listAccessor a way to access elements of the list type supplied by
-   *     the user
+   *        the user
    * @param params parameters passed to the Messages method call
    * @throws UnableToCompleteException
    */
   private void generateListFormattingCode(TreeLogger logger, GwtLocale locale,
-      StringBuffer generated, ArgumentChunk listArg, JType elemType,
+      ArgumentChunk listArg, JType elemType,
       boolean isSafeHtml, ListAccessor listAccessor, Parameters params)
       throws UnableToCompleteException {
     Map<String, String> listPatternParts = getListPatternParts(logger, locale);
     int listArgNum = listArg.getArgumentNumber();
-    generated.append(
-        "int arg" + listArgNum + "_size = " + listAccessor.getSize() + ";\n");
+    writer.println("int arg" + listArgNum + "_size = " + listAccessor.getSize()
+        + ";");
     if (isSafeHtml) {
-      generated.append(SafeHtml.class.getCanonicalName()).append(" arg").append(
-          listArgNum).append("_list = new ").append(
-          OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.class.getCanonicalName(
-          )).append("(\"\");\n");
+      writer.println(SafeHtml.class.getCanonicalName() + " arg" + listArgNum
+          + "_list = new "
+          + OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.class.getCanonicalName()
+          + "(\"\");");
     } else {
-      generated.append("String").append(" arg" + listArgNum
-          + "_list = \"\";\n");
+      writer.println("String arg" + listArgNum + "_list = \"\";");
     }
-    generated.append("switch (arg" + listArgNum + "_size) {\n");
+    writer.println("switch (arg" + listArgNum + "_size) {");
+    writer.indent();
     // TODO(jat): add support for special-cases besides 2 if CLDR ever adds them
     String pairPattern = listPatternParts.get("2");
     if (pairPattern != null) {
-      generated.append("case 2:\n");
-      generated.append("  arg" + listArgNum + "_list = ");
-      generated.append(
-          formatListPattern(logger, locale, listArg,
-              listAccessor.getElement("0"), listAccessor.getElement("1"),
-              elemType, isSafeHtml, pairPattern, true, params));
-      generated.append(";\n");
-      generated.append("  break;\n");
+      writer.println("case 2:");
+      writer.indent();
+      writer.println("  arg" + listArgNum + "_list = "
+          + formatListPattern(logger, locale, listArg,
+          listAccessor.getElement("0"), listAccessor.getElement("1"), elemType,
+          isSafeHtml, pairPattern, true, params) + ";");
+      writer.println("break;");
+      writer.outdent();
     }
-    generated.append("default:\n");
-    generated.append("  int i = arg" + listArgNum + "_size;\n");
-    generated.append("  if (i > 0) {\n");
-    generated.append("    arg" + listArgNum + "_list = ");
-    StringGenerator buf = new StringGenerator(generated, isSafeHtml);
+    writer.println("default:");
+    writer.indent();
+    writer.println("int i = arg" + listArgNum + "_size;");
+    writer.println("if (i > 0) {");
+    writer.indent();
+    StringBuffer outbuf = new StringBuffer();
+    StringGenerator buf = new StringGenerator(outbuf, isSafeHtml);
     formatArg(logger, locale, buf, listArg, listAccessor.getElement("--i"),
         elemType, params);
     buf.completeString();
-    generated.append(";\n");
-    generated.append("  }\n");
-    generated.append("  if (i > 0) {\n");
-    generated.append("    arg" + listArgNum + "_list = ");
-    generated.append(
-        formatListPattern(logger, locale, listArg,
-            listAccessor.getElement("--i"), "arg" + listArgNum + "_list",
-            elemType, isSafeHtml, listPatternParts.get("end"), false, params));
-    generated.append(";\n");
-    generated.append("  }\n");
-    generated.append("  while (i > 1) {\n");
-    generated.append("    arg" + listArgNum + "_list = ");
-    generated.append(formatListPattern(logger, locale, listArg,
-        listAccessor.getElement("--i"), "arg" + listArgNum + "_list",
-        elemType, isSafeHtml, listPatternParts.get("middle"), false, params));
-    generated.append("  ;\n");
-    generated.append("  }\n");
-    generated.append("  if (i > 0) {\n");
-    generated.append("    arg" + listArgNum + "_list = ");
-    generated.append(formatListPattern(logger, locale, listArg,
-        listAccessor.getElement("--i"), "arg" + listArgNum + "_list",
-        elemType, isSafeHtml, listPatternParts.get("start"), false, params));
-    generated.append(";\n");
-    generated.append("  }\n");
-    generated.append("  break;\n");
-    generated.append("}\n");
+    writer.println("arg" + listArgNum + "_list = " + outbuf + ";");
+    writer.outdent();
+    writer.println("}");
+    writer.println("if (i > 0) {");
+    writer.indent();
+    writer.println("arg" + listArgNum + "_list = "
+        + formatListPattern(logger, locale, listArg,
+        listAccessor.getElement("--i"), "arg" + listArgNum + "_list", elemType,
+        isSafeHtml, listPatternParts.get("end"), false, params) + ";");
+    writer.outdent();
+    writer.println("}");
+    writer.println("while (i > 1) {");
+    writer.indent();
+    writer.println("arg" + listArgNum + "_list = "
+        + formatListPattern(logger, locale, listArg,
+        listAccessor.getElement("--i"), "arg" + listArgNum + "_list", elemType,
+        isSafeHtml, listPatternParts.get("middle"), false, params) + ";");
+    writer.outdent();
+    writer.println("}");
+    writer.println("if (i > 0) {");
+    writer.indent();
+    writer.println("arg" + listArgNum + "_list = "
+        + formatListPattern(logger, locale, listArg,
+        listAccessor.getElement("--i"), "arg" + listArgNum + "_list", elemType,
+        isSafeHtml, listPatternParts.get("start"), false, params) + ";");
+    writer.outdent();
+    writer.println("}");
+    writer.println("break;");
+    writer.outdent();
+    writer.outdent();
+    writer.println("}");
+  }
+
+  /**
+   * @param logger
+   * @param m
+   * @param locale
+   * @param resourceEntry
+   * @param selectors
+   * @param paramsAccessor
+   * @param isSafeHtml
+   * @param forms
+   * @throws UnableToCompleteException
+   */
+  private void generateMessageSelectors(TreeLogger logger, JMethod m,
+      GwtLocale locale, ResourceEntry resourceEntry,
+      List<AlternateFormSelector> selectors, Parameters paramsAccessor,
+      boolean isSafeHtml, String[] forms)
+      throws UnableToCompleteException {
+    int numSelectors = selectors.size();
+    String[] lastForm = new String[numSelectors];
+    for (String form : forms) {
+      String[] splitForms = form.split("\\|");
+      if (splitForms.length != numSelectors) {
+        throw error(logger, "Incorrect number of selector forms for "
+            + m.getName() + " - '" + form + "'");
+      }
+      boolean seenEquals = false;
+      boolean allOther = true;
+      for (String splitForm : splitForms) {
+        if (splitForm.startsWith("=")) {
+          seenEquals = true;
+          allOther = false;
+        } else if (!"other".equals(splitForm)) {
+          allOther = false;
+        }
+      }
+      if (allOther) {
+        // don't process the all-other case, that is the default return value
+        logger.log(TreeLogger.WARN, "Ignoring supplied alternate form with all"
+            + " 'other' values, @DefaultMessage will be used");
+        continue;
+      }
+      
+      // find where the changes are
+      int firstDifferent = 0;
+      while (firstDifferent < numSelectors
+          && splitForms[firstDifferent].equals(lastForm[firstDifferent])) {
+        firstDifferent++;
+      }
+
+      // close nested selects deeper than where the change was
+      for (int i = numSelectors; i-- > firstDifferent; ) {
+        if (lastForm[i] != null) {
+          selectors.get(i).generateSelectMatchEnd(writer, lastForm[i]);
+          if (i > firstDifferent) {
+            selectors.get(i).generateSelectEnd(writer);
+          }
+        }
+      }
+
+      // open all the nested selects from here
+      for (int i = firstDifferent; i < numSelectors; ++i) {
+        if (i > firstDifferent || lastForm[i] == null) {
+          selectors.get(i).generateSelectStart(writer,
+              splitForms[i].startsWith("="));
+        }
+        selectors.get(i).generateSelectMatchStart(writer, logger,
+            splitForms[i]);
+        lastForm[i] = splitForms[i];
+      }
+      writer.print("returnVal = ");
+      generateString(logger, locale, resourceEntry.getForm(form),
+          paramsAccessor, writer, isSafeHtml);
+      writer.println(";");
+    }
+    for (int i = numSelectors; i-- > 0; ) {
+      if (lastForm[i] != null) {
+        selectors.get(i).generateSelectMatchEnd(writer, lastForm[i]);
+        selectors.get(i).generateSelectEnd(writer);
+      }
+    }
   }
 
   /**
@@ -986,43 +1442,40 @@
    * @param logger
    * @param template
    * @param paramsAccessor
-   * @param outputBuf
+   * @param writer
    * @throws UnableToCompleteException
    */
-  @SuppressWarnings("fallthrough")
-  private void generateString(final TreeLogger logger, final GwtLocale locale,
-      final String template, final Parameters paramsAccessor,
-      StringBuffer outputBuf, final boolean isSafeHtml)
+  private void generateString(final TreeLogger logger,final GwtLocale locale,
+      final String template,final Parameters paramsAccessor,
+      SourceWriter writer, final boolean isSafeHtml)
       throws UnableToCompleteException {
+    StringBuffer outputBuf = new StringBuffer();
     final StringGenerator buf = new StringGenerator(outputBuf, isSafeHtml);
     final int n = paramsAccessor.getCount();
     try {
       for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
         chunk.accept(new DefaultTemplateChunkVisitor() {
           @Override
-          public void visit(ArgumentChunk argChunk)
-              throws UnableToCompleteException {
+          public void visit(ArgumentChunk argChunk) throws UnableToCompleteException {
             int argNumber = argChunk.getArgumentNumber();
             if (argNumber >= n) {
-              throw error(logger,
-                  "Argument " + argNumber + " beyond range of arguments: "
-                      + template);
+              throw error(
+                  logger, "Argument " + argNumber + " beyond range of arguments: " + template);
             }
             JParameter param = paramsAccessor.getParameter(argNumber);
             String arg = "arg" + argNumber;
             if (argChunk.isList()) {
-              buf.appendExpression(arg + "_list", isSafeHtml, false,
-                  false);
+              buf.appendExpression(arg + "_list", isSafeHtml, false, false);
             } else {
               JType paramType = param.getType();
-              formatArg(logger, locale, buf, argChunk, arg, paramType,
+              formatArg(logger, locale, buf, argChunk,
+                  paramsAccessor.getParameterExpression(argNumber), paramType,
                   paramsAccessor);
             }
           }
 
           @Override
-          public void visit(StaticArgChunk staticArgChunk)
-              throws UnableToCompleteException {
+          public void visit(StaticArgChunk staticArgChunk) throws UnableToCompleteException {
             buf.appendStringLiteral(staticArgChunk.getReplacement());
           }
 
@@ -1036,15 +1489,14 @@
       throw error(logger, e);
     }
     buf.completeString();
+    writer.print(outputBuf.toString());
   }
 
-  private Map<String, String> getListPatternParts(
-      TreeLogger logger, GwtLocale locale) {
+  private Map<String, String> getListPatternParts(TreeLogger logger, GwtLocale locale) {
     Map<String, String> map = listPatternCache.get(locale);
     if (map == null) {
       // TODO(jat): get these from ResourceOracle instead
-      String baseName =
-          MessagesMethodCreator.class.getPackage().getName().replace('.', '/')
+      String baseName = MessagesMethodCreator.class.getPackage().getName().replace('.', '/')
           + "/cldr/ListPatterns_";
       ClassLoader cl = MessagesMethodCreator.class.getClassLoader();
       for (GwtLocale search : locale.getCompleteSearchList()) {
@@ -1052,14 +1504,12 @@
         InputStream stream = cl.getResourceAsStream(propFile);
         if (stream != null) {
           try {
-            LocalizedPropertiesLoader loader = new LocalizedPropertiesLoader(
-                stream, "UTF-8");
+            LocalizedPropertiesLoader loader = new LocalizedPropertiesLoader(stream, "UTF-8");
             map = new HashMap<String, String>();
             loader.load(map);
             break;
           } catch (IOException e) {
-            logger.log(
-                TreeLogger.WARN, "Ignoring error reading file " + propFile, e);
+            logger.log(TreeLogger.WARN, "Ignoring error reading file " + propFile, e);
           } finally {
             try {
               stream.close();
diff --git a/user/src/com/google/gwt/i18n/rebind/format/PropertiesFormat.java b/user/src/com/google/gwt/i18n/rebind/format/PropertiesFormat.java
index 043b659..e99b30d 100644
--- a/user/src/com/google/gwt/i18n/rebind/format/PropertiesFormat.java
+++ b/user/src/com/google/gwt/i18n/rebind/format/PropertiesFormat.java
@@ -188,6 +188,15 @@
           buf.append(" (Optional");
           inParen = true;
         }
+        if (argInfo.isSelect) {
+          if (inParen) {
+            buf.append("; ");
+          } else {
+            buf.append(" (");
+            inParen = true;
+          }
+          buf.append("Selector");
+        }
         if (argInfo.isPluralCount) {
           if (inParen) {
             buf.append("; ");
diff --git a/user/src/com/google/gwt/user/rebind/AbstractGeneratorClassCreator.java b/user/src/com/google/gwt/user/rebind/AbstractGeneratorClassCreator.java
index bc3f050..8a2530d 100644
--- a/user/src/com/google/gwt/user/rebind/AbstractGeneratorClassCreator.java
+++ b/user/src/com/google/gwt/user/rebind/AbstractGeneratorClassCreator.java
@@ -116,6 +116,7 @@
     classPrologue();
     emitMethods(logger, targetClass, locale);
     classEpilog();
+    getWriter().outdent();
     getWriter().println("}");
   }
 
@@ -231,8 +232,8 @@
       throws UnableToCompleteException {
     JMethod[] x = getAllInterfaceMethods(cur);
     for (int i = 0; i < x.length; i++) {
-      genMethod(logger, x[i], locale);
       getWriter().println();
+      genMethod(logger, x[i], locale);
     }
   }
 
diff --git a/user/test/com/google/gwt/i18n/client/I18N2Test.java b/user/test/com/google/gwt/i18n/client/I18N2Test.java
index 08a1d57..327835d 100644
--- a/user/test/com/google/gwt/i18n/client/I18N2Test.java
+++ b/user/test/com/google/gwt/i18n/client/I18N2Test.java
@@ -16,6 +16,7 @@
 package com.google.gwt.i18n.client;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.TestAnnotatedMessages.Gender;
 import com.google.gwt.i18n.client.TestAnnotatedMessages.Nested;
 import com.google.gwt.i18n.client.constants.TimeZoneConstants;
 import com.google.gwt.i18n.client.gen.Colors;
@@ -24,8 +25,10 @@
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
+import java.util.List;
 
 /**
  * Test the same things as I18NTest but with a different module which
@@ -214,6 +217,232 @@
   }
 
   /**
+   * Verifies correct output for multiple, nested selectors, using an enum
+   * for gender selection (and SafeHtml output).
+   */
+  public void testMultiSelectEnum() {
+    TestAnnotatedMessages m = GWT.create(TestAnnotatedMessages.class);
+    List<String> names = new ArrayList<String>();
+    
+    // empty list of names
+    assertEquals("Nobody liked his message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.MALE).asString());
+    assertEquals("Nobody liked his 2 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, Gender.MALE).asString());
+    assertEquals("Nobody liked her message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.FEMALE).asString());
+    assertEquals("Nobody liked her 3 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, Gender.FEMALE).asString());
+    assertEquals("Nobody liked their message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, null).asString());
+    assertEquals("Nobody liked their 4 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, Gender.UNKNOWN).asString());
+
+    // one name
+    names.add("John");
+    assertEquals("John liked his message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.MALE).asString());
+    assertEquals("John liked his 2 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, Gender.MALE).asString());
+    assertEquals("John liked her message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.FEMALE).asString());
+    assertEquals("John liked her 3 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, Gender.FEMALE).asString());
+    assertEquals("John liked their message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.UNKNOWN).asString());
+    assertEquals("John liked their 4 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, null).asString());
+
+    // two names
+    names.add("Bob");
+    assertEquals("John and Bob liked his message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.MALE).asString());
+    assertEquals("John and Bob liked his 2 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, Gender.MALE).asString());
+    assertEquals("John and Bob liked her message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.FEMALE).asString());
+    assertEquals("John and Bob liked her 3 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, Gender.FEMALE).asString());
+    assertEquals("John and Bob liked their message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, null).asString());
+    assertEquals("John and Bob liked their 4 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, Gender.UNKNOWN).asString());
+
+    // three names
+    names.add("Alice");
+    assertEquals("John, Bob, and one other liked his message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.MALE).asString());
+    assertEquals("John, Bob, and one other liked his 2 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, Gender.MALE).asString());
+    assertEquals("John, Bob, and one other liked her message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.FEMALE).asString());
+    assertEquals("John, Bob, and one other liked her 3 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, Gender.FEMALE).asString());
+    assertEquals("John, Bob, and one other liked their message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.UNKNOWN).asString());
+    assertEquals("John, Bob, and one other liked their 4 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, null).asString());
+
+    // four names
+    names.add("Carol");
+    assertEquals("John, Bob, and 2 others liked his message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.MALE).asString());
+    assertEquals("John, Bob, and 2 others liked his 2 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, Gender.MALE).asString());
+    assertEquals("John, Bob, and 2 others liked her message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.FEMALE).asString());
+    assertEquals("John, Bob, and 2 others liked her 3 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, Gender.FEMALE).asString());
+    assertEquals("John, Bob, and 2 others liked their message",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, Gender.UNKNOWN).asString());
+    assertEquals("John, Bob, and 2 others liked their 4 messages",
+        m.multiSelectEnum(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, null).asString());
+  }
+
+  /**
+   * Verifies correct output for multiple, nested selectors, using a string
+   * for gender selection.
+   */
+  public void testMultiSelectString() {
+    TestAnnotatedMessages m = GWT.create(TestAnnotatedMessages.class);
+    List<String> names = new ArrayList<String>();
+    
+    // empty list of names
+    assertEquals("Nobody liked his message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "MALE"));
+    assertEquals("Nobody liked his 2 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, "MALE"));
+    assertEquals("Nobody liked her message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "FEMALE"));
+    assertEquals("Nobody liked her 3 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, "FEMALE"));
+    assertEquals("Nobody liked their message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "unknown"));
+    assertEquals("Nobody liked their 4 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, "unknown"));
+
+    // one name
+    names.add("John");
+    assertEquals("John liked his message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "MALE"));
+    assertEquals("John liked his 2 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, "MALE"));
+    assertEquals("John liked her message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "FEMALE"));
+    assertEquals("John liked her 3 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, "FEMALE"));
+    assertEquals("John liked their message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "unknown"));
+    assertEquals("John liked their 4 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, "unknown"));
+
+    // two names
+    names.add("Bob");
+    assertEquals("John and Bob liked his message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "MALE"));
+    assertEquals("John and Bob liked his 2 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, "MALE"));
+    assertEquals("John and Bob liked her message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "FEMALE"));
+    assertEquals("John and Bob liked her 3 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, "FEMALE"));
+    assertEquals("John and Bob liked their message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "unknown"));
+    assertEquals("John and Bob liked their 4 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, "unknown"));
+
+    // three names
+    names.add("Alice");
+    assertEquals("John, Bob, and one other liked his message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "MALE"));
+    assertEquals("John, Bob, and one other liked his 2 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, "MALE"));
+    assertEquals("John, Bob, and one other liked her message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "FEMALE"));
+    assertEquals("John, Bob, and one other liked her 3 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, "FEMALE"));
+    assertEquals("John, Bob, and one other liked their message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "unknown"));
+    assertEquals("John, Bob, and one other liked their 4 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, "unknown"));
+
+    // four names
+    names.add("Carol");
+    assertEquals("John, Bob, and 2 others liked his message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "MALE"));
+    assertEquals("John, Bob, and 2 others liked his 2 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 2, "MALE"));
+    assertEquals("John, Bob, and 2 others liked her message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "FEMALE"));
+    assertEquals("John, Bob, and 2 others liked her 3 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 3, "FEMALE"));
+    assertEquals("John, Bob, and 2 others liked their message",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 1, "unknown"));
+    assertEquals("John, Bob, and 2 others liked their 4 messages",
+        m.multiSelectString(names, names.size() > 0 ? names.get(0) : null,
+        names.size() > 1 ? names.get(1) : null, 4, "unknown"));
+  }
+
+  /**
    * Verify that nested annotations are looked up with both A$B names
    * and A_B names.  Note that $ takes precedence and only one file for a
    * given level in the inheritance tree will be used, so A$B_locale will
@@ -238,6 +467,9 @@
     assertEquals(
         "John Doe, Betty Smith, and one other have reviewed this movie",
         m.reviewers(3, "John Doe", "Betty Smith"));
+    assertEquals(
+        "John Doe, Betty Smith, and 3 others have reviewed this movie",
+        m.reviewers(5, "John Doe", "Betty Smith"));
 
     assertEquals("No widgets", m.specialPluralsAsSafeHtml(0).asString());
     assertEquals("A widget", m.specialPluralsAsSafeHtml(1).asString());
@@ -251,6 +483,9 @@
     assertEquals(
         "John Doe, Betty Smith, and one other have reviewed this movie",
         m.reviewersAsSafeHtml(3, "John Doe", sh("Betty Smith")).asString());
+    assertEquals(
+        "John Doe, Betty Smith, and 3 others have reviewed this movie",
+        m.reviewersAsSafeHtml(5, "John Doe", sh("Betty Smith")).asString());
 }
 
   public void testStaticArg() {
diff --git a/user/test/com/google/gwt/i18n/client/TestAnnotatedMessages.java b/user/test/com/google/gwt/i18n/client/TestAnnotatedMessages.java
index c5f7159..ddc2dd6 100644
--- a/user/test/com/google/gwt/i18n/client/TestAnnotatedMessages.java
+++ b/user/test/com/google/gwt/i18n/client/TestAnnotatedMessages.java
@@ -31,9 +31,19 @@
 @GenerateKeys("com.google.gwt.i18n.rebind.keygen.MethodNameKeyGenerator")
 // default
 @Generate(format = "com.google.gwt.i18n.rebind.format.PropertiesFormat")
+@SuppressWarnings("deprecation")
 public interface TestAnnotatedMessages extends Messages {
 
   /**
+   * Represents the gender of a person in a message.
+   */
+  public enum Gender {
+    MALE,
+    FEMALE,
+    UNKNOWN
+  }
+
+  /**
    * Test of property file lookup on nested classes.
    *
    * nestedDollar() is redefined in a property file with a $ in it.
@@ -155,6 +165,13 @@
   @Key("defaultNumberFormat")
   SafeHtml defaultNumberFormatAsSafeHtml(double value);
 
+  @DefaultMessage("{1} wants to sell their car")
+  @AlternateMessage({
+    "FEMALE", "{1} wants to sell her car",
+    "MALE", "{1} wants to sell his car"
+  })
+  String gender(@Select Gender gender, String name);
+
   @DefaultMessage("It is {0,time,short} on {0,date,full}")
   String getTimeDate(Date value);
 
@@ -162,6 +179,78 @@
   @Key("getTimeDate")
   SafeHtml getTimeDateAsSafeHtml(Date value);
 
+  @DefaultMessage("{1}, {2}, and {0} others liked their {3} messages")
+  @AlternateMessage({
+    "=0|other|other", "Nobody liked their {3} messages",
+    "=0|other|FEMALE", "Nobody liked her {3} messages",
+    "=0|other|MALE", "Nobody liked his {3} messages",
+    "=0|one|other", "Nobody liked their message",
+    "=0|one|FEMALE", "Nobody liked her message",
+    "=0|one|MALE", "Nobody liked his message",
+    "=1|other|other", "{1} liked their {3} messages",
+    "=1|other|FEMALE", "{1} liked her {3} messages",
+    "=1|other|MALE", "{1} liked his {3} messages",
+    "=1|one|other", "{1} liked their message",
+    "=1|one|FEMALE", "{1} liked her message",
+    "=1|one|MALE", "{1} liked his message",
+    "=2|other|other", "{1} and {2} liked their {3} messages",
+    "=2|other|FEMALE", "{1} and {2} liked her {3} messages",
+    "=2|other|MALE", "{1} and {2} liked his {3} messages",
+    "=2|one|other", "{1} and {2} liked their message",
+    "=2|one|FEMALE", "{1} and {2} liked her message",
+    "=2|one|MALE", "{1} and {2} liked his message",
+    "one|other|other", "{1}, {2}, and one other liked their {3} messages",
+    "one|other|FEMALE", "{1}, {2}, and one other liked her {3} messages",
+    "one|other|MALE", "{1}, {2}, and one other liked his {3} messages",
+    "one|one|other", "{1}, {2}, and one other liked their message",
+    "one|one|FEMALE", "{1}, {2}, and one other liked her message",
+    "one|one|MALE", "{1}, {2}, and one other liked his message",
+    "other|one|other", "{1}, {2}, and {0} others liked their message",
+    "other|one|MALE", "{1}, {2}, and {0} others liked his message",
+    "other|one|FEMALE", "{1}, {2}, and {0} others liked her message",
+    "other|other|MALE", "{1}, {2}, and {0} others liked his {3} messages",
+    "other|other|FEMALE", "{1}, {2}, and {0} others liked her {3} messages"
+  })
+  String multiSelectString(@PluralCount @Offset(2) List<String> names,
+      String name1, String name2, @PluralCount int msgCount,
+      @Select String gender);
+
+  @DefaultMessage("{1}, {2}, and {0} others liked their {3} messages")
+  @AlternateMessage({
+    "=0|other|other", "Nobody liked their {3} messages",
+    "=0|other|FEMALE", "Nobody liked her {3} messages",
+    "=0|other|MALE", "Nobody liked his {3} messages",
+    "=0|one|other", "Nobody liked their message",
+    "=0|one|FEMALE", "Nobody liked her message",
+    "=0|one|MALE", "Nobody liked his message",
+    "=1|other|other", "{1} liked their {3} messages",
+    "=1|other|FEMALE", "{1} liked her {3} messages",
+    "=1|other|MALE", "{1} liked his {3} messages",
+    "=1|one|other", "{1} liked their message",
+    "=1|one|FEMALE", "{1} liked her message",
+    "=1|one|MALE", "{1} liked his message",
+    "=2|other|other", "{1} and {2} liked their {3} messages",
+    "=2|other|FEMALE", "{1} and {2} liked her {3} messages",
+    "=2|other|MALE", "{1} and {2} liked his {3} messages",
+    "=2|one|other", "{1} and {2} liked their message",
+    "=2|one|FEMALE", "{1} and {2} liked her message",
+    "=2|one|MALE", "{1} and {2} liked his message",
+    "one|other|other", "{1}, {2}, and one other liked their {3} messages",
+    "one|other|FEMALE", "{1}, {2}, and one other liked her {3} messages",
+    "one|other|MALE", "{1}, {2}, and one other liked his {3} messages",
+    "one|one|other", "{1}, {2}, and one other liked their message",
+    "one|one|FEMALE", "{1}, {2}, and one other liked her message",
+    "one|one|MALE", "{1}, {2}, and one other liked his message",
+    "other|one|other", "{1}, {2}, and {0} others liked their message",
+    "other|one|MALE", "{1}, {2}, and {0} others liked his message",
+    "other|one|FEMALE", "{1}, {2}, and {0} others liked her message",
+    "other|other|MALE", "{1}, {2}, and {0} others liked his {3} messages",
+    "other|other|FEMALE", "{1}, {2}, and {0} others liked her {3} messages"
+  })
+  SafeHtml multiSelectEnum(@PluralCount @Offset(2) List<String> names,
+      String name1, String name2, @PluralCount int msgCount,
+      @Select Gender gender);
+
   @DefaultMessage("{0} widgets")
   @PluralText({"one", "A widget"})
   String pluralWidgetsOther(@PluralCount int count);
@@ -213,7 +302,7 @@
   @DefaultMessage("Distance is {0,number,##0.0##E0}")
   String withNumberExponent(Number value);
 
-  @DefaultMessage("{1}, {2} and {0,number} others have reviewed this movie")
+  @DefaultMessage("{1}, {2}, and {0,number} others have reviewed this movie")
   @PluralText({
     "=0", "No one has reviewed this movie",
     "=1", "{1} has reviewed this movie",
@@ -222,7 +311,7 @@
   String reviewers(@PluralCount @Offset(2) int size,
        String name1, String name2);
 
-  @DefaultMessage("{1}, {2} and {0,number} others have reviewed this movie")
+  @DefaultMessage("{1}, {2}, and {0,number} others have reviewed this movie")
   @PluralText({
     "=0", "No one has reviewed this movie",
     "=1", "{1} has reviewed this movie",