Add a flag to prohibit plurals/genders that don't take up the whole string.

BUG=
R=tjblasi@google.com

Review URL: https://codereview.chromium.org//771253002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/intl@42071 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca7920e..9dca66f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.11.11
+  * Add a -no-embedded-plurals flag to reject plurals and genders that
+    have either leading or trailing text around them. This follows the
+    ICU recommendation that a plural or gender should contain the
+    entire phrase/sentence, not just part of it.
+
 ## 0.11.10
   * Fix some style glitches with naming. The only publicly visible one
     is DateFormat.parseUtc, but the parseUTC variant is still retained
diff --git a/bin/extract_to_arb.dart b/bin/extract_to_arb.dart
index 970c0bc..a8fab7b 100644
--- a/bin/extract_to_arb.dart
+++ b/bin/extract_to_arb.dart
@@ -21,16 +21,22 @@
   var targetDir;
   var parser = new ArgParser();
   parser.addFlag("suppress-warnings", defaultsTo: false, callback: (x) =>
-      suppressWarnings = x);
+      suppressWarnings = x, help: 'Suppress printing of warnings.');
   parser.addFlag("warnings-are-errors", defaultsTo: false, callback: (x) =>
-      warningsAreErrors = x);
+      warningsAreErrors = x,
+      help: 'Treat all warnings as errors, stop processing ');
+  parser.addFlag("embedded-plurals", 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.');
 
   parser.addOption("output-dir", defaultsTo: '.', callback: (value) => targetDir
-      = value);
+      = value, help: 'Specify the output directory.');
   parser.parse(args);
   if (args.length == 0) {
-    print('Usage: extract_to_arb [--output-dir=<dir>] [files.dart]');
     print('Accepts Dart files and produces intl_messages.json');
+    print('Usage: extract_to_arb [options] [files.dart]');
+    print(parser.getUsage());
     exit(0);
   }
   var allMessages = {};
diff --git a/bin/generate_from_arb.dart b/bin/generate_from_arb.dart
index 7156e23..b4e48d8 100644
--- a/bin/generate_from_arb.dart
+++ b/bin/generate_from_arb.dart
@@ -34,21 +34,26 @@
   var targetDir;
   var parser = new ArgParser();
   parser.addFlag("suppress-warnings", defaultsTo: false,
-      callback: (x) => suppressWarnings = x);
+      callback: (x) => suppressWarnings = x,
+      help: 'Suppress printing of warnings.');
   parser.addOption("output-dir", defaultsTo: '.',
-      callback: (x) => targetDir = x);
+      callback: (x) => targetDir = x,
+      help: 'Specify the output directory.');
   parser.addOption("generated-file-prefix", defaultsTo: '',
-      callback: (x) => generatedFilePrefix = x);
+      callback: (x) => generatedFilePrefix = x,
+      help: 'Specify a prefix to be used for the generated file names.');
   parser.addFlag("use-deferred-loading", defaultsTo: true,
-      callback: (x) => useDeferredLoading = x);
+      callback: (x) => 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 [--output-dir=<dir>]'
-        ' [--[no-]use-deferred-loading]'
-        ' [--generated-file-prefix=<prefix>] file1.dart file2.dart ...'
+    print('Usage: generate_from_arb [options]'
+        ' file1.dart file2.dart ...'
         ' translation1_<languageTag>.arb translation2.arb ...');
+    print(parser.getUsage());
     exit(0);
   }
 
diff --git a/lib/extract_messages.dart b/lib/extract_messages.dart
index 1f3a39e..88045be 100644
--- a/lib/extract_messages.dart
+++ b/lib/extract_messages.dart
@@ -47,6 +47,16 @@
 /** 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;
+
 /**
  * Parse the source of the Dart program file [file] and return a Map from
  * message names to [IntlMessage] instances.
@@ -228,7 +238,8 @@
     message.arguments = parameters.parameters.map(
         (x) => x.identifier.name).toList();
     var arguments = node.argumentList.arguments;
-    extract(message, arguments);
+    var extractionResult = extract(message, arguments);
+    if (extractionResult == null) return null;
 
     for (var namedArgument in arguments.where((x) => x is NamedExpression)) {
       var name = namedArgument.name.label.name;
@@ -249,10 +260,19 @@
    */
   MainMessage messageFromIntlMessageCall(MethodInvocation node) {
 
-    void extractFromIntlCall(MainMessage message, List arguments) {
+    MainMessage extractFromIntlCall(MainMessage message, List arguments) {
       try {
         var interpolation = new InterpolationVisitor(message);
         arguments.first.accept(interpolation);
+        if (interpolation.pieces.any((x) => x is Plural || x is Gender) &&
+            !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"
+                "Error at $node");
+          }
+        }
         message.messagePieces.addAll(interpolation.pieces);
       } on IntlMessageExtractionException catch (e) {
         message = null;
@@ -260,8 +280,9 @@
             ..writeAll(["Error ", e, "\nProcessing <", node, ">\n"])
             ..write(_reportErrorLocation(node));
         print(err);
-        warnings.add(err);
+        warnings.add(err.toString());
       }
+      return message; // Because we may have set it to null on an error.
     }
 
     void setValue(MainMessage message, String fieldName, Object fieldValue) {
@@ -279,10 +300,11 @@
   MainMessage messageFromDirectPluralOrGenderCall(MethodInvocation node) {
     var pluralOrGender;
 
-    void extractFromPluralOrGender(MainMessage message, _) {
+    MainMessage extractFromPluralOrGender(MainMessage message, _) {
       var visitor = new PluralAndGenderVisitor(message.messagePieces, message);
       node.accept(visitor);
       pluralOrGender = message.messagePieces.last;
+      return message;
     }
 
     void setAttribute(MainMessage msg, String fieldName, String fieldValue) {
@@ -424,7 +446,7 @@
 
   /**
    * Create a MainMessage from [node] using the name and
-   * parameters of the last function/method declaration we encountered            e
+   * parameters of the last function/method declaration we encountered
    * and the parameters to the Intl.message call.
    */
   Message messageFromMethodInvocation(MethodInvocation node) {
@@ -450,7 +472,7 @@
             ..writeAll(["Error ", e, "\nProcessing <", node, ">"])
             ..write(_reportErrorLocation(node));
         print(err);
-        warnings.add(err);
+        warnings.add(err.toString());
       }
     });
     var mainArg = node.argumentList.arguments.firstWhere(
diff --git a/pubspec.yaml b/pubspec.yaml
index 1f7f06d..c2bcbef 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: intl
-version: 0.11.10
+version: 0.11.11
 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://www.dartlang.org
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..15704ae
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_after.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 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]);
\ No newline at end of file
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..64032b1
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_after_test.dart
@@ -0,0 +1,16 @@
+// 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", () {
+    var 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..854b495
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_before.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 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..0cc44f7
--- /dev/null
+++ b/test/message_extraction/embedded_plural_text_before_test.dart
@@ -0,0 +1,16 @@
+// 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", () {
+    var files = ['embedded_plural_text_before.dart'];
+    runTestWithWarnings(warningsAreErrors: true, expectedExitCode: 1,
+        embeddedPlurals: false, sourceFiles: files);
+  });
+}
diff --git a/test/message_extraction/failed_extraction_test.dart b/test/message_extraction/failed_extraction_test.dart
index 430a43f..3a7d75e 100644
--- a/test/message_extraction/failed_extraction_test.dart
+++ b/test/message_extraction/failed_extraction_test.dart
@@ -13,7 +13,11 @@
   });
 }
 
-void runTestWithWarnings({bool warningsAreErrors, int expectedExitCode}) {
+const 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}) {
 
   void verify(ProcessResult result) {
     try {
@@ -29,8 +33,10 @@
   if (warningsAreErrors) {
     args.add('--warnings-are-errors');
   }
-  var files = [asTempDirPath("sample_with_messages.dart"), asTempDirPath(
-      "part_of_sample_with_messages.dart"),];
+  if (!embeddedPlurals) {
+    args.add('--no-embedded-plurals');
+  }
+  var files = sourceFiles.map(asTempDirPath).toList();
   var allArgs = [program]
       ..addAll(args)
       ..addAll(files);
diff --git a/test/message_extraction/message_extraction_test.dart b/test/message_extraction/message_extraction_test.dart
index 03f5499..2428b58 100644
--- a/test/message_extraction/message_extraction_test.dart
+++ b/test/message_extraction/message_extraction_test.dart
@@ -85,6 +85,8 @@
       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);
@@ -150,7 +152,7 @@
 
 Future<ProcessResult> generateCodeFromTranslation(ProcessResult previousResult)
     => run(previousResult, [asTestDirPath('../../bin/generate_from_arb.dart'),
-    deferredLoadArg, '--generated-file-prefix=foo_', 
+    deferredLoadArg, '--generated-file-prefix=foo_',
     'sample_with_messages.dart', 'part_of_sample_with_messages.dart',
     'translation_fr.arb', 'translation_de_DE.arb']);