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