Index: pkg/intl/lib/src/intl_message.dart |
diff --git a/pkg/intl/lib/src/intl_message.dart b/pkg/intl/lib/src/intl_message.dart |
deleted file mode 100644 |
index 8bb9fdb4805eb8531631e244d963d5eb8542d525..0000000000000000000000000000000000000000 |
--- a/pkg/intl/lib/src/intl_message.dart |
+++ /dev/null |
@@ -1,704 +0,0 @@ |
-// 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 '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; |
- |
- String checkValidity(MethodInvocation node, List arguments, |
- String outerName, FormalParameterList outerArgs) { |
- var hasArgs = arguments.any( |
- (each) => each is NamedExpression && each.name.label.name == 'args'); |
- var hasParameters = !outerArgs.parameters.isEmpty; |
- if (!hasArgs && hasParameters) { |
- return "The 'args' argument for Intl.message must be specified"; |
- } |
- |
- var messageName = arguments.firstWhere( |
- (eachArg) => eachArg is NamedExpression && |
- eachArg.name.label.name == 'name', |
- orElse: () => null); |
- if (messageName == null) { |
- return "The 'name' argument for Intl.message must be specified"; |
- } |
- if (messageName.expression is! SimpleStringLiteral) { |
- return "The 'name' argument for Intl.message must be a simple string " |
- "literal."; |
- } |
- if (outerName != null && outerName != messageName.expression.value) { |
- return "The 'name' argument for Intl.message must match " |
- "the name of the containing function (" |
- "'${messageName.expression.value}' vs. '$outerName')"; |
- } |
- 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 (arg is! StringLiteral) { |
- return( "Intl.message arguments must be string literals: $arg"); |
- } |
- } |
- return null; |
- } |
- |
- /** |
- * 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(value, Message parent) { |
- if (value is String) return new LiteralString(value, parent); |
- if (value is int) return new VariableSubstitution(value, parent); |
- if (value is Iterable) { |
- 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. |
- value.parent = parent; |
- return value; |
- } |
- |
- /** |
- * Return a string representation of this message for use in generated Dart |
- * code. |
- */ |
- String toCode(); |
- |
- /** |
- * Escape the string for use in generated Dart code and validate that it |
- * doesn't doesn't contain any illegal interpolations. We only allow |
- * simple variables ("$foo", but not "${foo}") and Intl.gender/plural |
- * calls. |
- */ |
- String escapeAndValidateString(String value) { |
- const escapes = const { |
- r"\" : r"\\", |
- '"' : r'\"', |
- "\b" : r"\b", |
- "\f" : r"\f", |
- "\n" : r"\n", |
- "\r" : r"\r", |
- "\t" : r"\t", |
- "\v" : r"\v", |
- "'" : r"\'", |
- }; |
- |
- _escape(String s) => (escapes[s] == null) ? s : escapes[s]; |
- |
- var escaped = value.splitMapJoin("", onNonMatch: _escape); |
- |
- // We don't allow any ${} expressions, only $variable to avoid malicious |
- // code. Disallow any usage of "${". If that makes a false positive |
- // on a translation that legitimately contains "\\${" or other variations, |
- // we'll live with that rather than risk a false negative. |
- var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})"); |
- var validMatches = validInterpolations.allMatches(escaped); |
- escapeInvalidMatches(Match m) { |
- var valid = validMatches.any((x) => x.start == m.start); |
- if (valid) { |
- return m.group(0); |
- } else { |
- return "\\${m.group(0)}"; |
- } |
- } |
- return escaped.replaceAllMapped("\$", escapeInvalidMatches); |
- } |
- |
- /** |
- * 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 [](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 []=(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 = []; |
- |
- /** Verify that this looks like a correct Intl.message invocation. */ |
- String checkValidity(MethodInvocation node, List arguments, |
- String outerName, FormalParameterList outerArgs) { |
- if (arguments.first is! StringLiteral) { |
- return "Intl.message messages must be string literals"; |
- } |
- |
- return super.checkValidity(node, arguments, outerName, outerArgs); |
- } |
- |
- void addPieces(List<Message> 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 arguments; |
- |
- /** |
- * 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 == null ? computeName() : _name; |
- set name(String newName) { _name = newName; } |
- |
- String computeName() => name = expanded((msg, chunk) => ""); |
- |
- /** |
- * 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) { |
- var out = new StringBuffer() |
- ..write('static $name(') |
- ..write(arguments.join(", ")) |
- ..write(') => "') |
- ..write(translations[locale]) |
- ..write('";'); |
- return out.toString(); |
- } |
- |
- /** |
- * The AST node will have the attribute names as strings, so we translate |
- * between those and the fields of the class. |
- */ |
- void operator []=(attributeName, value) { |
- switch (attributeName) { |
- case "desc" : description = value; return; |
- case "examples" : examples = value; 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; |
- default: return; |
- } |
- } |
- |
- /** |
- * The AST node will have the attribute names as strings, so we translate |
- * between those and the fields of the class. |
- */ |
- operator [](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(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 []=(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 []=(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) { |
- var 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(); |
- } |
-} |