Add an Intl transformer to automatically add name/args for messages.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=113212757
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e043b7..fba78b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 ## 0.12.7
  * Update SDK dependency to 1.12.0, to reflect use of null-aware operators.
+ * Add a transformer to automatically add the "name" and "args" parameters to
+   Intl.message and related calls. This removes a lot of tedious repetition.
 
 ## 0.12.6
   * Update links in README.md to point to current dartdocs.
diff --git a/README.md b/README.md
index 3ea5442..d26b39f 100644
--- a/README.md
+++ b/README.md
@@ -96,13 +96,20 @@
 description for translators, the arguments used in the message, and
 examples. The `name` and `args` parameters are required, and must
 match the name (or ClassName_methodName) and arguments list of the
-function respectively.  In the future we
-hope to have these provided automatically.
+function respectively. However, there is a transformer provided that
+will automatically insert those parameters for you. In pubspec.yaml,
+add a section like
 
-This can be run in the program before any translation has been done,
-and will just return the message string. It can also be extracted to a
-file and then be made to return a translated version without modifying
-the original program. See "Extracting Messages" below for more
+      transformers:
+      - intl:
+      $include: some_file.dart
+
+and then you can omit the name and args.
+
+A function with an Intl.message call can be run in the program before any
+translation has been done, and will just return the message string. It can also
+be extracted to a file and then be made to return a translated version without
+modifying the original program. See "Extracting Messages" below for more
 details.
 
 The purpose of wrapping the message in a function is to allow it to
diff --git a/bin/extract_to_arb.dart b/bin/extract_to_arb.dart
index 18b18f1..2b95437 100644
--- a/bin/extract_to_arb.dart
+++ b/bin/extract_to_arb.dart
@@ -10,13 +10,16 @@
 
 import 'dart:convert';
 import 'dart:io';
-import 'package:intl/extract_messages.dart';
-import 'package:path/path.dart' as path;
-import 'package:intl/src/intl_message.dart';
+
 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';
 
 main(List<String> args) {
   var targetDir;
+  bool transformer;
   var parser = new ArgParser();
   parser.addFlag("suppress-warnings",
       defaultsTo: false,
@@ -30,7 +33,12 @@
       defaultsTo: true,
       callback: (x) => allowEmbeddedPluralsAndGenders = x,
       help: 'Allow plurals and genders to be embedded as part of a larger '
-      'string, otherwise they must be at the top level.');
+          '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: '.',
@@ -45,7 +53,7 @@
   }
   var allMessages = {};
   for (var arg in args.where((x) => x.contains(".dart"))) {
-    var messages = parseFile(new File(arg));
+    var messages = parseFile(new File(arg), transformer);
     messages.forEach((k, v) => allMessages.addAll(toARB(v)));
   }
   var file = new File(path.join(targetDir, 'intl_messages.arb'));
@@ -116,7 +124,8 @@
   }
   if (chunk is Message) {
     return chunk.expanded((message, chunk) => turnInterpolationIntoICUForm(
-        message, chunk, shouldEscapeICU: shouldEscapeICU));
+        message, chunk,
+        shouldEscapeICU: shouldEscapeICU));
   }
   throw new FormatException("Illegal interpolation: $chunk");
 }
diff --git a/bin/generate_from_arb.dart b/bin/generate_from_arb.dart
index 338067a..06d0809 100644
--- a/bin/generate_from_arb.dart
+++ b/bin/generate_from_arb.dart
@@ -20,12 +20,14 @@
 
 import 'dart:convert';
 import 'dart:io';
-import 'package:intl/extract_messages.dart';
-import 'package:intl/src/icu_parser.dart';
-import 'package:intl/src/intl_message.dart';
-import 'package:intl/generate_localized.dart';
-import 'package:path/path.dart' as path;
+
 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.
@@ -63,9 +65,11 @@
   }
 
   // We're re-parsing the original files to find the corresponding messages,
-  // so if there are warnings extracting the messages, suppress them.
+  // so if there are warnings extracting the messages, suppress them, and
+  // always pretend the transformer was in use so we don't fail for missing
+  // names/args.
   suppressWarnings = true;
-  var allMessages = dartFiles.map((each) => parseFile(new File(each)));
+  var allMessages = dartFiles.map((each) => parseFile(new File(each), true));
 
   messages = new Map();
   for (var eachMap in allMessages) {
diff --git a/bin/rewrite_intl_messages.dart b/bin/rewrite_intl_messages.dart
new file mode 100644
index 0000000..f983c72
--- /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(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
index a2a8809..1b5928c 100644
--- a/lib/extract_messages.dart
+++ b/lib/extract_messages.dart
@@ -50,34 +50,38 @@
 
 /// Parse the source of the Dart program file [file] and return a Map from
 /// message names to [IntlMessage] instances.
-Map<String, MainMessage> parseFile(File file) {
+///
+/// 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]) {
   try {
-    _root = parseDartFile(file.path);
+    root = parseDartFile(file.path);
   } on AnalyzerErrorGroup catch (e) {
     print("Error in parsing ${file.path}, no messages extracted.");
     print("  $e");
     return {};
   }
-  _origin = file.path;
+  origin = file.path;
   var visitor = new MessageFindingVisitor();
-  _root.accept(visitor);
+  visitor.generateNameAndArgs = transformer;
+  root.accept(visitor);
   return visitor.messages;
 }
 
 /// 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;
+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 origin;
 
 String _reportErrorLocation(AstNode node) {
   var result = new StringBuffer();
-  if (_origin != null) result.write("    from $_origin");
-  var info = _root.lineInfo;
+  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}");
@@ -95,6 +99,10 @@
   /// 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.
@@ -134,7 +142,8 @@
     }
     var arguments = node.argumentList.arguments;
     var instance = _expectedInstance(node.methodName.name);
-    return instance.checkValidity(node, arguments, name, parameters);
+    return instance.checkValidity(node, arguments, name, parameters,
+        nameAndArgsGenerated: generateNameAndArgs);
   }
 
   /// Record the parameters of the function or method declaration we last
@@ -205,7 +214,15 @@
   MainMessage _messageFromNode(
       MethodInvocation node, Function extract, Function setAttribute) {
     var message = new MainMessage();
-    message.name = name;
+    message.sourcePosition = node.offset;
+    message.endPosition = node.end;
+    if (generateNameAndArgs) {
+      // Always try for class_method if this is a class method and transforming.
+      // It will be overwritten below if the message specifies it explicitly.
+      message.name = Message.classPlusMethodName(node, name) ?? name;
+    } else {
+      message.name = name;
+    }
     message.arguments =
         parameters.parameters.map((x) => x.identifier.name).toList();
     var arguments = node.argumentList.arguments;
@@ -271,7 +288,7 @@
       return message;
     }
 
-    void setAttribute(MainMessage msg, String fieldName, String fieldValue) {
+    void setAttribute(MainMessage msg, String fieldName, fieldValue) {
       if (msg.attributeNames.contains(fieldName)) {
         msg[fieldName] = fieldValue;
       }
diff --git a/lib/generate_localized.dart b/lib/generate_localized.dart
index 62aa394..1824cdb 100644
--- a/lib/generate_localized.dart
+++ b/lib/generate_localized.dart
@@ -163,6 +163,8 @@
 
 final messages = new MessageLookup();
 
+final _keepAnalysisHappy = Intl.defaultLocale;
+
 class MessageLookup extends MessageLookupByLibrary {
   get localeName => '$locale';
 
diff --git a/lib/src/intl_message.dart b/lib/src/intl_message.dart
index 8800a98..15068ae 100644
--- a/lib/src/intl_message.dart
+++ b/lib/src/intl_message.dart
@@ -65,8 +65,7 @@
 
   String _evaluateAsString(expression) {
     var result = expression.accept(_evaluator);
-    if (result == ConstantEvaluator.NOT_A_CONSTANT ||
-        result is! String) {
+    if (result == ConstantEvaluator.NOT_A_CONSTANT || result is! String) {
       return null;
     } else {
       return result;
@@ -74,11 +73,12 @@
   }
 
   String checkValidity(MethodInvocation node, List arguments, String outerName,
-      FormalParameterList outerArgs) {
+      FormalParameterList outerArgs,
+      {bool nameAndArgsGenerated: false}) {
     var hasArgs = arguments.any(
         (each) => each is NamedExpression && each.name.label.name == 'args');
     var hasParameters = !outerArgs.parameters.isEmpty;
-    if (!hasArgs && hasParameters) {
+    if (!nameAndArgsGenerated && !hasArgs && hasParameters) {
       return "The 'args' argument for Intl.message must be specified";
     }
 
@@ -86,30 +86,26 @@
         (eachArg) =>
             eachArg is NamedExpression && eachArg.name.label.name == 'name',
         orElse: () => null);
-    if (messageName == null) {
+    if (!nameAndArgsGenerated && messageName == null) {
       return "The 'name' argument for Intl.message must be specified";
     }
-    var givenName = _evaluateAsString(messageName.expression);
-    if (givenName == null) {
+
+    var givenName =
+        messageName == null ? null : _evaluateAsString(messageName.expression);
+    if (messageName != null && givenName == null) {
       return "The 'name' argument for Intl.message must be a string literal";
     }
     var hasOuterName = outerName != null;
-    var simpleMatch = outerName == givenName;
-    ClassDeclaration classNode(n) {
-      if (n == null) return null;
-      if (n is ClassDeclaration) return n;
-      return classNode(n.parent);
-    }
-    var classDeclaration = classNode(node);
-    var classPlusMethod = classDeclaration == null
-        ? null
-        : "${classDeclaration.name.token.toString()}_$outerName";
+    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> ("
-          "'$givenName' vs. '$outerName')";
+          "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();
@@ -121,6 +117,23 @@
     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
@@ -145,7 +158,8 @@
   /// code.
   String toCode();
 
-  /// Escape the string for use in generated Dart code and validate that it
+  /// Escape the string for use in generated Dart code and
+  /// optionally validate that it
   /// doesn't  doesn't contain any illegal interpolations. We only allow
   /// simple variables ("$foo", but not "${foo}") and Intl.gender/plural
   /// calls.
@@ -165,13 +179,16 @@
     _escape(String s) => (escapes[s] == null) ? s : escapes[s];
 
     var escaped = value.splitMapJoin("", onNonMatch: _escape);
+    return disallowInvalidInterpolations(escaped);
+  }
 
-    // We don't allow any ${} expressions, only $variable to avoid malicious
-    // code. Disallow any usage of "${". If that makes a false positive
-    // on a translation that legitimately contains "\\${" or other variations,
-    // we'll live with that rather than risk a false negative.
+  /// Disallow ${} expressions, only allow $variable so as to avoid malicious
+  /// code. Disallow any usage of "${". If that makes a false positive
+  /// on a translation that legitimately contains "\\${" or other variations,
+  /// we'll live with that rather than risk a false negative.
+  String disallowInvalidInterpolations(String input) {
     var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})");
-    var validMatches = validInterpolations.allMatches(escaped);
+    var validMatches = validInterpolations.allMatches(input);
     escapeInvalidMatches(Match m) {
       var valid = validMatches.any((x) => x.start == m.start);
       if (valid) {
@@ -180,7 +197,7 @@
         return "\\${m.group(0)}";
       }
     }
-    return escaped.replaceAllMapped("\$", escapeInvalidMatches);
+    return input.replaceAllMapped("\$", escapeInvalidMatches);
   }
 
   /// Expand this string out into a printed form. The function [f] will be
@@ -301,14 +318,22 @@
   /// 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) {
+      FormalParameterList outerArgs,
+      {nameAndArgsGenerated: false}) {
     if (arguments.first is! StringLiteral) {
       return "Intl.message messages must be string literals";
     }
 
-    return super.checkValidity(node, arguments, outerName, outerArgs);
+    return super.checkValidity(node, arguments, outerName, outerArgs,
+        nameAndArgsGenerated: nameAndArgsGenerated);
   }
 
   void addPieces(List<Message> messages) {
@@ -339,6 +364,9 @@
   /// The arguments list from the Intl.message call.
   List 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();
@@ -382,6 +410,24 @@
     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("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 []=(attributeName, value) {
@@ -402,6 +448,9 @@
       case "meaning":
         meaning = value;
         return;
+      case "locale":
+        locale = value;
+        return;
       default:
         return;
     }
@@ -504,6 +553,7 @@
 /// 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
@@ -640,6 +690,7 @@
 /// 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
diff --git a/lib/src/message_rewriter.dart b/lib/src/message_rewriter.dart
new file mode 100644
index 0000000..711c05a
--- /dev/null
+++ b/lib/src/message_rewriter.dart
@@ -0,0 +1,49 @@
+// 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) {
+    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<MainMessage> findMessages(String source, String sourceName) {
+  try {
+    root = parseCompilationUnit(source, name: sourceName);
+  } on AnalyzerErrorGroup catch (e) {
+    print("Error in parsing $sourceName, no messages extracted.");
+    print("  $e");
+    return '';
+  }
+  origin = sourceName;
+  var visitor = new MessageFindingVisitor();
+  visitor.generateNameAndArgs = true;
+  root.accept(visitor);
+  return visitor.messages.values.toList();
+}
diff --git a/lib/transformer.dart b/lib/transformer.dart
new file mode 100644
index 0000000..aa0d303
--- /dev/null
+++ b/lib/transformer.dart
@@ -0,0 +1,27 @@
+// 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 transformer for Intl messages, supplying the name and arguments
+/// automatically.
+library intl_transformer;
+
+import 'package:barback/barback.dart';
+
+import 'extract_messages.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";
+
+  Future 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 d0ff58c..aac05c9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -12,6 +12,7 @@
   path: '>=0.9.0 <2.0.0'
   petitparser: '>=1.1.3 <2.0.0'
 dev_dependencies:
+  barback: ^0.15.2
   fixnum: '>=0.9.0 <0.11.0'
   unittest: '>=0.10.0 <0.12.0'
 transformers:
@@ -29,3 +30,6 @@
     - 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/main.dart
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