Revert "Remove message extraction and generation code from Intl (now in intl_translation)"

This reverts commit 99d02c8eab1a77a3c66c092d3378bb6c6815126f.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 799dbad..321d5f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,3 @@
-## 0.14.0
- * MAJOR BREAKING CHANGE! Remove message extraction and code generation into a separate
-   intl_translation package. This means packages with a runtime dependency on
-   intl don't also depend on analyzer, barback, and so forth.
-
 ## 0.13.1
  * Update CLDR data to version 29.
  * Add a toBeginningOfSentenceCase() method which converts the first character
diff --git a/README.md b/README.md
index 0b38b17..1a0f074 100644
--- a/README.md
+++ b/README.md
@@ -186,12 +186,11 @@
 
 When your program contains messages that need translation, these must
 be extracted from the program source, sent to human translators, and the
-results need to be incorporated. The code for this is in the
-[Intl_translation][Intl_translation] package.
+results need to be incorporated.
 
 To extract messages, run the `extract_to_arb.dart` program.
 
-      pub run intl_translation:extract_to_arb --output-dir=target/directory
+      pub run intl:extract_to_arb --output-dir=target/directory
           my_program.dart more_of_my_program.dart
 
 This will produce a file `intl_messages.arb` with the messages from
@@ -205,10 +204,8 @@
 This expects to receive a series of files, one per
 locale.
 
-```
-pub run intl_translation:generate_from_arb --generated_file_prefix=<prefix>
-    <my_dart_files> <translated_ARB_files>
-```
+      pub run intl:generate_from_arb --generated_file_prefix=<prefix>
+          <my_dart_files> <translated_ARB_files>
 
 This will generate Dart libraries, one per locale, which contain the
 translated versions. Your Dart libraries can import the primary file,
diff --git a/bin/extract_to_arb.dart b/bin/extract_to_arb.dart
new file mode 100644
index 0000000..7c643f9
--- /dev/null
+++ b/bin/extract_to_arb.dart
@@ -0,0 +1,138 @@
+#!/usr/bin/env dart
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This script uses the extract_messages.dart library to find the Intl.message
+/// calls in the target dart files and produces ARB format output. See
+/// https://code.google.com/p/arb/wiki/ApplicationResourceBundleSpecification
+library extract_to_arb;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+
+import 'package:intl/extract_messages.dart';
+import 'package:intl/src/intl_message.dart';
+
+var outputFilename = 'intl_messages.arb';
+
+main(List<String> args) {
+  var targetDir;
+  bool transformer;
+  var parser = new ArgParser();
+  var extraction = new MessageExtraction();
+  parser.addFlag("suppress-warnings",
+      defaultsTo: false,
+      callback: (x) => extraction.suppressWarnings = x,
+      help: 'Suppress printing of warnings.');
+  parser.addFlag("warnings-are-errors",
+      defaultsTo: false,
+      callback: (x) => extraction.warningsAreErrors = x,
+      help: 'Treat all warnings as errors, stop processing ');
+  parser.addFlag("embedded-plurals",
+      defaultsTo: true,
+      callback: (x) => extraction.allowEmbeddedPluralsAndGenders = x,
+      help: 'Allow plurals and genders to be embedded as part of a larger '
+          'string, otherwise they must be at the top level.');
+  parser.addFlag("transformer",
+      defaultsTo: false,
+      callback: (x) => transformer = x,
+      help: "Assume that the transformer is in use, so name and args "
+          "don't need to be specified for messages.");
+
+  parser.addOption("output-dir",
+      defaultsTo: '.',
+      callback: (value) => targetDir = value,
+      help: 'Specify the output directory.');
+  parser.parse(args);
+  if (args.length == 0) {
+    print('Accepts Dart files and produces $outputFilename');
+    print('Usage: extract_to_arb [options] [files.dart]');
+    print(parser.usage);
+    exit(0);
+  }
+  var allMessages = {};
+  for (var arg in args.where((x) => x.contains(".dart"))) {
+    var messages = extraction.parseFile(new File(arg), transformer);
+    messages.forEach((k, v) => allMessages.addAll(toARB(v)));
+  }
+  var file = new File(path.join(targetDir, outputFilename));
+  file.writeAsStringSync(JSON.encode(allMessages));
+  if (extraction.hasWarnings && extraction.warningsAreErrors) {
+    exit(1);
+  }
+}
+
+/// This is a placeholder for transforming a parameter substitution from
+/// the translation file format into a Dart interpolation. In our case we
+/// store it to the file in Dart interpolation syntax, so the transformation
+/// is trivial.
+String leaveTheInterpolationsInDartForm(MainMessage msg, chunk) {
+  if (chunk is String) return chunk;
+  if (chunk is int) return "\$${msg.arguments[chunk]}";
+  return chunk.toCode();
+}
+
+/// Convert the [MainMessage] to a trivial JSON format.
+Map toARB(MainMessage message) {
+  if (message.messagePieces.isEmpty) return null;
+  var out = {};
+  out[message.name] = icuForm(message);
+  out["@${message.name}"] = arbMetadata(message);
+  return out;
+}
+
+Map arbMetadata(MainMessage message) {
+  var out = {};
+  var desc = message.description;
+  if (desc != null) {
+    out["description"] = desc;
+  }
+  out["type"] = "text";
+  var placeholders = {};
+  for (var arg in message.arguments) {
+    addArgumentFor(message, arg, placeholders);
+  }
+  out["placeholders"] = placeholders;
+  return out;
+}
+
+void addArgumentFor(MainMessage message, String arg, Map result) {
+  var extraInfo = {};
+  if (message.examples != null && message.examples[arg] != null) {
+    extraInfo["example"] = message.examples[arg];
+  }
+  result[arg] = extraInfo;
+}
+
+/// Return a version of the message string with with ICU parameters "{variable}"
+/// rather than Dart interpolations "$variable".
+String icuForm(MainMessage message) =>
+    message.expanded(turnInterpolationIntoICUForm);
+
+String turnInterpolationIntoICUForm(Message message, chunk,
+    {bool shouldEscapeICU: false}) {
+  if (chunk is String) {
+    return shouldEscapeICU ? escape(chunk) : chunk;
+  }
+  if (chunk is int && chunk >= 0 && chunk < message.arguments.length) {
+    return "{${message.arguments[chunk]}}";
+  }
+  if (chunk is SubMessage) {
+    return chunk.expanded((message, chunk) =>
+        turnInterpolationIntoICUForm(message, chunk, shouldEscapeICU: true));
+  }
+  if (chunk is Message) {
+    return chunk.expanded((message, chunk) => turnInterpolationIntoICUForm(
+        message, chunk,
+        shouldEscapeICU: shouldEscapeICU));
+  }
+  throw new FormatException("Illegal interpolation: $chunk");
+}
+
+String escape(String s) {
+  return s.replaceAll("'", "''").replaceAll("{", "'{'").replaceAll("}", "'}'");
+}
diff --git a/bin/generate_from_arb.dart b/bin/generate_from_arb.dart
new file mode 100644
index 0000000..13d3680
--- /dev/null
+++ b/bin/generate_from_arb.dart
@@ -0,0 +1,161 @@
+#!/usr/bin/env dart
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A main program that takes as input a source Dart file and a number
+/// of ARB files representing translations of messages from the corresponding
+/// Dart file. See extract_to_arb.dart and make_hardcoded_translation.dart.
+///
+/// If the ARB file has an @@locale or _locale value, that will be used as
+/// the locale. If not, we will try to figure out the locale from the end of
+/// the file name, e.g. foo_en_GB.arb will be assumed to be in en_GB locale.
+///
+/// This produces a series of files named
+/// "messages_<locale>.dart" containing messages for a particular locale
+/// and a main import file named "messages_all.dart" which has imports all of
+/// them and provides an initializeMessages function.
+
+library generate_from_arb;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+
+import 'package:intl/extract_messages.dart';
+import 'package:intl/generate_localized.dart';
+import 'package:intl/src/intl_message.dart';
+import 'package:intl/src/icu_parser.dart';
+
+/// Keeps track of all the messages we have processed so far, keyed by message
+/// name.
+Map<String, List<MainMessage>> messages;
+
+main(List<String> args) {
+  var targetDir;
+  var parser = new ArgParser();
+  var extraction = new MessageExtraction();
+  var generation = new MessageGeneration();
+  parser.addFlag("suppress-warnings",
+      defaultsTo: false,
+      callback: (x) => extraction.suppressWarnings = x,
+      help: 'Suppress printing of warnings.');
+  parser.addOption("output-dir",
+      defaultsTo: '.',
+      callback: (x) => targetDir = x,
+      help: 'Specify the output directory.');
+  parser.addOption("generated-file-prefix",
+      defaultsTo: '',
+      callback: (x) => generation.generatedFilePrefix = x,
+      help: 'Specify a prefix to be used for the generated file names.');
+  parser.addFlag("use-deferred-loading",
+      defaultsTo: true,
+      callback: (x) => generation.useDeferredLoading = x,
+      help: 'Generate message code that must be loaded with deferred loading. '
+          'Otherwise, all messages are eagerly loaded.');
+  parser.parse(args);
+  var dartFiles = args.where((x) => x.endsWith("dart")).toList();
+  var jsonFiles = args.where((x) => x.endsWith(".arb")).toList();
+  if (dartFiles.length == 0 || jsonFiles.length == 0) {
+    print('Usage: generate_from_arb [options]'
+        ' file1.dart file2.dart ...'
+        ' translation1_<languageTag>.arb translation2.arb ...');
+    print(parser.usage);
+    exit(0);
+  }
+
+  // TODO(alanknight): There is a possible regression here. If a project is
+  // using the transformer and expecting it to provide names for messages with
+  // parameters, we may report those names as missing. We now have two distinct
+  // mechanisms for providing names: the transformer and just using the message
+  // text if there are no parameters. Previously this was always acting as if
+  // the transformer was in use, but that breaks the case of using the message
+  // text. The intent is to deprecate the transformer, but if this is an issue
+  // for real projects we could provide a command-line flag to indicate which
+  // sort of automated name we're using.
+  extraction.suppressWarnings = true;
+  var allMessages =
+      dartFiles.map((each) => extraction.parseFile(new File(each), false));
+
+  messages = new Map();
+  for (var eachMap in allMessages) {
+    eachMap.forEach(
+        (key, value) => messages.putIfAbsent(key, () => []).add(value));
+  }
+  for (var arg in jsonFiles) {
+    var file = new File(arg);
+    generateLocaleFile(file, targetDir, generation);
+  }
+
+  var mainImportFile = new File(path.join(
+      targetDir, '${generation.generatedFilePrefix}messages_all.dart'));
+  mainImportFile.writeAsStringSync(generation.generateMainImportFile());
+}
+
+/// Create the file of generated code for a particular locale. We read the ARB
+/// data and create [BasicTranslatedMessage] instances from everything,
+/// excluding only the special _locale attribute that we use to indicate the
+/// locale. If that attribute is missing, we try to get the locale from the last
+/// section of the file name.
+void generateLocaleFile(
+    File file, String targetDir, MessageGeneration generation) {
+  var src = file.readAsStringSync();
+  var data = JSON.decode(src);
+  data.forEach((k, v) => data[k] = recreateIntlObjects(k, v));
+  var locale = data["@@locale"] ?? data["_locale"];
+  if (locale != null) {
+    locale = locale.translated.string;
+  } else {
+    // Get the locale from the end of the file name. This assumes that the file
+    // name doesn't contain any underscores except to begin the language tag
+    // and to separate language from country. Otherwise we can't tell if
+    // my_file_fr.arb is locale "fr" or "file_fr".
+    var name = path.basenameWithoutExtension(file.path);
+    locale = name.split("_").skip(1).join("_");
+    print("No @@locale or _locale field found in $name, "
+        "assuming '$locale' based on the file name.");
+  }
+  generation.allLocales.add(locale);
+
+  List<TranslatedMessage> translations = [];
+  data.forEach((key, value) {
+    if (value != null) {
+      translations.add(value);
+    }
+  });
+  generation.generateIndividualMessageFile(locale, translations, targetDir);
+}
+
+/// Regenerate the original IntlMessage objects from the given [data]. For
+/// things that are messages, we expect [id] not to start with "@" and
+/// [data] to be a String. For metadata we expect [id] to start with "@"
+/// and [data] to be a Map or null. For metadata we return null.
+BasicTranslatedMessage recreateIntlObjects(String id, data) {
+  if (id.startsWith("@")) return null;
+  if (data == null) return null;
+  var parsed = pluralAndGenderParser.parse(data).value;
+  if (parsed is LiteralString && parsed.string.isEmpty) {
+    parsed = plainParser.parse(data).value;
+    ;
+  }
+  return new BasicTranslatedMessage(id, parsed);
+}
+
+/// A TranslatedMessage that just uses the name as the id and knows how to look
+/// up its original messages in our [messages].
+class BasicTranslatedMessage extends TranslatedMessage {
+  BasicTranslatedMessage(String name, translated) : super(name, translated);
+
+  List<MainMessage> get originalMessages => (super.originalMessages == null)
+      ? _findOriginals()
+      : super.originalMessages;
+
+  // We know that our [id] is the name of the message, which is used as the
+  //key in [messages].
+  List<MainMessage> _findOriginals() => originalMessages = messages[id];
+}
+
+final pluralAndGenderParser = new IcuParser().message;
+final plainParser = new IcuParser().nonIcuMessage;
diff --git a/bin/rewrite_intl_messages.dart b/bin/rewrite_intl_messages.dart
new file mode 100644
index 0000000..8fc0dd6
--- /dev/null
+++ b/bin/rewrite_intl_messages.dart
@@ -0,0 +1,45 @@
+#!/usr/bin/env dart
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A main program that imitates the action of the transformer, adding
+/// name and args parameters to Intl.message calls automatically.
+///
+/// This is mainly intended to test the transformer logic outside of barback.
+/// It takes as input a single source Dart file and rewrites any
+/// Intl.message or related calls to automatically include the name and args
+/// parameters and writes the result to stdout.
+///
+import 'dart:io';
+
+import 'package:args/args.dart';
+
+import 'package:intl/src/message_rewriter.dart';
+
+String outputFile = 'transformed_output.dart';
+
+main(List<String> args) {
+  var parser = new ArgParser();
+  parser.addOption('output',
+      defaultsTo: 'transformed_output.dart',
+      callback: (x) => outputFile = x,
+      help: 'Specify the output file.');
+  print(args);
+  parser.parse(args);
+  if (args.length == 0) {
+    print('Accepts a single Dart file and adds "name" and "args" parameters '
+        ' to Intl.message calls.');
+    print('Primarily useful for exercising the transformer logic.');
+    print('Usage: rewrite_intl_messages [options] [file.dart]');
+    print(parser.usage);
+    exit(0);
+  }
+  var dartFile = args.where((x) => x.endsWith(".dart")).last;
+  var file = new File(dartFile);
+  var content = file.readAsStringSync();
+  var newSource = rewriteMessages(content, '$file');
+  print('Writing new source to $outputFile');
+  var out = new File(outputFile);
+  out.writeAsStringSync(newSource);
+}
diff --git a/lib/extract_messages.dart b/lib/extract_messages.dart
new file mode 100644
index 0000000..b9c1222
--- /dev/null
+++ b/lib/extract_messages.dart
@@ -0,0 +1,550 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This is for use in extracting messages from a Dart program
+/// using the Intl.message() mechanism and writing them to a file for
+/// translation. This provides only the stub of a mechanism, because it
+/// doesn't define how the file should be written. It provides an
+/// [IntlMessage] class that holds the extracted data and [parseString]
+/// and [parseFile] methods which
+/// can extract messages that conform to the expected pattern:
+///       (parameters) => Intl.message("Message $parameters", desc: ...);
+/// It uses the analyzer package to do the parsing, so may
+/// break if there are changes to the API that it provides.
+/// An example can be found in test/message_extraction/extract_to_json.dart
+///
+/// Note that this does not understand how to follow part directives, so it
+/// has to explicitly be given all the files that it needs. A typical use case
+/// is to run it on all .dart files in a directory.
+library extract_messages;
+
+import 'dart:io';
+
+import 'package:analyzer/analyzer.dart';
+import 'package:intl/src/intl_message.dart';
+import 'package:intl/src/intl_helpers.dart';
+
+/// A function that takes a message and does something useful with it.
+typedef void OnMessage(String message);
+
+/// A particular message extraction run.
+///
+///  This encapsulates all the state required for message extraction so that
+///  it can be run inside a persistent process.
+class MessageExtraction {
+  /// What to do when a message is encountered, defaults to [print].
+  OnMessage onMessage = print;
+
+  /// If this is true, print warnings for skipped messages. Otherwise, warnings
+  /// are suppressed.
+  bool suppressWarnings = false;
+
+  /// If this is true, then treat all warnings as errors.
+  bool warningsAreErrors = false;
+
+  /// This accumulates a list of all warnings/errors we have found. These are
+  /// saved as strings right now, so all that can really be done is print and
+  /// count them.
+  List<String> warnings = [];
+
+  /// Were there any warnings or errors in extracting messages.
+  bool get hasWarnings => warnings.isNotEmpty;
+
+  /// Are plural and gender expressions required to be at the top level
+  /// of an expression, or are they allowed to be embedded in string literals.
+  ///
+  /// For example, the following expression
+  ///     'There are ${Intl.plural(...)} items'.
+  /// is legal if [allowEmbeddedPluralsAndGenders] is true, but illegal
+  /// if [allowEmbeddedPluralsAndGenders] is false.
+  bool allowEmbeddedPluralsAndGenders = true;
+
+  /// Are examples required on all messages.
+  bool examplesRequired = false;
+
+  /// Parse the source of the Dart program file [file] and return a Map from
+  /// message names to [IntlMessage] instances.
+  ///
+  /// If [transformer] is true, assume the transformer will supply any "name"
+  /// and "args" parameters required in Intl.message calls.
+  Map<String, MainMessage> parseFile(File file, [transformer = false]) {
+    // Optimization to avoid parsing files we're sure don't contain any messages.
+    String contents = file.readAsStringSync();
+    origin = file.path;
+    if (contents.contains("Intl.")) {
+      root = _parseCompilationUnit(contents, origin);
+    } else {
+      return {};
+    }
+    var visitor = new MessageFindingVisitor(this);
+    visitor.generateNameAndArgs = transformer;
+    root.accept(visitor);
+    return visitor.messages;
+  }
+
+  CompilationUnit _parseCompilationUnit(String contents, String origin) {
+    var parsed;
+    try {
+      parsed = parseCompilationUnit(contents);
+    } on AnalyzerErrorGroup catch (e) {
+      print("Error in parsing $origin, no messages extracted.");
+      print("  $e");
+    }
+    return parsed;
+  }
+
+  /// The root of the compilation unit, and the first node we visit. We hold
+  /// on to this for error reporting, as it can give us line numbers of other
+  /// nodes.
+  CompilationUnit root;
+
+  /// An arbitrary string describing where the source code came from. Most
+  /// obviously, this could be a file path. We use this when reporting
+  /// invalid messages.
+  String origin;
+
+  String _reportErrorLocation(AstNode node) {
+    var result = new StringBuffer();
+    if (origin != null) result.write("    from $origin");
+    var info = root.lineInfo;
+    if (info != null) {
+      var line = info.getLocation(node.offset);
+      result
+          .write("    line: ${line.lineNumber}, column: ${line.columnNumber}");
+    }
+    return result.toString();
+  }
+}
+
+/// This visits the program source nodes looking for Intl.message uses
+/// that conform to its pattern and then creating the corresponding
+/// IntlMessage objects. We have to find both the enclosing function, and
+/// the Intl.message invocation.
+class MessageFindingVisitor extends GeneralizingAstVisitor {
+  MessageFindingVisitor(this.extraction);
+
+  /// The message extraction in which we are running.
+  final MessageExtraction extraction;
+
+  /// Accumulates the messages we have found, keyed by name.
+  final Map<String, MainMessage> messages = new Map<String, MainMessage>();
+
+  /// Should we generate the name and arguments from the function definition,
+  /// meaning we're running in the transformer.
+  bool generateNameAndArgs = false;
+
+  /// We keep track of the data from the last MethodDeclaration,
+  /// FunctionDeclaration or FunctionExpression that we saw on the way down,
+  /// as that will be the nearest parent of the Intl.message invocation.
+  FormalParameterList parameters;
+  String name;
+
+  /// Return true if [node] matches the pattern we expect for Intl.message()
+  bool looksLikeIntlMessage(MethodInvocation node) {
+    const validNames = const ["message", "plural", "gender", "select"];
+    if (!validNames.contains(node.methodName.name)) return false;
+    if (!(node.target is SimpleIdentifier)) return false;
+    SimpleIdentifier target = node.target;
+    return target.token.toString() == "Intl";
+  }
+
+  Message _expectedInstance(String type) {
+    switch (type) {
+      case 'message':
+        return new MainMessage();
+      case 'plural':
+        return new Plural();
+      case 'gender':
+        return new Gender();
+      case 'select':
+        return new Select();
+      default:
+        return null;
+    }
+  }
+
+  /// Returns a String describing why the node is invalid, or null if no
+  /// reason is found, so it's presumed valid.
+  String checkValidity(MethodInvocation node) {
+    // The containing function cannot have named parameters.
+    if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) {
+      return "Named parameters on message functions are not supported.";
+    }
+    var arguments = node.argumentList.arguments;
+    var instance = _expectedInstance(node.methodName.name);
+    return instance.checkValidity(node, arguments, name, parameters,
+        nameAndArgsGenerated: generateNameAndArgs,
+        examplesRequired: extraction.examplesRequired);
+  }
+
+  /// Record the parameters of the function or method declaration we last
+  /// encountered before seeing the Intl.message call.
+  void visitMethodDeclaration(MethodDeclaration node) {
+    parameters = node.parameters;
+    if (parameters == null) {
+      parameters = new FormalParameterList(null, [], null, null, null);
+    }
+    name = node.name.name;
+    super.visitMethodDeclaration(node);
+  }
+
+  /// Record the parameters of the function or method declaration we last
+  /// encountered before seeing the Intl.message call.
+  void visitFunctionDeclaration(FunctionDeclaration node) {
+    parameters = node.functionExpression.parameters;
+    if (parameters == null) {
+      parameters = new FormalParameterList(null, [], null, null, null);
+    }
+    name = node.name.name;
+    super.visitFunctionDeclaration(node);
+  }
+
+  /// Examine method invocations to see if they look like calls to Intl.message.
+  /// If we've found one, stop recursing. This is important because we can have
+  /// Intl.message(...Intl.plural...) and we don't want to treat the inner
+  /// plural as if it was an outermost message.
+  void visitMethodInvocation(MethodInvocation node) {
+    if (!addIntlMessage(node)) {
+      super.visitMethodInvocation(node);
+    }
+  }
+
+  /// Check that the node looks like an Intl.message invocation, and create
+  /// the [IntlMessage] object from it and store it in [messages]. Return true
+  /// if we successfully extracted a message and should stop looking. Return
+  /// false if we didn't, so should continue recursing.
+  bool addIntlMessage(MethodInvocation node) {
+    if (!looksLikeIntlMessage(node)) return false;
+    var reason = checkValidity(node);
+    if (reason != null) {
+      if (!extraction.suppressWarnings) {
+        var err = new StringBuffer()
+          ..write("Skipping invalid Intl.message invocation\n    <$node>\n")
+          ..writeAll(
+              ["    reason: $reason\n", extraction._reportErrorLocation(node)]);
+        var errString = err.toString();
+        extraction.warnings.add(errString);
+        extraction.onMessage(errString);
+      }
+      // We found one, but it's not valid. Stop recursing.
+      return true;
+    }
+    var message;
+    if (node.methodName.name == "message") {
+      message = messageFromIntlMessageCall(node);
+    } else {
+      message = messageFromDirectPluralOrGenderCall(node);
+    }
+    if (message != null) messages[message.name] = message;
+    return true;
+  }
+
+  /// Create a MainMessage from [node] using the name and
+  /// parameters of the last function/method declaration we encountered,
+  /// and the values we get by calling [extract]. We set those values
+  /// by calling [setAttribute]. This is the common parts between
+  /// [messageFromIntlMessageCall] and [messageFromDirectPluralOrGenderCall].
+  MainMessage _messageFromNode(
+      MethodInvocation node,
+      MainMessage extract(MainMessage message, List<AstNode> arguments),
+      void setAttribute(
+          MainMessage message, String fieldName, Object fieldValue)) {
+    var message = new MainMessage();
+    message.sourcePosition = node.offset;
+    message.endPosition = node.end;
+    message.arguments =
+        parameters.parameters.map((x) => x.identifier.name).toList();
+    var arguments = node.argumentList.arguments;
+    var extractionResult = extract(message, arguments);
+    if (extractionResult == null) return null;
+
+    for (NamedExpression namedArgument
+        in arguments.where((x) => x is NamedExpression)) {
+      var name = namedArgument.name.label.name;
+      var exp = namedArgument.expression;
+      var evaluator = new ConstantEvaluator();
+      var basicValue = exp.accept(evaluator);
+      var value = basicValue == ConstantEvaluator.NOT_A_CONSTANT
+          ? exp.toString()
+          : basicValue;
+      setAttribute(message, name, value);
+    }
+    // We only rewrite messages with parameters, otherwise we use the literal
+    // string as the name and no arguments are necessary.
+    if (!message.hasName) {
+      if (generateNameAndArgs && message.arguments.isNotEmpty) {
+        // Always try for class_method if this is a class method and
+        // generating names/args.
+        message.name = Message.classPlusMethodName(node, name) ?? name;
+      } else if (arguments.first is SimpleStringLiteral ||
+          arguments.first is AdjacentStrings) {
+        // If there's no name, and the message text is a simple string, compute
+        // a name based on that plus meaning, if present.
+        var simpleName = (arguments.first as StringLiteral).stringValue;
+        message.name =
+            computeMessageName(message.name, simpleName, message.meaning);
+      }
+    }
+    return message;
+  }
+
+  /// Find the message pieces from a Dart interpolated string.
+  List _extractFromIntlCallWithInterpolation(
+      MainMessage message, List<AstNode> arguments) {
+    var interpolation = new InterpolationVisitor(message, extraction);
+    arguments.first.accept(interpolation);
+    if (interpolation.pieces.any((x) => x is Plural || x is Gender) &&
+        !extraction.allowEmbeddedPluralsAndGenders) {
+      if (interpolation.pieces.any((x) => x is String && x.isNotEmpty)) {
+        throw new IntlMessageExtractionException(
+            "Plural and gender expressions must be at the top level, "
+            "they cannot be embedded in larger string literals.\n");
+      }
+    }
+    return interpolation.pieces;
+  }
+
+  /// Create a MainMessage from [node] using the name and
+  /// parameters of the last function/method declaration we encountered
+  /// and the parameters to the Intl.message call.
+  MainMessage messageFromIntlMessageCall(MethodInvocation node) {
+    MainMessage extractFromIntlCall(
+        MainMessage message, List<AstNode> arguments) {
+      try {
+        // The pieces of the message, either literal strings, or integers
+        // representing the index of the argument to be substituted.
+        List extracted;
+        extracted = _extractFromIntlCallWithInterpolation(message, arguments);
+        message.addPieces(extracted);
+      } on IntlMessageExtractionException catch (e) {
+        message = null;
+        var err = new StringBuffer()
+          ..writeAll(["Error ", e, "\nProcessing <", node, ">\n"])
+          ..write(extraction._reportErrorLocation(node));
+        var errString = err.toString();
+        extraction.onMessage(errString);
+        extraction.warnings.add(errString);
+      }
+      return message;
+    }
+
+    void setValue(MainMessage message, String fieldName, Object fieldValue) {
+      message[fieldName] = fieldValue;
+    }
+
+    return _messageFromNode(node, extractFromIntlCall, setValue);
+  }
+
+  /// Create a MainMessage from [node] using the name and
+  /// parameters of the last function/method declaration we encountered
+  /// and the parameters to the Intl.plural or Intl.gender call.
+  MainMessage messageFromDirectPluralOrGenderCall(MethodInvocation node) {
+    MainMessage extractFromPluralOrGender(MainMessage message, _) {
+      var visitor = new PluralAndGenderVisitor(
+          message.messagePieces, message, extraction);
+      node.accept(visitor);
+      return message;
+    }
+
+    void setAttribute(MainMessage msg, String fieldName, fieldValue) {
+      if (msg.attributeNames.contains(fieldName)) {
+        msg[fieldName] = fieldValue;
+      }
+    }
+    return _messageFromNode(node, extractFromPluralOrGender, setAttribute);
+  }
+}
+
+/// Given an interpolation, find all of its chunks, validate that they are only
+/// simple variable substitutions or else Intl.plural/gender calls,
+/// and keep track of the pieces of text so that other parts
+/// of the program can deal with the simple string sections and the generated
+/// parts separately. Note that this is a SimpleAstVisitor, so it only
+/// traverses one level of children rather than automatically recursing. If we
+/// find a plural or gender, which requires recursion, we do it with a separate
+/// special-purpose visitor.
+class InterpolationVisitor extends SimpleAstVisitor {
+  final Message message;
+
+  /// The message extraction in which we are running.
+  final MessageExtraction extraction;
+
+  InterpolationVisitor(this.message, this.extraction);
+
+  List pieces = [];
+  String get extractedMessage => pieces.join();
+
+  void visitAdjacentStrings(AdjacentStrings node) {
+    node.visitChildren(this);
+    super.visitAdjacentStrings(node);
+  }
+
+  void visitStringInterpolation(StringInterpolation node) {
+    node.visitChildren(this);
+    super.visitStringInterpolation(node);
+  }
+
+  void visitSimpleStringLiteral(SimpleStringLiteral node) {
+    pieces.add(node.value);
+    super.visitSimpleStringLiteral(node);
+  }
+
+  void visitInterpolationString(InterpolationString node) {
+    pieces.add(node.value);
+    super.visitInterpolationString(node);
+  }
+
+  void visitInterpolationExpression(InterpolationExpression node) {
+    if (node.expression is SimpleIdentifier) {
+      return handleSimpleInterpolation(node);
+    } else {
+      return lookForPluralOrGender(node);
+    }
+    // Note that we never end up calling super.
+  }
+
+  lookForPluralOrGender(InterpolationExpression node) {
+    var visitor = new PluralAndGenderVisitor(pieces, message, extraction);
+    node.accept(visitor);
+    if (!visitor.foundPluralOrGender) {
+      throw new IntlMessageExtractionException(
+          "Only simple identifiers and Intl.plural/gender/select expressions "
+          "are allowed in message "
+          "interpolation expressions.\nError at $node");
+    }
+  }
+
+  void handleSimpleInterpolation(InterpolationExpression node) {
+    var index = arguments.indexOf(node.expression.toString());
+    if (index == -1) {
+      throw new IntlMessageExtractionException(
+          "Cannot find argument ${node.expression}");
+    }
+    pieces.add(index);
+  }
+
+  List get arguments => message.arguments;
+}
+
+/// A visitor to extract information from Intl.plural/gender sends. Note that
+/// this is a SimpleAstVisitor, so it doesn't automatically recurse. So this
+/// needs to be called where we expect a plural or gender immediately below.
+class PluralAndGenderVisitor extends SimpleAstVisitor {
+  /// The message extraction in which we are running.
+  final MessageExtraction extraction;
+
+  /// A plural or gender always exists in the context of a parent message,
+  /// which could in turn also be a plural or gender.
+  final ComplexMessage parent;
+
+  /// The pieces of the message. We are given an initial version of this
+  /// from our parent and we add to it as we find additional information.
+  List pieces;
+
+  /// This will be set to true if we find a plural or gender.
+  bool foundPluralOrGender = false;
+
+  PluralAndGenderVisitor(this.pieces, this.parent, this.extraction) : super();
+
+  visitInterpolationExpression(InterpolationExpression node) {
+    // TODO(alanknight): Provide better errors for malformed expressions.
+    if (!looksLikePluralOrGender(node.expression)) return;
+    var reason = checkValidity(node.expression);
+    if (reason != null) throw reason;
+    var message = messageFromMethodInvocation(node.expression);
+    foundPluralOrGender = true;
+    pieces.add(message);
+    super.visitInterpolationExpression(node);
+  }
+
+  visitMethodInvocation(MethodInvocation node) {
+    pieces.add(messageFromMethodInvocation(node));
+    super.visitMethodInvocation(node);
+  }
+
+  /// Return true if [node] matches the pattern for plural or gender message.
+  bool looksLikePluralOrGender(MethodInvocation node) {
+    if (!["plural", "gender", "select"].contains(node.methodName.name)) {
+      return false;
+    }
+    if (!(node.target is SimpleIdentifier)) return false;
+    SimpleIdentifier target = node.target;
+    return target.token.toString() == "Intl";
+  }
+
+  /// Returns a String describing why the node is invalid, or null if no
+  /// reason is found, so it's presumed valid.
+  String checkValidity(MethodInvocation node) {
+    // TODO(alanknight): Add reasonable validity checks.
+    return null;
+  }
+
+  /// Create a MainMessage from [node] using the name and
+  /// parameters of the last function/method declaration we encountered
+  /// and the parameters to the Intl.message call.
+  Message messageFromMethodInvocation(MethodInvocation node) {
+    var message;
+    switch (node.methodName.name) {
+      case "gender":
+        message = new Gender();
+        break;
+      case "plural":
+        message = new Plural();
+        break;
+      case "select":
+        message = new Select();
+        break;
+      default:
+        throw new IntlMessageExtractionException(
+            "Invalid plural/gender/select message ${node.methodName.name} "
+            "in $node");
+    }
+    message.parent = parent;
+
+    var arguments = message.argumentsOfInterestFor(node);
+    arguments.forEach((key, value) {
+      try {
+        var interpolation = new InterpolationVisitor(message, extraction);
+        value.accept(interpolation);
+        message[key] = interpolation.pieces;
+      } on IntlMessageExtractionException catch (e) {
+        message = null;
+        var err = new StringBuffer()
+          ..writeAll(["Error ", e, "\nProcessing <", node, ">"])
+          ..write(extraction._reportErrorLocation(node));
+        var errString = err.toString();
+        extraction.onMessage(errString);
+        extraction.warnings.add(errString);
+      }
+    });
+    var mainArg = node.argumentList.arguments
+        .firstWhere((each) => each is! NamedExpression);
+    if (mainArg is SimpleStringLiteral) {
+      message.mainArgument = mainArg.toString();
+    } else if (mainArg is SimpleIdentifier) {
+      message.mainArgument = mainArg.name;
+    } else {
+      var err = new StringBuffer()
+        ..write("Error (Invalid argument to plural/gender/select, "
+            "must be simple variable reference) "
+            "\nProcessing <$node>")
+        ..write(extraction._reportErrorLocation(node));
+      var errString = err.toString();
+      extraction.onMessage(errString);
+      extraction.warnings.add(errString);
+    }
+    return message;
+  }
+}
+
+/// Exception thrown when we cannot process a message properly.
+class IntlMessageExtractionException implements Exception {
+  /// A message describing the error.
+  final String message;
+
+  /// Creates a new exception with an optional error [message].
+  const IntlMessageExtractionException([this.message = ""]);
+
+  String toString() => "IntlMessageExtractionException: $message";
+}
diff --git a/lib/generate_localized.dart b/lib/generate_localized.dart
new file mode 100644
index 0000000..f443ad7
--- /dev/null
+++ b/lib/generate_localized.dart
@@ -0,0 +1,274 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This provides utilities for generating localized versions of
+/// messages. It does not stand alone, but expects to be given
+/// TranslatedMessage objects and generate code for a particular locale
+/// based on them.
+///
+/// An example of usage can be found
+/// in test/message_extract/generate_from_json.dart
+library generate_localized;
+
+import 'intl.dart';
+import 'src/intl_message.dart';
+import 'dart:io';
+import 'package:path/path.dart' as path;
+
+class MessageGeneration {
+  /// If the import path following package: is something else, modify the
+  /// [intlImportPath] variable to change the import directives in the generated
+  /// code.
+  var intlImportPath = 'intl';
+
+  /// If the path to the generated files is something other than the current
+  /// directory, update the [generatedImportPath] variable to change the import
+  /// directives in the generated code.
+  var generatedImportPath = '';
+
+  /// Given a base file, return the file prefixed with the path to import it.
+  /// By default, that is in the current directory, but if [generatedImportPath]
+  /// has been set, then use that as a prefix.
+  String importForGeneratedFile(String file) =>
+      generatedImportPath.isEmpty ? file : "$generatedImportPath/$file";
+
+  /// A list of all the locales for which we have translations. Code that does
+  /// the reading of translations should add to this.
+  List<String> allLocales = [];
+
+  /// If we have more than one set of messages to generate in a particular
+  /// directory we may want to prefix some to distinguish them.
+  String generatedFilePrefix = '';
+
+  /// Should we use deferred loading for the generated libraries.
+  bool useDeferredLoading = true;
+
+  /// Generate a file <[generated_file_prefix]>_messages_<[locale]>.dart
+  /// for the [translations] in [locale] and put it in [targetDir].
+  void generateIndividualMessageFile(String basicLocale,
+      Iterable<TranslatedMessage> translations, String targetDir) {
+    var result = new StringBuffer();
+    var locale = new MainMessage()
+        .escapeAndValidateString(Intl.canonicalizedLocale(basicLocale));
+    result.write(prologue(locale));
+    // Exclude messages with no translation and translations with no matching
+    // original message (e.g. if we're using some messages from a larger catalog)
+    var usableTranslations = translations
+        .where((each) => each.originalMessages != null && each.message != null)
+        .toList();
+    for (var each in usableTranslations) {
+      for (var original in each.originalMessages) {
+        original.addTranslation(locale, each.message);
+      }
+    }
+    usableTranslations.sort((a, b) =>
+        a.originalMessages.first.name.compareTo(b.originalMessages.first.name));
+    for (var translation in usableTranslations) {
+      // Some messages we generate as methods in this class. Simpler ones
+      // we inline in the map from names to messages.
+      var messagesThatNeedMethods =
+          translation.originalMessages.where((each) => _hasArguments(each));
+      for (var original in messagesThatNeedMethods) {
+        result
+          ..write("  ")
+          ..write(
+              original.toCodeForLocale(locale, _methodNameFor(original.name)))
+          ..write("\n\n");
+      }
+    }
+    // Some gyrations to prevent parts of the deferred libraries from being
+    // inlined into the main one, defeating the space savings. Issue 24356
+    result.write("""
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static _notInlinedMessages(_) => {
+""");
+    var entries = usableTranslations
+        .expand((translation) => translation.originalMessages)
+        .map((original) =>
+            '    "${original.escapeAndValidateString(original.name)}" '
+            ': ${_mapReference(original, locale)}');
+    result..write(entries.join(",\n"))..write("\n  };\n}\n");
+
+    // To preserve compatibility, we don't use the canonical version of the
+    // locale in the file name.
+    var filename = path.join(
+        targetDir, "${generatedFilePrefix}messages_$basicLocale.dart");
+    new File(filename).writeAsStringSync(result.toString());
+  }
+
+  /// This returns the mostly constant string used in
+  /// [generateIndividualMessageFile] for the beginning of the file,
+  /// parameterized by [locale].
+  String prologue(String locale) => """
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a $locale locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+import 'package:$intlImportPath/intl.dart';
+import 'package:$intlImportPath/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+final _keepAnalysisHappy = Intl.defaultLocale;
+
+class MessageLookup extends MessageLookupByLibrary {
+  get localeName => '$locale';
+
+""";
+
+  /// This section generates the messages_all.dart file based on the list of
+  /// [allLocales].
+  String generateMainImportFile() {
+    var output = new StringBuffer();
+    output.write(mainPrologue);
+    for (var locale in allLocales) {
+      var baseFile = '${generatedFilePrefix}messages_$locale.dart';
+      var file = importForGeneratedFile(baseFile);
+      output.write("import '$file' ");
+      if (useDeferredLoading) output.write("deferred ");
+      output.write("as ${_libraryName(locale)};\n");
+    }
+    output.write("\n");
+    output.write("Map<String, Function> _deferredLibraries = {\n");
+    for (var rawLocale in allLocales) {
+      var locale = Intl.canonicalizedLocale(rawLocale);
+      var loadOperation = (useDeferredLoading)
+          ? "  '$locale': () => ${_libraryName(locale)}.loadLibrary(),\n"
+          : "  '$locale': () => new Future.value(null),\n";
+      output.write(loadOperation);
+    }
+    output.write("};\n");
+    output.write("\nMessageLookupByLibrary _findExact(localeName) {\n"
+        "  switch (localeName) {\n");
+    for (var rawLocale in allLocales) {
+      var locale = Intl.canonicalizedLocale(rawLocale);
+      output.write(
+          "    case '$locale':\n      return ${_libraryName(locale)}.messages;\n");
+    }
+    output.write(closing);
+    return output.toString();
+  }
+
+  /// Constant string used in [generateMainImportFile] for the beginning of the
+  /// file.
+  get mainPrologue => """
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that looks up messages for specific locales by
+// delegating to the appropriate library.
+
+import 'dart:async';
+
+import 'package:$intlImportPath/intl.dart';
+import 'package:$intlImportPath/message_lookup_by_library.dart';
+import 'package:$intlImportPath/src/intl_helpers.dart';
+
+""";
+
+  /// Constant string used in [generateMainImportFile] as the end of the file.
+  static const closing = """
+    default:\n      return null;
+  }
+}
+
+/// User programs should call this before using [localeName] for messages.
+Future initializeMessages(String localeName) {
+  var lib = _deferredLibraries[Intl.canonicalizedLocale(localeName)];
+  var load = lib == null ? new Future.value(false) : lib();
+  return load.then((_) {
+    initializeInternalMessageLookup(() => new CompositeMessageLookup());
+    messageLookup.addLocale(localeName, _findGeneratedMessagesFor);
+  });
+}
+
+bool _messagesExistFor(String locale) {
+  var messages;
+  try {
+    messages = _findExact(locale);
+  } catch (e) {}
+  return messages != null;
+}
+
+MessageLookupByLibrary _findGeneratedMessagesFor(locale) {
+  var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
+      onFailure: (_) => null);
+  if (actualLocale == null) return null;
+  return _findExact(actualLocale);
+}
+""";
+}
+
+/// This represents a message and its translation. We assume that the
+/// translation has some identifier that allows us to figure out the original
+/// message it corresponds to, and that it may want to transform the translated
+/// text in some way, e.g. to turn whatever format the translation uses for
+/// variables into a Dart string interpolation. Specific translation mechanisms
+/// are expected to subclass this.
+abstract class TranslatedMessage {
+  /// The identifier for this message. In the simplest case, this is the name
+  /// parameter from the Intl.message call,
+  /// but it can be any identifier that this program and the output of the
+  /// translation can agree on as identifying a message.
+  final String id;
+
+  /// Our translated version of all the [originalMessages].
+  final Message translated;
+
+  /// The original messages that we are a translation of. There can
+  ///  be more than one original message for the same translation.
+  List<MainMessage> _originalMessages;
+
+  List<MainMessage> get originalMessages => _originalMessages;
+  set originalMessages(List<MainMessage> x) {
+    _originalMessages = x;
+  }
+
+  /// For backward compatibility, we still have the originalMessage API.
+  MainMessage get originalMessage => originalMessages.first;
+  set originalMessage(MainMessage m) {
+    originalMessages = [m];
+  }
+
+  TranslatedMessage(this.id, this.translated);
+
+  Message get message => translated;
+
+  toString() => id.toString();
+}
+
+/// We can't use a hyphen in a Dart library name, so convert the locale
+/// separator to an underscore.
+String _libraryName(String x) => 'messages_' + x.replaceAll('-', '_');
+
+bool _hasArguments(MainMessage message) => message.arguments.length != 0;
+
+///  Simple messages are printed directly in the map of message names to
+///  functions as a call that returns a lambda. e.g.
+///
+///        "foo" : simpleMessage("This is foo"),
+///
+///  This is helpful for the compiler.
+/// */
+String _mapReference(MainMessage original, String locale) {
+  if (!_hasArguments(original)) {
+    // No parameters, can be printed simply.
+    return 'MessageLookupByLibrary.simpleMessage("'
+        '${original.translations[locale]}")';
+  } else {
+    return _methodNameFor(original.name);
+  }
+}
+
+/// Generated method counter for use in [_methodNameFor].
+int _methodNameCounter = 0;
+
+/// A map from Intl message names to the generated method names
+/// for their translated versions.
+Map<String, String> _internalMethodNames = {};
+
+/// Generate a Dart method name of the form "m<number>".
+String _methodNameFor(String name) {
+  return _internalMethodNames.putIfAbsent(
+      name, () => "m${_methodNameCounter++}");
+}
diff --git a/lib/src/icu_parser.dart b/lib/src/icu_parser.dart
new file mode 100644
index 0000000..8603a3b
--- /dev/null
+++ b/lib/src/icu_parser.dart
@@ -0,0 +1,98 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a parser for ICU format plural/gender/select format for localized
+/// messages. See extract_to_arb.dart and make_hardcoded_translation.dart.
+library icu_parser;
+
+import 'package:intl/src/intl_message.dart';
+import 'package:petitparser/petitparser.dart';
+
+/// This defines a grammar for ICU MessageFormat syntax. Usage is
+///       new IcuParser.message.parse(<string>).value;
+/// The "parse" method will return a Success or Failure object which responds
+/// to "value".
+class IcuParser {
+  get openCurly => char("{");
+
+  get closeCurly => char("}");
+  get quotedCurly => (string("'{'") | string("'}'")).map((x) => x[1]);
+
+  get icuEscapedText => quotedCurly | twoSingleQuotes;
+  get curly => (openCurly | closeCurly);
+  get notAllowedInIcuText => curly | char("<");
+  get icuText => notAllowedInIcuText.neg();
+  get notAllowedInNormalText => char("{");
+  get normalText => notAllowedInNormalText.neg();
+  get messageText => (icuEscapedText | icuText).plus().map((x) => x.join());
+  get nonIcuMessageText => normalText.plus().map((x) => x.join());
+  get twoSingleQuotes => string("''").map((x) => "'");
+  get number => digit().plus().flatten().trim().map(int.parse);
+  get id => (letter() & (word() | char("_")).star()).flatten().trim();
+  get comma => char(",").trim();
+
+  /// Given a list of possible keywords, return a rule that accepts any of them.
+  /// e.g., given ["male", "female", "other"], accept any of them.
+  asKeywords(list) => list.map(string).reduce((a, b) => a | b).flatten().trim();
+
+  get pluralKeyword => asKeywords(
+      ["=0", "=1", "=2", "zero", "one", "two", "few", "many", "other"]);
+  get genderKeyword => asKeywords(["female", "male", "other"]);
+
+  var interiorText = undefined();
+
+  get preface => (openCurly & id & comma).map((values) => values[1]);
+
+  get pluralLiteral => string("plural");
+  get pluralClause => (pluralKeyword & openCurly & interiorText & closeCurly)
+      .trim()
+      .map((result) => [result[0], result[2]]);
+  get plural =>
+      preface & pluralLiteral & comma & pluralClause.plus() & closeCurly;
+  get intlPlural =>
+      plural.map((values) => new Plural.from(values.first, values[3], null));
+
+  get selectLiteral => string("select");
+  get genderClause => (genderKeyword & openCurly & interiorText & closeCurly)
+      .trim()
+      .map((result) => [result[0], result[2]]);
+  get gender =>
+      preface & selectLiteral & comma & genderClause.plus() & closeCurly;
+  get intlGender =>
+      gender.map((values) => new Gender.from(values.first, values[3], null));
+  get selectClause =>
+      (id & openCurly & interiorText & closeCurly).map((x) => [x.first, x[2]]);
+  get generalSelect =>
+      preface & selectLiteral & comma & selectClause.plus() & closeCurly;
+  get intlSelect => generalSelect
+      .map((values) => new Select.from(values.first, values[3], null));
+
+  get pluralOrGenderOrSelect => intlPlural | intlGender | intlSelect;
+
+  get contents => pluralOrGenderOrSelect | parameter | messageText;
+  get simpleText => (nonIcuMessageText | parameter | openCurly).plus();
+  get empty => epsilon().map((_) => '');
+
+  get parameter => (openCurly & id & closeCurly)
+      .map((param) => new VariableSubstitution.named(param[1], null));
+
+  /// The primary entry point for parsing. Accepts a string and produces
+  /// a parsed representation of it as a Message.
+  get message => (pluralOrGenderOrSelect | empty)
+      .map((chunk) => Message.from(chunk, null));
+
+  /// Represents an ordinary message, i.e. not a plural/gender/select, although
+  /// it may have parameters.
+  get nonIcuMessage =>
+      (simpleText | empty).map((chunk) => Message.from(chunk, null));
+
+  get stuff => (pluralOrGenderOrSelect | empty)
+      .map((chunk) => Message.from(chunk, null));
+
+  IcuParser() {
+    // There is a cycle here, so we need the explicit set to avoid
+    // infinite recursion.
+    interiorText.set(contents.plus() | empty);
+  }
+}
diff --git a/lib/src/intl_helpers.dart b/lib/src/intl_helpers.dart
index 9d9cb5f..8e3c0f0 100644
--- a/lib/src/intl_helpers.dart
+++ b/lib/src/intl_helpers.dart
@@ -24,8 +24,8 @@
   operator [](String key) =>
       (key == 'en_US') ? fallbackData : _throwException();
 
-  String lookupMessage(String message_str, String locale, String name,
-          List args, String meaning,
+  String lookupMessage(
+      String message_str, String locale, String name, List args, String meaning,
           {MessageIfAbsent ifAbsent}) =>
       message_str;
 
@@ -78,10 +78,10 @@
   }
 }
 
-/// If a message is a string literal without interpolation, compute
-/// a name based on that and the meaning, if present.
-// NOTE: THIS LOGIC IS DUPLICATED IN intl_translation AND THE TWO MUST MATCH.
-String computeMessageName(String name, String text, String meaning) {
-  if (name != null && name != "") return name;
-  return meaning == null ? text : "${text}_${meaning}";
-}
+  /// If a message is a string literal without interpolation, compute
+  /// a name based on that and the meaning, if present.
+  String computeMessageName(String name, String message_str, String meaning) {
+     if (name != null && name != "") return name;
+     var simpleName = message_str;
+     return meaning == null ? simpleName : "${simpleName}_${meaning}";
+  }
diff --git a/lib/src/intl_message.dart b/lib/src/intl_message.dart
new file mode 100644
index 0000000..45ac177
--- /dev/null
+++ b/lib/src/intl_message.dart
@@ -0,0 +1,801 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This provides classes to represent the internal structure of the
+/// arguments to `Intl.message`. It is used when parsing sources to extract
+/// messages or to generate code for message substitution. Normal programs
+/// using Intl would not import this library.
+///
+/// While it's written
+/// in a somewhat abstract way, it has some assumptions about ICU-style
+/// message syntax for parameter substitutions, choices, selects, etc.
+///
+/// For example, if we have the message
+///      plurals(num) => Intl.message("""${Intl.plural(num,
+///          zero : 'Is zero plural?',
+///          one : 'This is singular.',
+///          other : 'This is plural ($num).')
+///         }""",
+///         name: "plurals", args: [num], desc: "Basic plurals");
+/// That is represented as a MainMessage which has only one message component, a
+/// Plural, but also has a name, list of arguments, and a description.
+/// The Plural has three different clauses. The `zero` clause is
+/// a LiteralString containing 'Is zero plural?'. The `other` clause is a
+/// CompositeMessage containing three pieces, a LiteralString for
+/// 'This is plural (', a VariableSubstitution for `num`. amd a LiteralString
+/// for '.)'.
+///
+/// This representation isn't used at runtime. Rather, we read some format
+/// from a translation file, parse it into these objects, and they are then
+/// used to generate the code representation above.
+library intl_message;
+
+import 'dart:convert';
+import 'package:analyzer/analyzer.dart';
+
+/// A default function for the [Message.expanded] method.
+_nullTransform(msg, chunk) => chunk;
+
+/// An abstract superclass for Intl.message/plural/gender calls in the
+/// program's source text. We
+/// assemble these into objects that can be used to write out some translation
+/// format and can also print themselves into code.
+abstract class Message {
+  /// All [Message]s except a [MainMessage] are contained inside some parent,
+  /// terminating at an Intl.message call which supplies the arguments we
+  /// use for variable substitutions.
+  Message parent;
+
+  Message(this.parent);
+
+  /// We find the arguments from the top-level [MainMessage] and use those to
+  /// do variable substitutions. [MainMessage] overrides this to return
+  /// the actual arguments.
+  get arguments => parent == null ? const [] : parent.arguments;
+
+  /// We find the examples from the top-level [MainMessage] and use those
+  /// when writing out variables. [MainMessage] overrides this to return
+  /// the actual examples.
+  get examples => parent == null ? const [] : parent.examples;
+
+  /// The name of the top-level [MainMessage].
+  String get name => parent == null ? '<unnamed>' : parent.name;
+
+  static final _evaluator = new ConstantEvaluator();
+
+  String _evaluateAsString(expression) {
+    var result = expression.accept(_evaluator);
+    if (result == ConstantEvaluator.NOT_A_CONSTANT || result is! String) {
+      return null;
+    } else {
+      return result;
+    }
+  }
+
+  Map _evaluateAsMap(expression) {
+    var result = expression.accept(_evaluator);
+    if (result == ConstantEvaluator.NOT_A_CONSTANT || result is! Map) {
+      return null;
+    } else {
+      return result;
+    }
+  }
+
+  /// Verify that this looks like a correct
+  /// Intl.message/plural/gender/... invocation.
+  ///
+  /// We expect an invocation like
+  ///
+  ///       outerName(x) => Intl.message("foo \$x", ...)
+  ///
+  /// The [node] parameter is the Intl.message invocation node in the AST,
+  /// [arguments] is the list of arguments to that node (also reachable as
+  /// node.argumentList.arguments), [outerName] is the name of the containing
+  /// function, e.g. "outerName" in this case and [outerArgs] is the list of
+  /// arguments to that function. Of the optional parameters
+  /// [nameAndArgsGenerated] indicates if we are generating names and arguments
+  /// while rewriting the code in the transformer or a development-time rewrite,
+  /// so we should not expect them to be present. The [examplesRequired]
+  /// parameter indicates if we will fail if parameter examples are not provided
+  /// for messages with parameters.
+  String checkValidity(MethodInvocation node, List arguments, String outerName,
+      FormalParameterList outerArgs,
+      {bool nameAndArgsGenerated: false, bool examplesRequired: false}) {
+    // If we have parameters, we must specify args and name.
+    var hasArgs = arguments.any(
+        (each) => each is NamedExpression && each.name.label.name == 'args');
+    var hasParameters = !outerArgs.parameters.isEmpty;
+    if (!nameAndArgsGenerated && !hasArgs && hasParameters) {
+      return "The 'args' argument for Intl.message must be specified";
+    }
+
+    var messageNameArgument = arguments.firstWhere(
+        (eachArg) =>
+            eachArg is NamedExpression && eachArg.name.label.name == 'name',
+        orElse: () => null);
+    var nameExpression = messageNameArgument?.expression;
+    String messageName;
+    String givenName;
+
+    //TODO(alanknight): If we generalize this to messages with parameters
+    // this check will need to change.
+    if (nameExpression == null) {
+      if (!hasParameters) {
+        // No name supplied, no parameters. Use the message as the name.
+        messageName = _evaluateAsString(arguments[0]);
+        outerName = messageName;
+      } else {
+        // We have no name and parameters, but the transformer generates the
+        // name.
+        if (nameAndArgsGenerated) {
+          givenName = outerName;
+          messageName = givenName;
+        } else {
+          return "The 'name' argument for Intl.message must be supplied for "
+              "messages with parameters";
+        }
+      }
+    } else {
+      // Name argument is supplied, use it.
+      givenName = _evaluateAsString(nameExpression);
+      messageName = givenName;
+    }
+
+    if (messageName == null) {
+      return "The 'name' argument for Intl.message must be a string literal";
+    }
+
+    var hasOuterName = outerName != null;
+    var simpleMatch = outerName == givenName || givenName == null;
+
+    var classPlusMethod = Message.classPlusMethodName(node, outerName);
+    var classMatch = classPlusMethod != null && (givenName == classPlusMethod);
+    if (!(hasOuterName && (simpleMatch || classMatch))) {
+      return "The 'name' argument for Intl.message must match either "
+          "the name of the containing function or <ClassName>_<methodName> ("
+          "was '$givenName' but must be '$outerName'  or '$classPlusMethod')";
+    }
+
+    var simpleArguments = arguments.where((each) =>
+        each is NamedExpression &&
+        ["desc", "name"].contains(each.name.label.name));
+    var values = simpleArguments.map((each) => each.expression).toList();
+    for (var arg in values) {
+      if (_evaluateAsString(arg) == null) {
+        return ("Intl.message arguments must be string literals: $arg");
+      }
+    }
+
+    if (hasParameters && examplesRequired) {
+      var exampleArg = arguments.where((each) =>
+          each is NamedExpression && each.name.label.name == "examples");
+      var examples = exampleArg.map((each) => each.expression).toList();
+      if (examples.isEmpty) {
+        return "Examples must be provided for messages with parameters";
+      }
+      var map = _evaluateAsMap(examples.first);
+      if (map == null) {
+        return "Examples must be a Map literal, preferably const";
+      }
+    }
+
+    return null;
+  }
+
+  /// Return the name of the enclosing class (if any) plus method name, or null
+  /// if there's no enclosing class.
+  ///
+  /// For a method foo in class Bar we allow either "foo" or "Bar_Foo" as the
+  /// name.
+  static String classPlusMethodName(MethodInvocation node, String outerName) {
+    ClassDeclaration classNode(n) {
+      if (n == null) return null;
+      if (n is ClassDeclaration) return n;
+      return classNode(n.parent);
+    }
+    var classDeclaration = classNode(node);
+    return classDeclaration == null
+        ? null
+        : "${classDeclaration.name.token}_$outerName";
+  }
+
+  /// Turn a value, typically read from a translation file or created out of an
+  /// AST for a source program, into the appropriate
+  /// subclass. We expect to get literal Strings, variable substitutions
+  /// represented by integers, things that are already MessageChunks and
+  /// lists of the same.
+  static Message from(Object value, Message parent) {
+    if (value is String) return new LiteralString(value, parent);
+    if (value is int) return new VariableSubstitution(value, parent);
+    if (value is List) {
+      if (value.length == 1) return Message.from(value[0], parent);
+      var result = new CompositeMessage([], parent);
+      var items = value.map((x) => from(x, result)).toList();
+      result.pieces.addAll(items);
+      return result;
+    }
+    // We assume this is already a Message.
+    Message mustBeAMessage = value;
+    mustBeAMessage.parent = parent;
+    return mustBeAMessage;
+  }
+
+  /// Return a string representation of this message for use in generated Dart
+  /// code.
+  String toCode();
+
+  /// Escape the string for use in generated Dart code.
+  String escapeAndValidateString(String value) {
+    const Map<String, String> escapes = const {
+      r"\": r"\\",
+      '"': r'\"',
+      "\b": r"\b",
+      "\f": r"\f",
+      "\n": r"\n",
+      "\r": r"\r",
+      "\t": r"\t",
+      "\v": r"\v",
+      "'": r"\'",
+      r"$": r"\$"
+    };
+
+    String _escape(String s) => escapes[s] ?? s;
+
+    var escaped = value.splitMapJoin("", onNonMatch: _escape);
+    return escaped;
+  }
+
+  /// Expand this string out into a printed form. The function [f] will be
+  /// applied to any sub-messages, allowing this to be used to generate a form
+  /// suitable for a wide variety of translation file formats.
+  String expanded([Function f]);
+}
+
+/// Abstract class for messages with internal structure, representing the
+/// main Intl.message call, plurals, and genders.
+abstract class ComplexMessage extends Message {
+  ComplexMessage(parent) : super(parent);
+
+  /// When we create these from strings or from AST nodes, we want to look up
+  /// and set their attributes by string names, so we override the indexing
+  /// operators so that they behave like maps with respect to those attribute
+  /// names.
+  operator [](String x);
+
+  /// When we create these from strings or from AST nodes, we want to look up
+  /// and set their attributes by string names, so we override the indexing
+  /// operators so that they behave like maps with respect to those attribute
+  /// names.
+  operator []=(String x, y);
+
+  List<String> get attributeNames;
+
+  /// Return the name of the message type, as it will be generated into an
+  /// ICU-type format. e.g. choice, select
+  String get icuMessageName;
+
+  /// Return the message name we would use for this when doing Dart code
+  /// generation, e.g. "Intl.plural".
+  String get dartMessageName;
+}
+
+/// This represents a message chunk that is a list of multiple sub-pieces,
+/// each of which is in turn a [Message].
+class CompositeMessage extends Message {
+  List<Message> pieces;
+
+  CompositeMessage.withParent(parent) : super(parent);
+  CompositeMessage(this.pieces, ComplexMessage parent) : super(parent) {
+    pieces.forEach((x) => x.parent = this);
+  }
+  toCode() => pieces.map((each) => each.toCode()).join('');
+  toString() => "CompositeMessage(" + pieces.toString() + ")";
+  String expanded([Function f = _nullTransform]) =>
+      pieces.map((chunk) => f(this, chunk)).join("");
+}
+
+/// Represents a simple constant string with no dynamic elements.
+class LiteralString extends Message {
+  String string;
+  LiteralString(this.string, Message parent) : super(parent);
+  toCode() => escapeAndValidateString(string);
+  toString() => "Literal($string)";
+  String expanded([Function f = _nullTransform]) => f(this, string);
+}
+
+/// Represents an interpolation of a variable value in a message. We expect
+/// this to be specified as an [index] into the list of variables, or else
+/// as the name of a variable that exists in [arguments] and we will
+/// compute the variable name or the index based on the value of the other.
+class VariableSubstitution extends Message {
+  VariableSubstitution(this._index, Message parent) : super(parent);
+
+  /// Create a substitution based on the name rather than the index. The name
+  /// may have been used as all upper-case in the translation tool, so we
+  /// save it separately and look it up case-insensitively once the parent
+  /// (and its arguments) are definitely available.
+  VariableSubstitution.named(String name, Message parent) : super(parent) {
+    _variableNameUpper = name.toUpperCase();
+  }
+
+  /// The index in the list of parameters of the containing function.
+  int _index;
+  int get index {
+    if (_index != null) return _index;
+    if (arguments.isEmpty) return null;
+    // We may have been given an all-uppercase version of the name, so compare
+    // case-insensitive.
+    _index = arguments
+        .map((x) => x.toUpperCase())
+        .toList()
+        .indexOf(_variableNameUpper);
+    if (_index == -1) {
+      throw new ArgumentError(
+          "Cannot find parameter named '$_variableNameUpper' in "
+          "message named '$name'. Available "
+          "parameters are $arguments");
+    }
+    return _index;
+  }
+
+  /// The variable name we get from parsing. This may be an all uppercase
+  /// version of the Dart argument name.
+  String _variableNameUpper;
+
+  /// The name of the variable in the parameter list of the containing function.
+  /// Used when generating code for the interpolation.
+  String get variableName =>
+      _variableName == null ? _variableName = arguments[index] : _variableName;
+  String _variableName;
+  // Although we only allow simple variable references, we always enclose them
+  // in curly braces so that there's no possibility of ambiguity with
+  // surrounding text.
+  toCode() => "\${${variableName}}";
+  toString() => "VariableSubstitution($index)";
+  String expanded([Function f = _nullTransform]) => f(this, index);
+}
+
+class MainMessage extends ComplexMessage {
+  MainMessage() : super(null);
+
+  /// All the pieces of the message. When we go to print, these will
+  /// all be expanded appropriately. The exact form depends on what we're
+  /// printing it for See [expanded], [toCode].
+  List<Message> messagePieces = [];
+
+  /// The position in the source at which this message starts.
+  int sourcePosition;
+
+  /// The position in the source at which this message ends.
+  int endPosition;
+
+  /// Verify that this looks like a correct Intl.message invocation.
+  String checkValidity(MethodInvocation node, List arguments, String outerName,
+      FormalParameterList outerArgs,
+      {bool nameAndArgsGenerated: false, bool examplesRequired: false}) {
+    if (arguments.first is! StringLiteral) {
+      return "Intl.message messages must be string literals";
+    }
+
+    return super.checkValidity(node, arguments, outerName, outerArgs,
+        nameAndArgsGenerated: nameAndArgsGenerated,
+        examplesRequired: examplesRequired);
+  }
+
+  void addPieces(List<Object> messages) {
+    for (var each in messages) {
+      messagePieces.add(Message.from(each, this));
+    }
+  }
+
+  /// The description provided in the Intl.message call.
+  String description;
+
+  /// The examples from the Intl.message call
+  Map<String, dynamic> examples;
+
+  /// A field to disambiguate two messages that might have exactly the
+  /// same text. The two messages will also need different names, but
+  /// this can be used by machine translation tools to distinguish them.
+  String meaning;
+
+  /// The name, which may come from the function name, from the arguments
+  /// to Intl.message, or we may just re-use the message.
+  String _name;
+
+  /// A placeholder for any other identifier that the translation format
+  /// may want to use.
+  String id;
+
+  /// The arguments list from the Intl.message call.
+  List<String> arguments;
+
+  /// The locale argument from the Intl.message call
+  String locale;
+
+  /// When generating code, we store translations for each locale
+  /// associated with the original message.
+  Map<String, String> translations = new Map();
+
+  /// If the message was not given a name, we use the entire message string as
+  /// the name.
+  String get name => _name ?? "";
+  set name(String newName) {
+    _name = newName;
+  }
+
+  /// Does this message have an assigned name.
+  bool get hasName => _name != null;
+
+  /// Return the full message, with any interpolation expressions transformed
+  /// by [f] and all the results concatenated. The chunk argument to [f] may be
+  /// either a String, an int or an object representing a more complex
+  /// message entity.
+  /// See [messagePieces].
+  String expanded([Function f = _nullTransform]) =>
+      messagePieces.map((chunk) => f(this, chunk)).join("");
+
+  /// Record the translation for this message in the given locale, after
+  /// suitably escaping it.
+  void addTranslation(String locale, Message translated) {
+    translated.parent = this;
+    translations[locale] = translated.toCode();
+  }
+
+  toCode() =>
+      throw new UnsupportedError("MainMessage.toCode requires a locale");
+
+  /// Generate code for this message, expecting it to be part of a map
+  /// keyed by name with values the function that calls Intl.message.
+  String toCodeForLocale(String locale, String name) {
+    var out = new StringBuffer()
+      ..write('static $name(')
+      ..write(arguments.join(", "))
+      ..write(') => "')
+      ..write(translations[locale])
+      ..write('";');
+    return out.toString();
+  }
+
+  turnInterpolationBackIntoStringForm(Message message, chunk) {
+    if (chunk is String) return escapeAndValidateString(chunk);
+    if (chunk is int) return r"${" + message.arguments[chunk] + "}";
+    if (chunk is Message) return chunk.toCode();
+    throw new ArgumentError.value(chunk, "Unexpected value in Intl.message");
+  }
+
+  String toOriginalCode() {
+    var out = new StringBuffer()..write("Intl.message('");
+    out.write(expanded(turnInterpolationBackIntoStringForm));
+    out.write("', ");
+    out.write("name: '$name', ");
+    out.write(locale == null ? "" : "locale: '$locale', ");
+    out.write(description == null
+        ? ""
+        : "desc: '${escapeAndValidateString(description)}', ");
+    // json is already mostly-escaped, but we need to handle interpolations.
+    var json = JSON.encode(examples).replaceAll(r"$", r"\$");
+    out.write(examples == null ? "" : "examples: const ${json}, ");
+    out.write(meaning == null
+        ? ""
+        : "meaning: '${escapeAndValidateString(meaning)}', ");
+    out.write("args: [${arguments.join(', ')}]");
+    out.write(")");
+    return out.toString();
+  }
+
+  /// The AST node will have the attribute names as strings, so we translate
+  /// between those and the fields of the class.
+  void operator []=(String attributeName, value) {
+    switch (attributeName) {
+      case "desc":
+        description = value;
+        return;
+      case "examples":
+        examples = value as Map<String, dynamic>;
+        return;
+      case "name":
+        name = value;
+        return;
+      // We use the actual args from the parser rather than what's given in the
+      // arguments to Intl.message.
+      case "args":
+        return;
+      case "meaning":
+        meaning = value;
+        return;
+      case "locale":
+        locale = value;
+        return;
+      default:
+        return;
+    }
+  }
+
+  /// The AST node will have the attribute names as strings, so we translate
+  /// between those and the fields of the class.
+  operator [](String attributeName) {
+    switch (attributeName) {
+      case "desc":
+        return description;
+      case "examples":
+        return examples;
+      case "name":
+        return name;
+      // We use the actual args from the parser rather than what's given in the
+      // arguments to Intl.message.
+      case "args":
+        return [];
+      case "meaning":
+        return meaning;
+      default:
+        return null;
+    }
+  }
+
+  // This is the top-level construct, so there's no meaningful ICU name.
+  get icuMessageName => '';
+
+  get dartMessageName => "message";
+
+  /// The parameters that the Intl.message call may provide.
+  get attributeNames => const ["name", "desc", "examples", "args", "meaning"];
+
+  String toString() =>
+      "Intl.message(${expanded()}, $name, $description, $examples, $arguments)";
+}
+
+/// An abstract class to represent sub-sections of a message, primarily
+/// plurals and genders.
+abstract class SubMessage extends ComplexMessage {
+  SubMessage() : super(null);
+
+  /// Creates the sub-message, given a list of [clauses] in the sort of form
+  /// that we're likely to get them from parsing a translation file format,
+  /// as a list of [key, value] where value may in turn be a list.
+  SubMessage.from(this.mainArgument, List clauses, parent) : super(parent) {
+    for (var clause in clauses) {
+      this[clause.first] = (clause.last is List) ? clause.last : [clause.last];
+    }
+  }
+
+  toString() => expanded();
+
+  /// The name of the main argument, which is expected to have the value which
+  /// is one of [attributeNames] and is used to decide which clause to use.
+  String mainArgument;
+
+  /// Return the arguments that affect this SubMessage as a map of
+  /// argument names and values.
+  Map argumentsOfInterestFor(MethodInvocation node) {
+    var basicArguments = node.argumentList.arguments;
+    var others = basicArguments.where((each) => each is NamedExpression);
+    return new Map.fromIterable(others,
+        key: (node) => node.name.label.token.value(),
+        value: (node) => node.expression);
+  }
+
+  /// Return the list of attribute names to use when generating code. This
+  ///  may be different from [attributeNames] if there are multiple aliases
+  ///  that map to the same clause.
+  List<String> get codeAttributeNames;
+
+  String expanded([Function transform = _nullTransform]) {
+    fullMessageForClause(String key) =>
+        key + '{' + transform(parent, this[key]).toString() + '}';
+    var clauses = attributeNames
+        .where((key) => this[key] != null)
+        .map(fullMessageForClause)
+        .toList();
+    return "{$mainArgument,$icuMessageName, ${clauses.join("")}}";
+  }
+
+  String toCode() {
+    var out = new StringBuffer();
+    out.write('\${');
+    out.write(dartMessageName);
+    out.write('(');
+    out.write(mainArgument);
+    var args = codeAttributeNames.where((attribute) => this[attribute] != null);
+    args.fold(
+        out, (buffer, arg) => buffer..write(", $arg: '${this[arg].toCode()}'"));
+    out.write(")}");
+    return out.toString();
+  }
+}
+
+/// Represents a message send of [Intl.gender] inside a message that is to
+/// be internationalized. This corresponds to an ICU message syntax "select"
+/// with "male", "female", and "other" as the possible options.
+class Gender extends SubMessage {
+  Gender();
+
+  /// Create a new Gender providing [mainArgument] and the list of possible
+  /// clauses. Each clause is expected to be a list whose first element is a
+  /// variable name and whose second element is either a [String] or
+  /// a list of strings and [Message] or [VariableSubstitution].
+  Gender.from(String mainArgument, List clauses, Message parent)
+      : super.from(mainArgument, clauses, parent);
+
+  Message female;
+  Message male;
+  Message other;
+
+  String get icuMessageName => "select";
+  String get dartMessageName => 'Intl.gender';
+
+  get attributeNames => ["female", "male", "other"];
+  get codeAttributeNames => attributeNames;
+
+  /// The node will have the attribute names as strings, so we translate
+  /// between those and the fields of the class.
+  void operator []=(String attributeName, rawValue) {
+    var value = Message.from(rawValue, this);
+    switch (attributeName) {
+      case "female":
+        female = value;
+        return;
+      case "male":
+        male = value;
+        return;
+      case "other":
+        other = value;
+        return;
+      default:
+        return;
+    }
+  }
+
+  Message operator [](String attributeName) {
+    switch (attributeName) {
+      case "female":
+        return female;
+      case "male":
+        return male;
+      case "other":
+        return other;
+      default:
+        return other;
+    }
+  }
+}
+
+class Plural extends SubMessage {
+  Plural();
+  Plural.from(String mainArgument, List clauses, Message parent)
+      : super.from(mainArgument, clauses, parent);
+
+  Message zero;
+  Message one;
+  Message two;
+  Message few;
+  Message many;
+  Message other;
+
+  String get icuMessageName => "plural";
+  String get dartMessageName => "Intl.plural";
+
+  get attributeNames => ["=0", "=1", "=2", "few", "many", "other"];
+  get codeAttributeNames => ["zero", "one", "two", "few", "many", "other"];
+
+  /// The node will have the attribute names as strings, so we translate
+  /// between those and the fields of the class.
+  void operator []=(String attributeName, rawValue) {
+    var value = Message.from(rawValue, this);
+    switch (attributeName) {
+      case "zero":
+        zero = value;
+        return;
+      case "=0":
+        zero = value;
+        return;
+      case "one":
+        one = value;
+        return;
+      case "=1":
+        one = value;
+        return;
+      case "two":
+        two = value;
+        return;
+      case "=2":
+        two = value;
+        return;
+      case "few":
+        few = value;
+        return;
+      case "many":
+        many = value;
+        return;
+      case "other":
+        other = value;
+        return;
+      default:
+        return;
+    }
+  }
+
+  Message operator [](String attributeName) {
+    switch (attributeName) {
+      case "zero":
+        return zero;
+      case "=0":
+        return zero;
+      case "one":
+        return one;
+      case "=1":
+        return one;
+      case "two":
+        return two;
+      case "=2":
+        return two;
+      case "few":
+        return few;
+      case "many":
+        return many;
+      case "other":
+        return other;
+      default:
+        return other;
+    }
+  }
+}
+
+/// Represents a message send of [Intl.select] inside a message that is to
+/// be internationalized. This corresponds to an ICU message syntax "select"
+/// with arbitrary options.
+class Select extends SubMessage {
+  Select();
+
+  /// Create a new [Select] providing [mainArgument] and the list of possible
+  /// clauses. Each clause is expected to be a list whose first element is a
+  /// variable name and whose second element is either a String or
+  /// a list of strings and [Message]s or [VariableSubstitution]s.
+  Select.from(String mainArgument, List clauses, Message parent)
+      : super.from(mainArgument, clauses, parent);
+
+  Map<String, Message> cases = new Map<String, Message>();
+
+  String get icuMessageName => "select";
+  String get dartMessageName => 'Intl.select';
+
+  get attributeNames => cases.keys;
+  get codeAttributeNames => attributeNames;
+
+  void operator []=(String attributeName, rawValue) {
+    var value = Message.from(rawValue, this);
+    cases[attributeName] = value;
+  }
+
+  Message operator [](String attributeName) {
+    var exact = cases[attributeName];
+    return exact == null ? cases["other"] : exact;
+  }
+
+  /// Return the arguments that we care about for the select. In this
+  /// case they will all be passed in as a Map rather than as the named
+  /// arguments used in Plural/Gender.
+  Map argumentsOfInterestFor(MethodInvocation node) {
+    MapLiteral casesArgument = node.argumentList.arguments[1];
+    return new Map.fromIterable(casesArgument.entries,
+        key: (node) => node.key.value, value: (node) => node.value);
+  }
+
+  /// Write out the generated representation of this message. This differs
+  /// from Plural/Gender in that it prints a literal map rather than
+  /// named arguments.
+  String toCode() {
+    var out = new StringBuffer();
+    out.write('\${');
+    out.write(dartMessageName);
+    out.write('(');
+    out.write(mainArgument);
+    var args = codeAttributeNames;
+    out.write(", {");
+    args.fold(out,
+        (buffer, arg) => buffer..write("'$arg': '${this[arg].toCode()}', "));
+    out.write("})}");
+    return out.toString();
+  }
+}
diff --git a/lib/src/message_rewriter.dart b/lib/src/message_rewriter.dart
new file mode 100644
index 0000000..0d512bd
--- /dev/null
+++ b/lib/src/message_rewriter.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Code to rewrite Intl.message calls adding the name and args parameters
+/// automatically, primarily used by the transformer.
+import 'package:analyzer/analyzer.dart';
+
+import 'package:intl/extract_messages.dart';
+
+/// Rewrite all Intl.message/plural/etc. calls in [source], adding "name"
+/// and "args" parameters if they are not provided.
+///
+/// Return the modified source code. If there are errors parsing, list
+/// [sourceName] in the error message.
+String rewriteMessages(String source, String sourceName) {
+  var messages = findMessages(source, sourceName);
+  messages.sort((a, b) => a.sourcePosition.compareTo(b.sourcePosition));
+
+  var start = 0;
+  var newSource = new StringBuffer();
+  for (var message in messages) {
+    if (message.arguments.isNotEmpty) {
+      newSource.write(source.substring(start, message.sourcePosition));
+    // TODO(alanknight): We could generate more efficient code than the
+    // original here, dispatching more directly to the MessageLookup.
+    newSource.write(message.toOriginalCode());
+    start = message.endPosition;
+    }
+  }
+  newSource.write(source.substring(start));
+  return newSource.toString();
+}
+
+/// Find all the messages in the [source] text.
+///
+/// Report errors as coming from [sourceName]
+List findMessages(String source, String sourceName) {
+  var extraction = new MessageExtraction();
+  try {
+    extraction.root = parseCompilationUnit(source, name: sourceName);
+  } on AnalyzerErrorGroup catch (e) {
+    extraction.onMessage("Error in parsing $sourceName, no messages extracted.");
+    extraction.onMessage("  $e");
+    return [];
+  }
+  extraction.origin = sourceName;
+  var visitor = new MessageFindingVisitor(extraction);
+  visitor.generateNameAndArgs = true;
+  extraction.root.accept(visitor);
+  return visitor.messages.values.toList();
+}
diff --git a/lib/transformer.dart b/lib/transformer.dart
index 4031f0a..38dc8d0 100644
--- a/lib/transformer.dart
+++ b/lib/transformer.dart
@@ -1,12 +1,26 @@
-/// This is a redirector so that people can continue, in google3, to depend on
-/// the transformer as -intl, not needing to change it to intl_translation.
-//
-// Note that this is not exported into the opensource version, and would not
-// work there, since it depends on a library not in ourpubspec. It is a complete
-// hack and google3 specific. Fortunately, transformers in general are
-// deprecated so it should go away soon.
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
 
-// TODO(alanknight): Remove this.
-library transformer_forwarder;
+/// A transformer for Intl messages, supplying the name and arguments
+/// automatically.
+library intl_transformer;
 
-export 'package:intl_translation/transformer.dart';
\ No newline at end of file
+import 'package:barback/barback.dart';
+
+import 'src/message_rewriter.dart';
+
+/// Rewrites Intl.message calls to automatically insert the name and args
+/// parameters.
+class IntlMessageTransformer extends Transformer {
+  IntlMessageTransformer.asPlugin();
+
+  String get allowedExtensions => ".dart";
+
+  apply(Transform transform) async {
+    var content = await transform.primaryInput.readAsString();
+    var id = transform.primaryInput.id;
+    var newContent = rewriteMessages(content, '$id');
+    transform.addOutput(new Asset.fromString(id, newContent));
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 0cff830..d0f4687 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: intl
-version: 0.14.0
+version: 0.13.1
 author: Dart Team <misc@dartlang.org>
 description: Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
 homepage: https://github.com/dart-lang/intl
@@ -7,6 +7,11 @@
   sdk: '>=1.12.0 <2.0.0'
 documentation: http://www.dartdocs.org/documentation/intl/latest
 dependencies:
+  analyzer: '>=0.13.2 <0.29.0'
+  args: '>=0.12.1 <0.14.0'
+  path: '>=0.9.0 <2.0.0'
+  petitparser: '>=1.1.3 <2.0.0'
+  barback: ^0.15.2
 dev_dependencies:
   fixnum: '>=0.9.0 <0.11.0'
   unittest: '>=0.10.0 <0.12.0'
@@ -16,4 +21,15 @@
     - test/date_time_format_file_even_test.dart
     - test/date_time_format_file_odd_test.dart
     - test/find_default_locale_standalone_test.dart
+    - test/message_extraction/embedded_plural_text_after_test.dart
+    - test/message_extraction/embedded_plural_text_before_test.dart
+    - test/message_extraction/examples_parsing_test.dart
+    - test/message_extraction/failed_extraction_test.dart
+    - test/message_extraction/make_hardcoded_translation.dart
+    - test/message_extraction/message_extraction_no_deferred_test.dart
+    - test/message_extraction/message_extraction_test.dart
+    - test/message_extraction/really_fail_extraction_test.dart
     - test/intl_message_basic_example_test.dart # invalid import under pub's package-layout
+- intl:
+    $include:
+    - test/messages_with_transformer/transformer_test.dart
diff --git a/test/message_extraction/debug.sh b/test/message_extraction/debug.sh
new file mode 100755
index 0000000..82c8b87
--- /dev/null
+++ b/test/message_extraction/debug.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# The message_extraction_test.dart test uses a temporary directory and spawns 
+# separate processes for each step. This can make it very painful to debug the
+# steps. 
+# This script runs the steps individually, putting the files in the current
+# directory. You can run the script to run the test locally, or use this to
+# run individual steps or create them as launches in the editor.
+dart ../../bin/extract_to_arb.dart sample_with_messages.dart \
+part_of_sample_with_messages.dart
+dart make_hardcoded_translation.dart intl_messages.arb
+dart ../../bin/generate_from_arb.dart --generated-file-prefix=foo_ \
+sample_with_messages.dart part_of_sample_with_messages.dart \
+translation_fr.arb translation_de_DE.arb
diff --git a/test/message_extraction/embedded_plural_text_after.dart b/test/message_extraction/embedded_plural_text_after.dart
new file mode 100644
index 0000000..cda5bae
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_after.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A test library that should fail because there is a plural with text
+/// following the plural expression.
+library embedded_plural_text_after;
+
+import "package:intl/intl.dart";
+
+embeddedPlural2(n) => Intl.message(
+    "${Intl.plural(n, zero: 'none', one: 'one', other: 'some')} plus text.",
+    name: 'embeddedPlural2', desc: 'An embedded plural', args: [n]);
diff --git a/test/message_extraction/embedded_plural_text_after_test.dart b/test/message_extraction/embedded_plural_text_after_test.dart
new file mode 100644
index 0000000..ed9c16f
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_after_test.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library embedded_plural_text_after_test;
+
+import "failed_extraction_test.dart";
+import "package:unittest/unittest.dart";
+
+main() {
+  test("Expect failure because of embedded plural with text after it", () {
+    List<String> specialFiles = ['embedded_plural_text_after.dart'];
+    runTestWithWarnings(
+        warningsAreErrors: true,
+        expectedExitCode: 1,
+        embeddedPlurals: false,
+        sourceFiles: specialFiles);
+  });
+}
diff --git a/test/message_extraction/embedded_plural_text_before.dart b/test/message_extraction/embedded_plural_text_before.dart
new file mode 100644
index 0000000..4843831
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_before.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A test library that should fail because there is a plural with text
+/// before the plural expression.
+library embedded_plural_text_before;
+
+import "package:intl/intl.dart";
+
+embeddedPlural(n) => Intl.message(
+    "There are ${Intl.plural(n, zero: 'nothing', one: 'one', other: 'some')}.",
+    name: 'embeddedPlural', desc: 'An embedded plural', args: [n]);
diff --git a/test/message_extraction/embedded_plural_text_before_test.dart b/test/message_extraction/embedded_plural_text_before_test.dart
new file mode 100644
index 0000000..75b411c
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_before_test.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library embedded_plural_text_before_test;
+
+import "failed_extraction_test.dart";
+import "package:unittest/unittest.dart";
+
+main() {
+  test("Expect failure because of embedded plural with text before it", () {
+    List<String> files = ['embedded_plural_text_before.dart'];
+    runTestWithWarnings(
+        warningsAreErrors: true,
+        expectedExitCode: 1,
+        embeddedPlurals: false,
+        sourceFiles: files);
+  });
+}
diff --git a/test/message_extraction/examples_parsing_test.dart b/test/message_extraction/examples_parsing_test.dart
new file mode 100644
index 0000000..f61add1
--- /dev/null
+++ b/test/message_extraction/examples_parsing_test.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Test for parsing the examples argument from an Intl.message call. Very
+/// minimal so far.
+import 'package:unittest/unittest.dart';
+import 'package:intl/extract_messages.dart';
+import '../data_directory.dart';
+import 'package:path/path.dart' as path;
+import 'dart:io';
+
+main() {
+  test("Message examples are correctly extracted", () {
+    var file = path.join(intlDirectory, 'test', 'message_extraction',
+        'sample_with_messages.dart');
+    var extraction = new MessageExtraction();
+    var messages = extraction.parseFile(new File(file));
+    expect(messages['message2'].examples, {"x": 3});
+  });
+}
diff --git a/test/message_extraction/failed_extraction_test.dart b/test/message_extraction/failed_extraction_test.dart
new file mode 100644
index 0000000..b2758a7
--- /dev/null
+++ b/test/message_extraction/failed_extraction_test.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library failed_extraction_test;
+
+import "message_extraction_test.dart";
+import "dart:io";
+import "package:unittest/unittest.dart";
+
+main() {
+  test("Expect warnings but successful extraction", () {
+    runTestWithWarnings(warningsAreErrors: false, expectedExitCode: 0);
+  });
+}
+
+const List<String> defaultFiles = const [
+  "sample_with_messages.dart",
+  "part_of_sample_with_messages.dart"
+];
+
+void runTestWithWarnings({bool warningsAreErrors, int expectedExitCode,
+    bool embeddedPlurals: true, List<String> sourceFiles: defaultFiles}) {
+  verify(ProcessResult result) {
+    try {
+      expect(result.exitCode, expectedExitCode);
+    } finally {
+      deleteGeneratedFiles();
+    }
+  }
+
+  copyFilesToTempDirectory();
+  var program = asTestDirPath("../../bin/extract_to_arb.dart");
+  List<String> args = ["--output-dir=$tempDir"];
+  if (warningsAreErrors) {
+    args.add('--warnings-are-errors');
+  }
+  if (!embeddedPlurals) {
+    args.add('--no-embedded-plurals');
+  }
+  var files = sourceFiles.map(asTempDirPath).toList();
+  List<String> allArgs = [program]
+    ..addAll(args)
+    ..addAll(files);
+  var callback = expectAsync(verify) as ThenArgument;
+
+  run(null, allArgs).then(callback);
+}
+
+typedef dynamic ThenArgument(ProcessResult _);
diff --git a/test/message_extraction/foo_messages_all.dart b/test/message_extraction/foo_messages_all.dart
new file mode 100644
index 0000000..06b482e
--- /dev/null
+++ b/test/message_extraction/foo_messages_all.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library keep_the_static_analysis_from_complaining;
+
+initializeMessages(_) => throw new UnimplementedError(
+    "This entire file is only here to make the static"
+    " analysis happy. It will be generated during actual tests.");
diff --git a/test/message_extraction/make_hardcoded_translation.dart b/test/message_extraction/make_hardcoded_translation.dart
new file mode 100644
index 0000000..0c3af44
--- /dev/null
+++ b/test/message_extraction/make_hardcoded_translation.dart
@@ -0,0 +1,174 @@
+#!/usr/bin/env dart
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This simulates a translation process, reading the messages generated from
+/// extract_message.dart for the files sample_with_messages.dart and
+/// part_of_sample_with_messages.dart and writing out hard-coded translations
+/// for German and French locales.
+
+import 'dart:convert';
+import 'dart:io';
+import 'package:path/path.dart' as path;
+import 'package:args/args.dart';
+
+/// A list of the French translations that we will produce.
+var french = {
+  "types": r"{a}, {b}, {c}",
+  "This string extends across multiple lines.":
+      "Cette message prend plusiers lignes.",
+  "message2": r"Un autre message avec un seul paramètre {x}",
+  "alwaysTranslated": "Cette chaîne est toujours traduit",
+  "message1": "Il s'agit d'un message",
+  "\"So-called\"": "\"Soi-disant\"",
+  "trickyInterpolation": r"L'interpolation est délicate "
+      r"quand elle se termine une phrase comme {s}.",
+  "message3": "Caractères qui doivent être échapper, par exemple barres \\ "
+      "dollars \${ (les accolades sont ok), et xml/html réservés <& et "
+      "des citations \" "
+      "avec quelques paramètres ainsi {a}, {b}, et {c}",
+  "YouveGotMessages_method": "Cela vient d'une méthode",
+  "nonLambda": "Cette méthode n'est pas un lambda",
+  "staticMessage": "Cela vient d'une méthode statique",
+  "notAlwaysTranslated": "Ce manque certaines traductions",
+  "thisNameIsNotInTheOriginal": "Could this lead to something malicious?",
+  "Ancient Greek hangman characters: 𐅆𐅇.":
+      "Anciens caractères grecs jeux du pendu: 𐅆𐅇.",
+  "escapable": "Escapes: \n\r\f\b\t\v.",
+  "sameContentsDifferentName": "Bonjour tout le monde",
+  "differentNameSameContents": "Bonjour tout le monde",
+  "rentToBePaid": "loyer",
+  "rentAsVerb": "louer",
+  "plurals": "{num,plural, =0{Est-ce que nulle est pluriel?}=1{C'est singulier}"
+      "other{C'est pluriel ({num}).}}",
+  "whereTheyWentMessage": "{gender,select, male{{name} est allé à sa {place}}"
+      "female{{name} est allée à sa {place}}other{{name}"
+      " est allé à sa {place}}}",
+  // Gratuitously different translation for testing. Ignoring gender of place.
+  "nestedMessage": "{combinedGender,select, "
+      "other{"
+      "{number,plural, "
+      "=0{Personne n'avait allé à la {place}}"
+      "=1{{names} était allé à la {place}}"
+      "other{{names} étaient allés à la {place}}"
+      "}"
+      "}"
+      "female{"
+      "{number,plural, "
+      "=1{{names} était allée à la {place}}"
+      "other{{names} étaient allées à la {place}}"
+      "}"
+      "}"
+      "}",
+  "outerPlural": "{n,plural, =0{rien}=1{un}other{quelques-uns}}",
+  "outerGender": "{g,select, male {homme} female {femme} other {autre}}",
+  "pluralThatFailsParsing": "{noOfThings,plural, "
+      "=1{1 chose:}other{{noOfThings} choses:}}",
+  "nestedOuter": "{number,plural, other{"
+      "{gen,select, male{{number} homme}other{{number} autre}}}}",
+  "outerSelect": "{currency,select, CDN{{amount} dollars Canadiens}"
+      "other{{amount} certaine devise ou autre.}}}",
+  "nestedSelect": "{currency,select, CDN{{amount,plural, "
+      "=1{{amount} dollar Canadien}"
+      "other{{amount} dollars Canadiens}}}"
+      "other{N'importe quoi}"
+      "}}",
+  "literalDollar": "Cinq sous est US\$0.05",
+  r"'<>{}= +-_$()&^%$#@!~`'": r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'"
+};
+
+/// A list of the German translations that we will produce.
+var german = {
+  "types": r"{a}, {b}, {c}",
+  "This string extends across multiple lines.":
+      "Dieser String erstreckt sich über mehrere Zeilen erstrecken.",
+  "message2": r"Eine weitere Meldung mit dem Parameter {x}",
+  "alwaysTranslated": "Diese Zeichenkette wird immer übersetzt",
+  "message1": "Dies ist eine Nachricht",
+  "\"So-called\"": "\"Sogenannt\"",
+  "trickyInterpolation": r"Interpolation ist schwierig, wenn es einen Satz "
+      "wie dieser endet {s}.",
+  "message3": "Zeichen, die Flucht benötigen, zB Schrägstriche \\ Dollar "
+      "\${ (geschweiften Klammern sind ok) und xml reservierte Zeichen <& und "
+      "Zitate \" Parameter {a}, {b} und {c}",
+  "YouveGotMessages_method": "Dies ergibt sich aus einer Methode",
+  "nonLambda": "Diese Methode ist nicht eine Lambda",
+  "staticMessage": "Dies ergibt sich aus einer statischen Methode",
+  "thisNameIsNotInTheOriginal": "Could this lead to something malicious?",
+  "Ancient Greek hangman characters: 𐅆𐅇.":
+      "Antike griechische Galgenmännchen Zeichen: 𐅆𐅇",
+  "escapable": "Escapes: \n\r\f\b\t\v.",
+  "sameContentsDifferentName": "Hallo Welt",
+  "differentNameSameContents": "Hallo Welt",
+  "rentToBePaid": "Miete",
+  "rentAsVerb": "mieten",
+  "plurals": "{num,plural, =0{Ist Null Plural?}=1{Dies ist einmalig}"
+      "other{Dies ist Plural ({num}).}}",
+  "whereTheyWentMessage": "{gender,select, male{{name} ging zu seinem {place}}"
+      "female{{name} ging zu ihrem {place}}other{{name} ging zu seinem {place}}}",
+  //Note that we're only using the gender of the people. The gender of the
+  //place also matters, but we're not dealing with that here.
+  "nestedMessage": "{combinedGender,select, "
+      "other{"
+      "{number,plural, "
+      "=0{Niemand ging zu {place}}"
+      "=1{{names} ging zum {place}}"
+      "other{{names} gingen zum {place}}"
+      "}"
+      "}"
+      "female{"
+      "{number,plural, "
+      "=1{{names} ging in dem {place}}"
+      "other{{names} gingen zum {place}}"
+      "}"
+      "}"
+      "}",
+  "outerPlural": "{n,plural, =0{Null}=1{ein}other{einige}}",
+  "outerGender": "{g,select, male{Mann}female{Frau}other{andere}}",
+  "pluralThatFailsParsing": "{noOfThings,plural, "
+      "=1{eins:}other{{noOfThings} Dinge:}}",
+  "nestedOuter": "{number,plural, other{"
+      "{gen,select, male{{number} Mann}other{{number} andere}}}}",
+  "outerSelect": "{currency,select, CDN{{amount} Kanadischen dollar}"
+      "other{{amount} einige Währung oder anderen.}}}",
+  "nestedSelect": "{currency,select, CDN{{amount,plural, "
+      "=1{{amount} Kanadischer dollar}"
+      "other{{amount} Kanadischen dollar}}}"
+      "other{whatever}"
+      "}",
+  "literalDollar": "Fünf Cent US \$ 0.05",
+  r"'<>{}= +-_$()&^%$#@!~`'": r"interessant (de): '<>{}= +-_$()&^%$#@!~`'"
+};
+
+/// The output directory for translated files.
+String targetDir;
+
+/// Generate a translated json version from [originals] in [locale] looking
+/// up the translations in [translations].
+void translate(Map originals, String locale, Map translations) {
+  var translated = {"_locale": locale};
+  originals.forEach((name, text) {
+    translated[name] = translations[name];
+  });
+  var file = new File(path.join(targetDir, 'translation_$locale.arb'));
+  file.writeAsStringSync(JSON.encode(translated));
+}
+
+main(List<String> args) {
+  if (args.length == 0) {
+    print('Usage: make_hardcoded_translation [--output-dir=<dir>] '
+        '[originalFile.arb]');
+    exit(0);
+  }
+  var parser = new ArgParser();
+  parser.addOption("output-dir",
+      defaultsTo: '.', callback: (value) => targetDir = value);
+  parser.parse(args);
+
+  var fileArgs = args.where((x) => x.contains('.arb'));
+
+  var messages = JSON.decode(new File(fileArgs.first).readAsStringSync());
+  translate(messages, "fr", french);
+  translate(messages, "de_DE", german);
+}
diff --git a/test/message_extraction/message_extraction_no_deferred_test.dart b/test/message_extraction/message_extraction_no_deferred_test.dart
new file mode 100644
index 0000000..68936f1
--- /dev/null
+++ b/test/message_extraction/message_extraction_no_deferred_test.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+
+/// A test for message extraction and code generation not using deferred
+/// loading for the generated code.
+library message_extraction_no_deferred_test;
+
+import 'message_extraction_test.dart' as mainTest;
+
+main(arguments) {
+  mainTest.useDeferredLoading = false;
+  mainTest.main(arguments);
+}
diff --git a/test/message_extraction/message_extraction_test.dart b/test/message_extraction/message_extraction_test.dart
new file mode 100644
index 0000000..d22e1e3
--- /dev/null
+++ b/test/message_extraction/message_extraction_test.dart
@@ -0,0 +1,167 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library message_extraction_test;
+
+import 'package:unittest/unittest.dart';
+import 'dart:io';
+import 'dart:async';
+import 'dart:convert';
+import 'package:path/path.dart' as path;
+import '../data_directory.dart';
+
+final dart = Platform.executable;
+
+/// Should we use deferred loading.
+bool useDeferredLoading = true;
+
+String get _deferredLoadPrefix => useDeferredLoading ? '' : 'no-';
+
+String get deferredLoadArg => '--${_deferredLoadPrefix}use-deferred-loading';
+
+/// The VM arguments we were given, most important package-root.
+final vmArgs = Platform.executableArguments;
+
+/// For testing we move the files into a temporary directory so as not to leave
+/// generated files around after a failed test. For debugging, we omit that
+/// step if [useLocalDirectory] is true. The place we move them to is saved as
+/// [tempDir].
+String get tempDir => _tempDir == null ? _tempDir = _createTempDir() : _tempDir;
+var _tempDir;
+_createTempDir() => useLocalDirectory
+    ? '.'
+    : Directory.systemTemp.createTempSync('message_extraction_test').path;
+
+var useLocalDirectory = false;
+
+/// Translate a relative file path into this test directory. This is
+/// applied to all the arguments of [run]. It will ignore a string that
+/// is an absolute path or begins with "--", because some of the arguments
+/// might be command-line options.
+String asTestDirPath([String s]) {
+  if (s == null || s.startsWith("--") || path.isAbsolute(s)) return s;
+  return path.join(intlDirectory, 'test', 'message_extraction', s);
+}
+
+/// Translate a relative file path into our temp directory. This is
+/// applied to all the arguments of [run]. It will ignore a string that
+/// is an absolute path or begins with "--", because some of the arguments
+/// might be command-line options.
+String asTempDirPath([String s]) {
+  if (s == null || s.startsWith("--") || path.isAbsolute(s)) return s;
+  return path.join(tempDir, s);
+}
+
+typedef ProcessResult ThenResult(ProcessResult _);
+main(arguments) {
+  // If debugging, use --local to avoid copying everything to temporary
+  // directories to make it even harder to debug. Note that this will also
+  // not delete the generated files, so may require manual cleanup.
+  if (arguments.contains("--local")) {
+    print("Testing using local directory for generated files");
+    useLocalDirectory = true;
+  }
+  setUp(copyFilesToTempDirectory);
+  tearDown(deleteGeneratedFiles);
+  test("Test round trip message extraction, translation, code generation, "
+      "and printing", () {
+    var makeSureWeVerify = expectAsync(runAndVerify) as ThenResult;
+    return extractMessages(null).then((result) {
+      return generateTranslationFiles(result);
+    }).then((result) {
+      return generateCodeFromTranslation(result);
+    }).then(makeSureWeVerify).then(checkResult);
+  });
+}
+
+void copyFilesToTempDirectory() {
+  if (useLocalDirectory) return;
+  var files = [
+    asTestDirPath('sample_with_messages.dart'),
+    asTestDirPath('part_of_sample_with_messages.dart'),
+    asTestDirPath('verify_messages.dart'),
+    asTestDirPath('run_and_verify.dart'),
+    asTestDirPath('embedded_plural_text_before.dart'),
+    asTestDirPath('embedded_plural_text_after.dart'),
+    asTestDirPath('print_to_list.dart')
+  ];
+  for (var filename in files) {
+    var file = new File(filename);
+    file.copySync(path.join(tempDir, path.basename(filename)));
+  }
+}
+
+void deleteGeneratedFiles() {
+  if (useLocalDirectory) return;
+  try {
+    new Directory(tempDir).deleteSync(recursive: true);
+  } on Error catch (e) {
+    print("Failed to delete $tempDir");
+    print("Exception:\n$e");
+  }
+}
+
+/// Run the process with the given list of filenames, which we assume
+/// are in dir() and need to be qualified in case that's not our working
+/// directory.
+Future<ProcessResult> run(
+    ProcessResult previousResult, List<String> filenames) {
+  // If there's a failure in one of the sub-programs, print its output.
+  checkResult(previousResult);
+  var filesInTheRightDirectory = filenames
+      .map((x) => asTempDirPath(x))
+      .map((x) => path.normalize(x))
+      .toList();
+  // Inject the script argument --output-dir in between the script and its
+  // arguments.
+  List<String> args = []
+    ..addAll(vmArgs)
+    ..add(filesInTheRightDirectory.first)
+    ..addAll(["--output-dir=$tempDir"])
+    ..addAll(filesInTheRightDirectory.skip(1));
+  var result =
+      Process.run(dart, args, stdoutEncoding: UTF8, stderrEncoding: UTF8);
+  return result;
+}
+
+checkResult(ProcessResult previousResult) {
+  if (previousResult != null) {
+    if (previousResult.exitCode != 0) {
+      print("Error running sub-program:");
+    }
+    print(previousResult.stdout);
+    print(previousResult.stderr);
+    print("exitCode=${previousResult.exitCode}");
+    // Fail the test.
+    expect(previousResult.exitCode, 0);
+  }
+}
+
+Future<ProcessResult> extractMessages(ProcessResult previousResult) => run(
+    previousResult, [
+  asTestDirPath('../../bin/extract_to_arb.dart'),
+  '--suppress-warnings',
+  'sample_with_messages.dart',
+  'part_of_sample_with_messages.dart'
+]);
+
+Future<ProcessResult> generateTranslationFiles(ProcessResult previousResult) =>
+    run(previousResult, [
+  asTestDirPath('make_hardcoded_translation.dart'),
+  'intl_messages.arb'
+]);
+
+Future<ProcessResult> generateCodeFromTranslation(
+    ProcessResult previousResult) => run(previousResult, [
+  asTestDirPath('../../bin/generate_from_arb.dart'),
+  deferredLoadArg,
+  '--generated-file-prefix=foo_',
+  'sample_with_messages.dart',
+  'part_of_sample_with_messages.dart',
+  'translation_fr.arb',
+  'translation_de_DE.arb'
+]);
+
+Future<ProcessResult> runAndVerify(ProcessResult previousResult) =>
+    run(previousResult, [asTempDirPath('run_and_verify.dart')]);
diff --git a/test/message_extraction/part_of_sample_with_messages.dart b/test/message_extraction/part_of_sample_with_messages.dart
new file mode 100644
index 0000000..31cddcc
--- /dev/null
+++ b/test/message_extraction/part_of_sample_with_messages.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.part of sample;
+
+part of sample;
+
+class Person {
+  String name;
+  String gender;
+  Person(this.name, this.gender);
+}
+
+class YouveGotMessages {
+
+  // A static message, rather than a standalone function.
+  static staticMessage() =>
+      Intl.message("This comes from a static method", name: 'staticMessage');
+
+  // An instance method, rather than a standalone function.
+  method() => Intl.message("This comes from a method",
+      name: 'YouveGotMessages_method', desc: 'This is a method with a '
+      'long description which spans '
+      'multiple lines.');
+
+  // A non-lambda, i.e. not using => syntax, and with an additional statement
+  // before the Intl.message call.
+  nonLambda() {
+    var aTrueValue = true;
+    var msg = Intl.message("This method is not a lambda", name: 'nonLambda');
+    expect(aTrueValue, isTrue,
+        reason: 'Parser should not fail with additional code.');
+    return msg;
+  }
+
+  plurals(num) => Intl.message("""${Intl.plural(num,
+         zero : 'Is zero plural?',
+         one : 'This is singular.',
+         other : 'This is plural ($num).')
+        }""", name: "plurals", args: [num], desc: "Basic plurals");
+
+  whereTheyWent(Person person, String place) =>
+      whereTheyWentMessage(person.name, person.gender, place);
+
+  whereTheyWentMessage(String name, String gender, String place) {
+    return Intl.message("${Intl.gender(gender,
+            male: '$name went to his $place',
+            female: '$name went to her $place',
+            other: '$name went to its $place')
+        }",
+        name: "whereTheyWentMessage",
+        args: [name, gender, place],
+        desc: 'A person went to some place that they own, e.g. their room');
+  }
+
+  // English doesn't do enough with genders, so this example is French.
+  nested(List people, String place) {
+    var names = people.map((x) => x.name).join(", ");
+    var number = people.length;
+    var combinedGender =
+        people.every((x) => x.gender == "female") ? "female" : "other";
+    if (number == 0) combinedGender = "other";
+
+    nestedMessage(names, number, combinedGender, place) => Intl.message(
+        '''${Intl.gender(combinedGender,
+          other: '${Intl.plural(number,
+            zero: "Personne n'est allé au $place",
+            one: "${names} est allé au $place",
+            other: "${names} sont allés au $place")}',
+          female: '${Intl.plural(number,
+            one: "$names est allée au $place",
+          other: "$names sont allées au $place")}'
+        )}''',
+        name: "nestedMessage", args: [names, number, combinedGender, place]);
+    return nestedMessage(names, number, combinedGender, place);
+  }
+}
diff --git a/test/message_extraction/print_to_list.dart b/test/message_extraction/print_to_list.dart
new file mode 100644
index 0000000..f5446d4
--- /dev/null
+++ b/test/message_extraction/print_to_list.dart
@@ -0,0 +1,10 @@
+/// This provides a way for a test to print to an internal list so the
+/// results can be verified rather than writing to and reading a file.
+
+library print_to_list.dart;
+
+List<String> lines = [];
+
+void printOut(String s) {
+  lines.add(s);
+}
diff --git a/test/message_extraction/really_fail_extraction_test.dart b/test/message_extraction/really_fail_extraction_test.dart
new file mode 100644
index 0000000..76f5251
--- /dev/null
+++ b/test/message_extraction/really_fail_extraction_test.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library really_fail_extraction_test;
+
+import "failed_extraction_test.dart";
+import "package:unittest/unittest.dart";
+
+main() {
+  test("Expect failure because warnings are errors", () {
+    runTestWithWarnings(warningsAreErrors: true, expectedExitCode: 1);
+  });
+}
diff --git a/test/message_extraction/run_and_verify.dart b/test/message_extraction/run_and_verify.dart
new file mode 100644
index 0000000..ef9a1a0
--- /dev/null
+++ b/test/message_extraction/run_and_verify.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library verify_and_run;
+
+import 'sample_with_messages.dart' as sample;
+import 'verify_messages.dart';
+
+main() {
+  sample.main().then(verifyResult);
+}
diff --git a/test/message_extraction/sample_with_messages.dart b/test/message_extraction/sample_with_messages.dart
new file mode 100644
index 0000000..c22380f
--- /dev/null
+++ b/test/message_extraction/sample_with_messages.dart
@@ -0,0 +1,259 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This is a program with various [Intl.message] messages. It just prints
+/// all of them, and is used for testing of message extraction, translation,
+/// and code generation.
+library sample;
+
+import "package:intl/intl.dart";
+import "foo_messages_all.dart";
+import "print_to_list.dart";
+import "dart:async";
+import "package:unittest/unittest.dart";
+
+part 'part_of_sample_with_messages.dart';
+
+message1() => Intl.message("This is a message", name: 'message1', desc: 'foo');
+
+message2(x) => Intl.message("Another message with parameter $x",
+    name: 'mess' 'age2',
+    desc: 'Description ' '2',
+    args: [x],
+    examples: {'x': 3});
+
+// A string with multiple adjacent strings concatenated together, verify
+// that the parser handles this properly.
+multiLine() => Intl.message("This "
+    "string "
+    "extends "
+    "across "
+    "multiple "
+    "lines.");
+
+get interestingCharactersNoName =>
+    Intl.message("'<>{}= +-_\$()&^%\$#@!~`'", desc: "interesting characters");
+
+// Have types on the enclosing function's arguments.
+types(int a, String b, List c) =>
+    Intl.message("$a, $b, $c", name: 'types', args: [a, b, c]);
+
+// This string will be printed with a French locale, so it will always show
+// up in the French version, regardless of the current locale.
+alwaysTranslated() => Intl.message("This string is always translated",
+    locale: 'fr', name: 'alwaysTranslated');
+
+// Test interpolation with curly braces around the expression, but it must
+// still be just a variable reference.
+trickyInterpolation(s) =>
+    Intl.message("Interpolation is tricky when it ends a sentence like ${s}.",
+        name: 'trickyInterpolation', args: [s]);
+
+get leadingQuotes => Intl.message("\"So-called\"");
+
+// A message with characters not in the basic multilingual plane.
+originalNotInBMP() => Intl.message("Ancient Greek hangman characters: 𐅆𐅇.");
+
+// A string for which we don't provide all translations.
+notAlwaysTranslated() => Intl.message("This is missing some translations",
+    name: "notAlwaysTranslated");
+
+// This is invalid and should be recognized as such, because the message has
+// to be a literal. Otherwise, interpolations would be outside of the function
+// scope.
+var someString = "No, it has to be a literal string";
+noVariables() => Intl.message(someString, name: "noVariables");
+
+// This is unremarkable in English, but the translated versions will contain
+// characters that ought to be escaped during code generation.
+escapable() => Intl.message("Escapable characters here: ", name: "escapable");
+
+outerPlural(n) => Intl.plural(n,
+    zero: 'none',
+    one: 'one',
+    other: 'some',
+    name: 'outerPlural',
+    desc: 'A plural with no enclosing message',
+    args: [n]);
+
+outerGender(g) => Intl.gender(g,
+    male: 'm',
+    female: 'f',
+    other: 'o',
+    name: 'outerGender',
+    desc: 'A gender with no enclosing message',
+    args: [g]);
+
+pluralThatFailsParsing(noOfThings) => Intl.plural(noOfThings,
+    one: "1 thing:",
+    other: "$noOfThings things:",
+    name: "pluralThatFailsParsing",
+    args: [noOfThings],
+    desc: "How many things are there?");
+
+// A standalone gender message where we don't provide name or args. This should
+// be rejected by validation code.
+invalidOuterGender(g) => Intl.gender(g, other: 'o');
+
+// A general select
+outerSelect(currency, amount) => Intl.select(
+    currency,
+    {
+      "CDN": "$amount Canadian dollars",
+      "other": "$amount some currency or other."
+    },
+    name: "outerSelect",
+    args: [currency, amount]);
+
+// A select with a plural inside the expressions.
+nestedSelect(currency, amount) => Intl.select(
+    currency,
+    {
+      "CDN": """${Intl.plural(amount, one: '$amount Canadian dollar',
+          other: '$amount Canadian dollars')}""",
+      "other": "Whatever",
+    },
+    name: "nestedSelect",
+    args: [currency, amount]);
+
+// A trivial nested plural/gender where both are done directly rather than
+// in interpolations.
+nestedOuter(number, gen) => Intl.plural(number,
+    other: Intl.gender(gen, male: "$number male", other: "$number other"),
+    name: 'nestedOuter',
+    args: [number, gen]);
+
+sameContentsDifferentName() => Intl.message("Hello World",
+    name: "sameContentsDifferentName",
+    desc: "One of two messages with the same contents, but different names");
+
+differentNameSameContents() => Intl.message("Hello World",
+    name: "differentNameSameContents",
+    desc: "One of two messages with the same contents, but different names");
+
+/// Distinguish two messages with identical text using the meaning parameter.
+rentToBePaid() => Intl.message("rent",
+    name: "rentToBePaid",
+    meaning: 'Money for rent',
+    desc: "Money to be paid for rent");
+
+rentAsVerb() => Intl.message("rent",
+    name: "rentAsVerb",
+    meaning: 'rent as a verb',
+    desc: "The action of renting, as in rent a car");
+
+literalDollar() => Intl.message("Five cents is US\$0.05",
+    name: "literalDollar", desc: "Literal dollar sign with valid number");
+
+printStuff(Intl locale) {
+  // Use a name that's not a literal so this will get skipped. Then we have
+  // a name that's not in the original but we include it in the French
+  // translation. Because it's not in the original it shouldn't get included
+  // in the generated catalog and shouldn't get translated.
+  if (locale.locale == 'fr') {
+    var badName = "thisNameIsNotInTheOriginal";
+    var notInOriginal = Intl.message("foo", name: badName);
+    if (notInOriginal != "foo") {
+      throw "You shouldn't be able to introduce a new message in a translation";
+    }
+  }
+
+  // A function that is assigned to a variable. It's also nested
+  // within another function definition.
+  message3(a, b, c) => Intl.message(
+      "Characters that need escaping, e.g slashes \\ dollars \${ (curly braces "
+      "are ok) and xml reserved characters <& and quotes \" "
+      "parameters $a, $b, and $c",
+      name: 'message3',
+      args: [a, b, c]);
+  var messageVariable = message3;
+
+  printOut("-------------------------------------------");
+  printOut("Printing messages for ${locale.locale}");
+  Intl.withLocale(locale.locale, () {
+    printOut(message1());
+    printOut(message2("hello"));
+    printOut(messageVariable(1, 2, 3));
+    printOut(multiLine());
+    printOut(types(1, "b", ["c", "d"]));
+    printOut(leadingQuotes);
+    printOut(alwaysTranslated());
+    printOut(trickyInterpolation("this"));
+    var thing = new YouveGotMessages();
+    printOut(thing.method());
+    printOut(thing.nonLambda());
+    printOut(YouveGotMessages.staticMessage());
+    printOut(notAlwaysTranslated());
+    printOut(originalNotInBMP());
+    printOut(escapable());
+
+    printOut(thing.plurals(0));
+    printOut(thing.plurals(1));
+    printOut(thing.plurals(2));
+    printOut(thing.plurals(3));
+    printOut(thing.plurals(4));
+    printOut(thing.plurals(5));
+    printOut(thing.plurals(6));
+    printOut(thing.plurals(7));
+    printOut(thing.plurals(8));
+    printOut(thing.plurals(9));
+    printOut(thing.plurals(10));
+    printOut(thing.plurals(11));
+    printOut(thing.plurals(20));
+    printOut(thing.plurals(100));
+    printOut(thing.plurals(101));
+    printOut(thing.plurals(100000));
+    var alice = new Person("Alice", "female");
+    var bob = new Person("Bob", "male");
+    var cat = new Person("cat", null);
+    printOut(thing.whereTheyWent(alice, "house"));
+    printOut(thing.whereTheyWent(bob, "house"));
+    printOut(thing.whereTheyWent(cat, "litter box"));
+    printOut(thing.nested([alice, bob], "magasin"));
+    printOut(thing.nested([alice], "magasin"));
+    printOut(thing.nested([], "magasin"));
+    printOut(thing.nested([bob, bob], "magasin"));
+    printOut(thing.nested([alice, alice], "magasin"));
+
+    printOut(outerPlural(0));
+    printOut(outerPlural(1));
+    printOut(outerGender("male"));
+    printOut(outerGender("female"));
+    printOut(nestedOuter(7, "male"));
+    printOut(outerSelect("CDN", 7));
+    printOut(outerSelect("EUR", 5));
+    printOut(nestedSelect("CDN", 1));
+    printOut(nestedSelect("CDN", 2));
+    printOut(pluralThatFailsParsing(1));
+    printOut(pluralThatFailsParsing(2));
+    printOut(differentNameSameContents());
+    printOut(sameContentsDifferentName());
+    printOut(rentAsVerb());
+    printOut(rentToBePaid());
+    printOut(literalDollar());
+    printOut(interestingCharactersNoName);
+  });
+}
+
+var localeToUse = 'en_US';
+
+main() {
+  var fr = new Intl("fr");
+  var english = new Intl("en_US");
+  var de = new Intl("de_DE");
+  // Throw in an initialize of a null locale to make sure it doesn't throw.
+  initializeMessages(null);
+
+  // Verify that a translated message isn't initially present.
+  var messageInGerman = Intl.withLocale('de_DE', message1);
+  expect(messageInGerman, "This is a message");
+
+  var f1 = initializeMessages(fr.locale)
+      // Since English has the one message which is always translated, we
+      // can't print it until French is ready.
+      .then((_) => printStuff(english))
+      .then((_) => printStuff(fr));
+  var f2 = initializeMessages('de-de').then((_) => printStuff(de));
+  return Future.wait([f1, f2]);
+}
diff --git a/test/message_extraction/verify_messages.dart b/test/message_extraction/verify_messages.dart
new file mode 100644
index 0000000..b68abcd
--- /dev/null
+++ b/test/message_extraction/verify_messages.dart
@@ -0,0 +1,213 @@
+library verify_messages;
+
+import "print_to_list.dart";
+import "package:unittest/unittest.dart";
+
+verifyResult(ignored) {
+  test("Verify message translation output", actuallyVerifyResult);
+}
+
+actuallyVerifyResult() {
+  var lineIterator;
+  verify(String s) {
+    lineIterator.moveNext();
+    var value = lineIterator.current;
+    expect(value, s);
+  }
+
+  var expanded = lines.expand((line) => line.split("\n")).toList();
+  lineIterator = expanded.iterator..moveNext();
+  verify("Printing messages for en_US");
+  verify("This is a message");
+  verify("Another message with parameter hello");
+  verify("Characters that need escaping, e.g slashes \\ dollars \${ "
+      "(curly braces are ok) and xml reserved characters <& and "
+      "quotes \" parameters 1, 2, and 3");
+  verify("This string extends across multiple lines.");
+  verify("1, b, [c, d]");
+  verify('"So-called"');
+  verify("Cette chaîne est toujours traduit");
+  verify("Interpolation is tricky when it ends a sentence like this.");
+  verify("This comes from a method");
+  verify("This method is not a lambda");
+  verify("This comes from a static method");
+  verify("This is missing some translations");
+  verify("Ancient Greek hangman characters: 𐅆𐅇.");
+  verify("Escapable characters here: ");
+
+  verify('Is zero plural?');
+  verify('This is singular.');
+  verify('This is plural (2).');
+  verify('This is plural (3).');
+  verify('This is plural (4).');
+  verify('This is plural (5).');
+  verify('This is plural (6).');
+  verify('This is plural (7).');
+  verify('This is plural (8).');
+  verify('This is plural (9).');
+  verify('This is plural (10).');
+  verify('This is plural (11).');
+  verify('This is plural (20).');
+  verify('This is plural (100).');
+  verify('This is plural (101).');
+  verify('This is plural (100000).');
+  verify('Alice went to her house');
+  verify('Bob went to his house');
+  verify('cat went to its litter box');
+  verify('Alice, Bob sont allés au magasin');
+  verify('Alice est allée au magasin');
+  verify('Personne n\'est allé au magasin');
+  verify('Bob, Bob sont allés au magasin');
+  verify('Alice, Alice sont allées au magasin');
+  verify('none');
+  verify('one');
+  verify('m');
+  verify('f');
+  verify('7 male');
+  verify('7 Canadian dollars');
+  verify('5 some currency or other.');
+  verify('1 Canadian dollar');
+  verify('2 Canadian dollars');
+  verify('1 thing:');
+  verify('2 things:');
+  verify('Hello World');
+  verify('Hello World');
+  verify('rent');
+  verify('rent');
+  verify('Five cents is US\$0.05');
+  verify(r"'<>{}= +-_$()&^%$#@!~`'");
+
+  var fr_lines =
+      expanded.skip(1).skipWhile((line) => !line.contains('----')).toList();
+  lineIterator = fr_lines.iterator..moveNext();
+  verify("Printing messages for fr");
+  verify("Il s'agit d'un message");
+  verify("Un autre message avec un seul paramètre hello");
+  verify("Caractères qui doivent être échapper, par exemple barres \\ "
+      "dollars \${ (les accolades sont ok), et xml/html réservés <& et "
+      "des citations \" "
+      "avec quelques paramètres ainsi 1, 2, et 3");
+  verify("Cette message prend plusiers lignes.");
+  verify("1, b, [c, d]");
+  verify('"Soi-disant"');
+  verify("Cette chaîne est toujours traduit");
+  verify("L'interpolation est délicate quand elle se termine une "
+      "phrase comme this.");
+  verify("Cela vient d'une méthode");
+  verify("Cette méthode n'est pas un lambda");
+  verify("Cela vient d'une méthode statique");
+  verify("Ce manque certaines traductions");
+  verify("Anciens caractères grecs jeux du pendu: 𐅆𐅇.");
+  verify("Escapes: ");
+  verify("\r\f\b\t\v.");
+
+  verify('Est-ce que nulle est pluriel?');
+  verify('C\'est singulier');
+  verify('C\'est pluriel (2).');
+  verify('C\'est pluriel (3).');
+  verify('C\'est pluriel (4).');
+  verify('C\'est pluriel (5).');
+  verify('C\'est pluriel (6).');
+  verify('C\'est pluriel (7).');
+  verify('C\'est pluriel (8).');
+  verify('C\'est pluriel (9).');
+  verify('C\'est pluriel (10).');
+  verify('C\'est pluriel (11).');
+  verify('C\'est pluriel (20).');
+  verify('C\'est pluriel (100).');
+  verify('C\'est pluriel (101).');
+  verify('C\'est pluriel (100000).');
+  verify('Alice est allée à sa house');
+  verify('Bob est allé à sa house');
+  verify('cat est allé à sa litter box');
+  verify('Alice, Bob étaient allés à la magasin');
+  verify('Alice était allée à la magasin');
+  verify('Personne n\'avait allé à la magasin');
+  verify('Bob, Bob étaient allés à la magasin');
+  verify('Alice, Alice étaient allées à la magasin');
+  verify('rien');
+  verify('un');
+  verify('homme');
+  verify('femme');
+  verify('7 homme');
+  verify('7 dollars Canadiens');
+  verify('5 certaine devise ou autre.');
+  verify('1 dollar Canadien');
+  verify('2 dollars Canadiens');
+  verify('1 chose:');
+  verify('2 choses:');
+  verify('Bonjour tout le monde');
+  verify('Bonjour tout le monde');
+  verify('louer');
+  verify('loyer');
+  // Using a non-French format for the currency to test interpolation.
+  verify('Cinq sous est US\$0.05');
+  verify(r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'");
+
+  var de_lines =
+      fr_lines.skip(1).skipWhile((line) => !line.contains('----')).toList();
+  lineIterator = de_lines.iterator..moveNext();
+  verify("Printing messages for de_DE");
+  verify("Dies ist eine Nachricht");
+  verify("Eine weitere Meldung mit dem Parameter hello");
+  verify("Zeichen, die Flucht benötigen, zB Schrägstriche \\ Dollar "
+      "\${ (geschweiften Klammern sind ok) und xml reservierte Zeichen <& und "
+      "Zitate \" Parameter 1, 2 und 3");
+  verify("Dieser String erstreckt sich über mehrere "
+      "Zeilen erstrecken.");
+  verify("1, b, [c, d]");
+  verify('"Sogenannt"');
+  // This is correct, the message is forced to French, even in a German locale.
+  verify("Cette chaîne est toujours traduit");
+  verify(
+      "Interpolation ist schwierig, wenn es einen Satz wie dieser endet this.");
+  verify("Dies ergibt sich aus einer Methode");
+  verify("Diese Methode ist nicht eine Lambda");
+  verify("Dies ergibt sich aus einer statischen Methode");
+  verify("This is missing some translations");
+  verify("Antike griechische Galgenmännchen Zeichen: 𐅆𐅇");
+  verify("Escapes: ");
+  verify("\r\f\b\t\v.");
+
+  verify('Ist Null Plural?');
+  verify('Dies ist einmalig');
+  verify('Dies ist Plural (2).');
+  verify('Dies ist Plural (3).');
+  verify('Dies ist Plural (4).');
+  verify('Dies ist Plural (5).');
+  verify('Dies ist Plural (6).');
+  verify('Dies ist Plural (7).');
+  verify('Dies ist Plural (8).');
+  verify('Dies ist Plural (9).');
+  verify('Dies ist Plural (10).');
+  verify('Dies ist Plural (11).');
+  verify('Dies ist Plural (20).');
+  verify('Dies ist Plural (100).');
+  verify('Dies ist Plural (101).');
+  verify('Dies ist Plural (100000).');
+  verify('Alice ging zu ihrem house');
+  verify('Bob ging zu seinem house');
+  verify('cat ging zu seinem litter box');
+  verify('Alice, Bob gingen zum magasin');
+  verify('Alice ging in dem magasin');
+  verify('Niemand ging zu magasin');
+  verify('Bob, Bob gingen zum magasin');
+  verify('Alice, Alice gingen zum magasin');
+  verify('Null');
+  verify('ein');
+  verify('Mann');
+  verify('Frau');
+  verify('7 Mann');
+  verify('7 Kanadischen dollar');
+  verify('5 einige Währung oder anderen.');
+  verify('1 Kanadischer dollar');
+  verify('2 Kanadischen dollar');
+  verify('eins:');
+  verify('2 Dinge:');
+  verify('Hallo Welt');
+  verify('Hallo Welt');
+  verify('mieten');
+  verify('Miete');
+  verify('Fünf Cent US \$ 0.05');
+  verify(r"interessant (de): '<>{}= +-_$()&^%$#@!~`'");
+}
diff --git a/test/messages_with_transformer/messages_all.dart b/test/messages_with_transformer/messages_all.dart
new file mode 100644
index 0000000..b51ae4c
--- /dev/null
+++ b/test/messages_with_transformer/messages_all.dart
@@ -0,0 +1,38 @@
+/// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+/// This is a library that looks up messages for specific locales by
+/// delegating to the appropriate library.
+
+import 'dart:async';
+import 'package:intl/message_lookup_by_library.dart';
+import 'package:intl/src/intl_helpers.dart';
+import 'package:intl/intl.dart';
+
+import 'messages_zz.dart' deferred as messages_zz;
+
+
+Map<String, Function> _deferredLibraries = {
+  'zz' : () => messages_zz.loadLibrary(),
+};
+
+MessageLookupByLibrary _findExact(localeName) {
+  switch (localeName) {
+    case 'zz' : return messages_zz.messages;
+    default: return null;
+  }
+}
+
+/// User programs should call this before using [localeName] for messages.
+Future initializeMessages(String localeName) {
+  initializeInternalMessageLookup(() => new CompositeMessageLookup());
+  var lib = _deferredLibraries[Intl.canonicalizedLocale(localeName)];
+  var load = lib == null ? new Future.value(false) : lib();
+  return load.then((_) =>
+      messageLookup.addLocale(localeName, _findGeneratedMessagesFor));
+}
+
+MessageLookupByLibrary _findGeneratedMessagesFor(locale) {
+  var actualLocale = Intl.verifiedLocale(locale, (x) => _findExact(x) != null,
+      onFailure: (_) => null);
+  if (actualLocale == null) return null;
+  return _findExact(actualLocale);
+}
diff --git a/test/messages_with_transformer/messages_zz.dart b/test/messages_with_transformer/messages_zz.dart
new file mode 100644
index 0000000..1c2d054
--- /dev/null
+++ b/test/messages_with_transformer/messages_zz.dart
@@ -0,0 +1,20 @@
+/// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+/// This is a library that provides messages for a zz locale. All the
+/// messages from the main program should be duplicated here with the same
+/// function name.
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+final _keepAnalysisHappy = Intl.defaultLocale;
+
+class MessageLookup extends MessageLookupByLibrary {
+
+  get localeName => 'zz';
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static _notInlinedMessages(_) => {
+    "foo" : MessageLookupByLibrary.simpleMessage("bar")
+  };
+}
\ No newline at end of file
diff --git a/test/messages_with_transformer/regenerate_translated_libraries.sh b/test/messages_with_transformer/regenerate_translated_libraries.sh
new file mode 100755
index 0000000..5f02a29
--- /dev/null
+++ b/test/messages_with_transformer/regenerate_translated_libraries.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+#
+# This test is just trying to verify that the transformer runs,
+# so we generate the translated messages once and just use them.
+# If the transformer successfully invokes them then we're good.
+#
+# To regenerate the code you can run the lines in this script,
+# although translation_zz.arb must be manually created.
+dart ../../bin/extract_to_arb.dart --transformer main.dart
+# manually edit to create translation_zz.arb
+dart ../../bin/generate_from_arb.dart translation_zz.arb transformer_test.dart
diff --git a/test/messages_with_transformer/transformer_test.dart b/test/messages_with_transformer/transformer_test.dart
new file mode 100644
index 0000000..08d902b
--- /dev/null
+++ b/test/messages_with_transformer/transformer_test.dart
@@ -0,0 +1,16 @@
+import 'package:unittest/unittest.dart';
+
+import 'package:intl/intl.dart';
+
+import 'messages_all.dart';
+
+foo() => Intl.message("foo");
+
+main() async {
+  await initializeMessages("zz");
+
+test("Message without name/args", () {
+  Intl.defaultLocale = "zz";
+  expect(foo(), "bar");
+  });
+}
\ No newline at end of file