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 |
index 4a1f6cd178e301c99fee80b3865262a0233ada66..07c3f993c5bdc4780e23210a257b4624effc28cc 100644 |
--- a/pkg/intl/lib/src/intl_message.dart |
+++ b/pkg/intl/lib/src/intl_message.dart |
@@ -4,29 +4,224 @@ |
/** |
* This provides the class IntlMessage to represent an occurence of |
Emily Fortuna
2013/07/03 17:52:33
IntlMessage -> MainMessage?
Alan Knight
2013/07/03 18:41:07
Yes. Rephrased the whole thing, as this now provid
|
- * [Intl.message] in a program. It is used when parsing sources to extract |
- * messages or to generate code for message substitution. |
+ * Intl.message in a program. 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. |
*/ |
library intl_message; |
+/** A default function for [IntlMessageSend.expanded]. */ |
+_nullTransform(msg, chunk) => chunk; |
+ |
/** |
- * Represents an occurence of Intl.message in the program's source text. We |
- * assemble it into an object that can be used to write out some translation |
- * format and can also print itself into code. |
+ * 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. |
*/ |
-class IntlMessage { |
+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. |
+ */ |
+ get arguments => parent == null ? const [] : parent.arguments; |
/** |
- * This holds either Strings or ints representing the message. Literal |
- * parts of the message are stored as strings. Interpolations are represented |
- * by the index of the function parameter that they represent. When writing |
- * out to a translation file format the interpolations must be turned |
- * into the appropriate syntax, and the non-interpolated sections |
- * may be modified. See [fullMessage]. |
+ * 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. |
*/ |
- // TODO(alanknight): This will need to be changed for plural support. |
- List messagePieces; |
+ 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) { |
+ 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. |
Emily Fortuna
2013/07/03 17:52:33
can you give an example in the documentation of ea
Alan Knight
2013/07/03 18:41:07
Done.
|
+ */ |
+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.parent(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(""); |
Emily Fortuna
2013/07/03 17:52:33
+2 spaces
Alan Knight
2013/07/03 18:41:07
Done.
|
+} |
+ |
+/** 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, and we will |
+ * compute the variable name for the interpolation based on that. |
+ */ |
+class VariableSubstitution extends Message { |
+ VariableSubstitution(this.index, Message parent) : super(parent); |
+ |
+ /** The index in the list of parameters of the containing function. */ |
+ int index; |
+ |
+ /** |
+ * 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 = []; |
+ |
+ 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 */ |
@@ -38,46 +233,68 @@ class IntlMessage { |
*/ |
String _name; |
- /** The arguments parameter from the Intl.message call. */ |
- List<String> arguments; |
- |
/** |
* 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(); |
- IntlMessage(); |
- |
/** |
* If the message was not given a name, we use the entire message string as |
* the name. |
*/ |
String get name => _name == null ? computeName() : _name; |
void set name(x) {_name = x;} |
- String computeName() => name = fullMessage((msg, chunk) => ""); |
+ |
+ String computeName() => name = expanded((msg, chunk) => ""); |
/** |
* Return the full message, with any interpolation expressions transformed |
- * by [f] and all the results concatenated. The argument to [f] may be |
- * either a String or an int representing the index of a function parameter |
- * that's being interpolated. See [messagePieces]. |
+ * 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 fullMessage([Function f]) { |
- var transform = f == null ? (msg, chunk) => chunk : f; |
- var out = new StringBuffer(); |
- messagePieces.map((chunk) => transform(this, chunk)).forEach(out.write); |
+ String expanded([Function f = _nullTransform]) => |
+ messagePieces.map((chunk) => f(this, chunk)).join(""); |
Emily Fortuna
2013/07/03 17:52:33
+2 spaces
Alan Knight
2013/07/03 18:41:07
Done.
|
+ |
+ /** |
+ * Record the translation for this message in the given locale, after |
+ * suitably escaping it. |
+ */ |
+ String 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(') => Intl.$dartMessageName("') |
+ ..write(translations[locale]) |
+ ..write('");'); |
return out.toString(); |
} |
/** |
- * The node will have the attribute names as strings, so we translate |
+ * 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) { |
@@ -93,77 +310,192 @@ class IntlMessage { |
} |
/** |
- * Record the translation for this message in the given locale, after |
- * suitably escaping it. |
+ * The AST node will have the attribute names as strings, so we translate |
+ * between those and the fields of the class. |
*/ |
- String addTranslation(locale, value) => |
- translations[locale] = escapeAndValidate(locale, value); |
+ 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 []; |
+ default: return null; |
+ } |
+ } |
- /** |
- * Escape the string and validate that it doesn't contain any interpolations |
- * more complex than including a simple variable value. |
- */ |
- String escapeAndValidate(String locale, String s) { |
- const escapes = const { |
- r"\" : r"\\", |
- '"' : r'\"', |
- "\b" : r"\b", |
- "\f" : r"\f", |
- "\n" : r"\n", |
- "\r" : r"\r", |
- "\t" : r"\t", |
- "\v" : r"\v" |
- }; |
+ // This is the top-level construct, so there's no meaningful ICU name. |
+ get icuMessageName => ''; |
- _escape(String s) => (escapes[s] == null) ? s : escapes[s]; |
+ get dartMessageName => "message"; |
- // We know that we'll be enclosing the string in double-quotes, so we need |
- // to escape those, but not single-quotes. In addition we must escape |
- // backslashes, newlines, and other formatting characters. |
- var escaped = s.splitMapJoin("", onNonMatch: _escape); |
+ /** The parameters that the Intl.message call may provide. */ |
+ get attributeNames => const ["name", "desc", "examples", "args"]; |
- // 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 legitimate 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)}"; |
- } |
+ 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]; |
} |
- return escaped.replaceAllMapped("\$", escapeInvalidMatches); |
} |
+ toString() => expanded(); |
+ |
/** |
- * Generate code for this message, expecting it to be part of a map |
- * keyed by name with values the function that calls Intl.message. |
+ * 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 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. |
*/ |
- String toCode(String locale) { |
+ List<String> get codeAttributeNames; |
+ |
+ String expanded([Function transform = _nullTransform]) { |
+ fullMessageForClause(key) => key + '{' + transform(parent, this[key]) + '}'; |
+ var clauses = attributeNames |
+ .where((key) => this[key] != null) |
+ .map(fullMessageForClause); |
+ return "{$mainArgument,$icuMessageName, ${clauses.join("")}}"; |
+ } |
+ |
+ String toCode() { |
var out = new StringBuffer(); |
- // These are statics because we want to closurize them into a map and |
- // that doesn't work for instance methods. |
- out.write('static $name('); |
- out.write(arguments.join(", ")); |
- out.write(') => Intl.message("${translations[locale]}");'); |
+ 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(); |
/** |
- * Escape the string to be used in the name, as a map key. So no double quotes |
- * and no interpolation. Assumes that the string has no existing escaping. |
+ * Create a new IntlGender 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 IntlMessageSends or IntlVariableSubstitution. |
*/ |
- String escapeForName(String s) { |
- var escaped1 = s.replaceAll('"', r'\"'); |
- var escaped2 = escaped1.replaceAll('\$', r'\$'); |
- return escaped2; |
+ Gender.from(mainArgument, List clauses, 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(mainArgument, clauses, 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; |
+ } |
+ } |
+} |
- String toString() => |
- "Intl.message(${fullMessage()}, $name, $description, $examples, " |
- "$arguments)"; |
-} |