blob: 28a118acbf1ec5e7ddc4e61e04b26191c5992cf5 [file] [log] [blame]
// 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();
}
}