| // 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; |
| |
| 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; |
| } |
| } |
| |
| String checkValidity(MethodInvocation node, List arguments, String outerName, |
| FormalParameterList outerArgs, |
| {bool nameAndArgsGenerated: false}) { |
| 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 messageName = arguments.firstWhere( |
| (eachArg) => |
| eachArg is NamedExpression && eachArg.name.label.name == 'name', |
| orElse: () => null); |
| if (!nameAndArgsGenerated && messageName == null) { |
| return "The 'name' argument for Intl.message must be specified"; |
| } |
| |
| var givenName = |
| messageName == null ? null : _evaluateAsString(messageName.expression); |
| if (messageName != null && givenName == null) { |
| return "The 'name' argument for Intl.message must be a string literal"; |
| } |
| var hasOuterName = outerName != null; |
| var simpleMatch = outerName == givenName || 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"); |
| } |
| } |
| 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 and |
| /// optionally validate that it |
| /// doesn't doesn't contain any illegal interpolations. We only allow |
| /// simple variables ("$foo", but not "${foo}") and Intl.gender/plural |
| /// calls. |
| 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"\'", |
| }; |
| |
| String _escape(String s) => (escapes[s] == null) ? s : escapes[s]; |
| |
| var escaped = value.splitMapJoin("", onNonMatch: _escape); |
| return disallowInvalidInterpolations(escaped); |
| } |
| |
| /// Disallow ${} expressions, only allow $variable so as to avoid malicious |
| /// code. Disallow any usage of "${". If that makes a false positive |
| /// on a translation that legitimately contains "\\${" or other variations, |
| /// we'll live with that rather than risk a false negative. |
| String disallowInvalidInterpolations(String input) { |
| var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})"); |
| var validMatches = validInterpolations.allMatches(input); |
| String escapeInvalidMatches(Match m) { |
| var valid = validMatches.any((x) => x.start == m.start); |
| if (valid) { |
| return m.group(0); |
| } else { |
| return "\\${m.group(0)}"; |
| } |
| } |
| return input.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 = []; |
| |
| /// 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, |
| {nameAndArgsGenerated: false}) { |
| if (arguments.first is! StringLiteral) { |
| return "Intl.message messages must be string literals"; |
| } |
| |
| return super.checkValidity(node, arguments, outerName, outerArgs, |
| nameAndArgsGenerated: nameAndArgsGenerated); |
| } |
| |
| 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; |
| |
| /// 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 == 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(); |
| } |
| |
| turnInterpolationBackIntoStringForm(Message message, chunk) { |
| if (chunk is String) return escapeAndValidateString(chunk); |
| if (chunk is int) return r"${" + message.arguments[chunk] + "}"; |
| if (chunk is Message) return chunk.toCode(); |
| throw new ArgumentError.value(chunk, "Unexpected value in Intl.message"); |
| } |
| |
| String toOriginalCode() { |
| var out = new StringBuffer()..write('Intl.message("'); |
| out.write(expanded(turnInterpolationBackIntoStringForm)); |
| out.write('", '); |
| out.write('name: "$name", '); |
| out.write(locale == null ? "" : 'locale: "$locale", '); |
| out.write("args: [${arguments.join(', ')}]"); |
| out.write(")"); |
| return out.toString(); |
| } |
| |
| /// The AST node will have the attribute names as strings, so we translate |
| /// between those and the fields of the class. |
| void operator []=(attributeName, value) { |
| 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 [](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 []=(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) { |
| 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(); |
| } |
| } |