Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(41)

Side by Side Diff: pkg/intl/lib/src/intl_message.dart

Issue 18543009: Plurals and Genders (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Fix typo Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/intl/lib/src/intl_helpers.dart ('k') | pkg/intl/pubspec.yaml » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 /** 5 /**
6 * This provides the class IntlMessage to represent an occurence of 6 * This provides classes to represent the internal structure of the
7 * [Intl.message] in a program. It is used when parsing sources to extract 7 * arguments to `Intl.message`. It is used when parsing sources to extract
8 * messages or to generate code for message substitution. 8 * messages or to generate code for message substitution. Normal programs
9 * using Intl would not import this library.
10 *
11 * While it's written
12 * in a somewhat abstract way, it has some assumptions about ICU-style
13 * message syntax for parameter substitutions, choices, selects, etc.
14 *
15 * For example, if we have the message
16 * plurals(num) => Intl.message("""${Intl.plural(num,
17 * zero : 'Is zero plural?',
18 * one : 'This is singular.',
19 * other : 'This is plural ($num).')
20 * }""",
21 * name: "plurals", args: [num], desc: "Basic plurals");
22 * That is represented as a MainMessage which has only one message component, a
23 * Plural, but also has a name, list of arguments, and a description.
24 * The Plural has three different clauses. The `zero` clause is
25 * a LiteralString containing 'Is zero plural?'. The `other` clause is a
26 * CompositeMessage containing three pieces, a LiteralString for
27 * 'This is plural (', a VariableSubstitution for `num`. amd a LiteralString
28 * for '.)'.
29 *
30 * This representation isn't used at runtime. Rather, we read some format
31 * from a translation file, parse it into these objects, and they are then
32 * used to generate the code representation above.
9 */ 33 */
10 library intl_message; 34 library intl_message;
11 35
12 /** 36 /** A default function for the [Message.expanded] method. */
13 * Represents an occurence of Intl.message in the program's source text. We 37 _nullTransform(msg, chunk) => chunk;
14 * assemble it into an object that can be used to write out some translation 38
15 * format and can also print itself into code. 39 /**
16 */ 40 * An abstract superclass for Intl.message/plural/gender calls in the
17 class IntlMessage { 41 * program's source text. We
18 42 * assemble these into objects that can be used to write out some translation
19 /** 43 * format and can also print themselves into code.
20 * This holds either Strings or ints representing the message. Literal 44 */
21 * parts of the message are stored as strings. Interpolations are represented 45 abstract class Message {
22 * by the index of the function parameter that they represent. When writing 46
23 * out to a translation file format the interpolations must be turned 47 /**
24 * into the appropriate syntax, and the non-interpolated sections 48 * All [Message]s except a [MainMessage] are contained inside some parent,
25 * may be modified. See [fullMessage]. 49 * terminating at an Intl.message call which supplies the arguments we
26 */ 50 * use for variable substitutions.
27 // TODO(alanknight): This will need to be changed for plural support. 51 */
28 List messagePieces; 52 Message parent;
29 53
54 Message(this.parent);
55
56 /**
57 * We find the arguments from the top-level [MainMessage] and use those to
58 * do variable substitutions.
59 */
60 get arguments => parent == null ? const [] : parent.arguments;
61
62 /**
63 * Turn a value, typically read from a translation file or created out of an
64 * AST for a source program, into the appropriate
65 * subclass. We expect to get literal Strings, variable substitutions
66 * represented by integers, things that are already MessageChunks and
67 * lists of the same.
68 */
69 static Message from(value, Message parent) {
70 if (value is String) return new LiteralString(value, parent);
71 if (value is int) return new VariableSubstitution(value, parent);
72 if (value is Iterable) {
73 var result = new CompositeMessage([], parent);
74 var items = value.map((x) => from(x, result)).toList();
75 result.pieces.addAll(items);
76 return result;
77 }
78 // We assume this is already a Message.
79 value.parent = parent;
80 return value;
81 }
82
83 /**
84 * Return a string representation of this message for use in generated Dart
85 * code.
86 */
87 String toCode();
88
89 /**
90 * Escape the string for use in generated Dart code and validate that it
91 * doesn't doesn't contain any illegal interpolations. We only allow
92 * simple variables ("$foo", but not "${foo}") and Intl.gender/plural
93 * calls.
94 */
95 String escapeAndValidateString(String value) {
96 const escapes = const {
97 r"\" : r"\\",
98 '"' : r'\"',
99 "\b" : r"\b",
100 "\f" : r"\f",
101 "\n" : r"\n",
102 "\r" : r"\r",
103 "\t" : r"\t",
104 "\v" : r"\v",
105 "'" : r"\'",
106 };
107
108 _escape(String s) => (escapes[s] == null) ? s : escapes[s];
109
110 var escaped = value.splitMapJoin("", onNonMatch: _escape);
111
112 // We don't allow any ${} expressions, only $variable to avoid malicious
113 // code. Disallow any usage of "${". If that makes a false positive
114 // on a translation that legitimately contains "\\${" or other variations,
115 // we'll live with that rather than risk a false negative.
116 var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})");
117 var validMatches = validInterpolations.allMatches(escaped);
118 escapeInvalidMatches(Match m) {
119 var valid = validMatches.any((x) => x.start == m.start);
120 if (valid) {
121 return m.group(0);
122 } else {
123 return "\\${m.group(0)}";
124 }
125 }
126 return escaped.replaceAllMapped("\$", escapeInvalidMatches);
127 }
128
129 /**
130 * Expand this string out into a printed form. The function [f] will be
131 * applied to any sub-messages, allowing this to be used to generate a form
132 * suitable for a wide variety of translation file formats.
133 */
134 String expanded([Function f]);
135 }
136
137 /**
138 * Abstract class for messages with internal structure, representing the
139 * main Intl.message call, plurals, and genders.
140 */
141 abstract class ComplexMessage extends Message {
142
143 ComplexMessage(parent) : super(parent);
144
145 /**
146 * When we create these from strings or from AST nodes, we want to look up and
147 * set their attributes by string names, so we override the indexing operators
148 * so that they behave like maps with respect to those attribute names.
149 */
150 operator [](x);
151
152 /**
153 * When we create these from strings or from AST nodes, we want to look up and
154 * set their attributes by string names, so we override the indexing operators
155 * so that they behave like maps with respect to those attribute names.
156 */
157 operator []=(x, y);
158
159 List<String> get attributeNames;
160
161 /**
162 * Return the name of the message type, as it will be generated into an
163 * ICU-type format. e.g. choice, select
164 */
165 String get icuMessageName;
166
167 /**
168 * Return the message name we would use for this when doing Dart code
169 * generation, e.g. "Intl.plural".
170 */
171 String get dartMessageName;
172 }
173
174 /**
175 * This represents a message chunk that is a list of multiple sub-pieces,
176 * each of which is in turn a [Message].
177 */
178 class CompositeMessage extends Message {
179 List<Message> pieces;
180
181 CompositeMessage.parent(parent) : super(parent);
182 CompositeMessage(this.pieces, ComplexMessage parent) : super(parent) {
183 pieces.forEach((x) => x.parent = this);
184 }
185 toCode() => pieces.map((each) => each.toCode()).join('');
186 toString() => "CompositeMessage(" + pieces.toString() + ")";
187 String expanded([Function f = _nullTransform]) =>
188 pieces.map((chunk) => f(this, chunk)).join("");
189 }
190
191 /** Represents a simple constant string with no dynamic elements. */
192 class LiteralString extends Message {
193 String string;
194 LiteralString(this.string, Message parent) : super(parent);
195 toCode() => escapeAndValidateString(string);
196 toString() => "Literal($string)";
197 String expanded([Function f = _nullTransform]) => f(this, string);
198 }
199
200 /**
201 * Represents an interpolation of a variable value in a message. We expect
202 * this to be specified as an [index] into the list of variables, and we will
203 * compute the variable name for the interpolation based on that.
204 */
205 class VariableSubstitution extends Message {
206 VariableSubstitution(this.index, Message parent) : super(parent);
207
208 /** The index in the list of parameters of the containing function. */
209 int index;
210
211 /**
212 * The name of the variable in the parameter list of the containing function.
213 * Used when generating code for the interpolation.
214 */
215 String get variableName =>
216 _variableName == null ? _variableName = arguments[index] : _variableName;
217 String _variableName;
218 // Although we only allow simple variable references, we always enclose them
219 // in curly braces so that there's no possibility of ambiguity with
220 // surrounding text.
221 toCode() => "\${${variableName}}";
222 toString() => "VariableSubstitution($index)";
223 String expanded([Function f = _nullTransform]) => f(this, index);
224 }
225
226 class MainMessage extends ComplexMessage {
227
228 MainMessage() : super(null);
229
230 /**
231 * All the pieces of the message. When we go to print, these will
232 * all be expanded appropriately. The exact form depends on what we're
233 * printing it for See [expanded], [toCode].
234 */
235 List<Message> messagePieces = [];
236
237 void addPieces(List<Message> messages) {
238 for (var each in messages) {
239 messagePieces.add(Message.from(each, this));
240 }
241 }
242
243 /** The description provided in the Intl.message call. */
30 String description; 244 String description;
31 245
32 /** The examples from the Intl.message call */ 246 /** The examples from the Intl.message call */
33 String examples; 247 String examples;
34 248
35 /** 249 /**
36 * The name, which may come from the function name, from the arguments 250 * The name, which may come from the function name, from the arguments
37 * to Intl.message, or we may just re-use the message. 251 * to Intl.message, or we may just re-use the message.
38 */ 252 */
39 String _name; 253 String _name;
40 254
41 /** The arguments parameter from the Intl.message call. */
42 List<String> arguments;
43
44 /** 255 /**
45 * A placeholder for any other identifier that the translation format 256 * A placeholder for any other identifier that the translation format
46 * may want to use. 257 * may want to use.
47 */ 258 */
48 String id; 259 String id;
49 260
261 /** The arguments list from the Intl.message call. */
262 List arguments;
263
50 /** 264 /**
51 * When generating code, we store translations for each locale 265 * When generating code, we store translations for each locale
52 * associated with the original message. 266 * associated with the original message.
53 */ 267 */
54 Map<String, String> translations = new Map(); 268 Map<String, String> translations = new Map();
55 269
56 IntlMessage();
57
58 /** 270 /**
59 * If the message was not given a name, we use the entire message string as 271 * If the message was not given a name, we use the entire message string as
60 * the name. 272 * the name.
61 */ 273 */
62 String get name => _name == null ? computeName() : _name; 274 String get name => _name == null ? computeName() : _name;
63 void set name(x) {_name = x;} 275 void set name(x) {_name = x;}
64 String computeName() => name = fullMessage((msg, chunk) => ""); 276
277 String computeName() => name = expanded((msg, chunk) => "");
65 278
66 /** 279 /**
67 * Return the full message, with any interpolation expressions transformed 280 * Return the full message, with any interpolation expressions transformed
68 * by [f] and all the results concatenated. The argument to [f] may be 281 * by [f] and all the results concatenated. The chunk argument to [f] may be
69 * either a String or an int representing the index of a function parameter 282 * either a String, an int or an object representing a more complex
70 * that's being interpolated. See [messagePieces]. 283 * message entity.
284 * See [messagePieces].
71 */ 285 */
72 String fullMessage([Function f]) { 286 String expanded([Function f = _nullTransform]) =>
73 var transform = f == null ? (msg, chunk) => chunk : f; 287 messagePieces.map((chunk) => f(this, chunk)).join("");
74 var out = new StringBuffer(); 288
75 messagePieces.map((chunk) => transform(this, chunk)).forEach(out.write); 289 /**
290 * Record the translation for this message in the given locale, after
291 * suitably escaping it.
292 */
293 String addTranslation(String locale, Message translated) {
294 translated.parent = this;
295 translations[locale] = translated.toCode();
296 }
297
298 toCode() => throw
299 new UnsupportedError("MainMessage.toCode requires a locale");
300
301 /**
302 * Generate code for this message, expecting it to be part of a map
303 * keyed by name with values the function that calls Intl.message.
304 */
305 String toCodeForLocale(String locale) {
306 var out = new StringBuffer()
307 ..write('static $name(')
308 ..write(arguments.join(", "))
309 ..write(') => Intl.$dartMessageName("')
310 ..write(translations[locale])
311 ..write('");');
76 return out.toString(); 312 return out.toString();
77 } 313 }
78 314
79 /** 315 /**
80 * The node will have the attribute names as strings, so we translate 316 * The AST node will have the attribute names as strings, so we translate
81 * between those and the fields of the class. 317 * between those and the fields of the class.
82 */ 318 */
83 void operator []=(attributeName, value) { 319 void operator []=(attributeName, value) {
84 switch (attributeName) { 320 switch (attributeName) {
85 case "desc" : description = value; return; 321 case "desc" : description = value; return;
86 case "examples" : examples = value; return; 322 case "examples" : examples = value; return;
87 case "name" : name = value; return; 323 case "name" : name = value; return;
88 // We use the actual args from the parser rather than what's given in the 324 // We use the actual args from the parser rather than what's given in the
89 // arguments to Intl.message. 325 // arguments to Intl.message.
90 case "args" : return; 326 case "args" : return;
91 default: return; 327 default: return;
92 } 328 }
93 } 329 }
94 330
95 /** 331 /**
96 * Record the translation for this message in the given locale, after 332 * The AST node will have the attribute names as strings, so we translate
97 * suitably escaping it. 333 * between those and the fields of the class.
98 */ 334 */
99 String addTranslation(locale, value) => 335 operator [](attributeName) {
100 translations[locale] = escapeAndValidate(locale, value); 336 switch (attributeName) {
337 case "desc" : return description;
338 case "examples" : return examples;
339 case "name" : return name;
340 // We use the actual args from the parser rather than what's given in the
341 // arguments to Intl.message.
342 case "args" : return [];
343 default: return null;
344 }
345 }
346
347 // This is the top-level construct, so there's no meaningful ICU name.
348 get icuMessageName => '';
349
350 get dartMessageName => "message";
351
352 /** The parameters that the Intl.message call may provide. */
353 get attributeNames => const ["name", "desc", "examples", "args"];
354
355 String toString() =>
356 "Intl.message(${expanded()}, $name, $description, $examples, $arguments)";
357 }
358
359 /**
360 * An abstract class to represent sub-sections of a message, primarily
361 * plurals and genders.
362 */
363 abstract class SubMessage extends ComplexMessage {
364
365 SubMessage() : super(null);
101 366
102 /** 367 /**
103 * Escape the string and validate that it doesn't contain any interpolations 368 * Creates the sub-message, given a list of [clauses] in the sort of form
104 * more complex than including a simple variable value. 369 * that we're likely to get them from parsing a translation file format,
370 * as a list of [key, value] where value may in turn be a list.
105 */ 371 */
106 String escapeAndValidate(String locale, String s) { 372 SubMessage.from(this.mainArgument, List clauses, parent) : super(parent) {
107 const escapes = const { 373 for (var clause in clauses) {
108 r"\" : r"\\", 374 this[clause.first] = (clause.last is List) ? clause.last : [clause.last];
109 '"' : r'\"',
110 "\b" : r"\b",
111 "\f" : r"\f",
112 "\n" : r"\n",
113 "\r" : r"\r",
114 "\t" : r"\t",
115 "\v" : r"\v"
116 };
117
118 _escape(String s) => (escapes[s] == null) ? s : escapes[s];
119
120 // We know that we'll be enclosing the string in double-quotes, so we need
121 // to escape those, but not single-quotes. In addition we must escape
122 // backslashes, newlines, and other formatting characters.
123 var escaped = s.splitMapJoin("", onNonMatch: _escape);
124
125 // We don't allow any ${} expressions, only $variable to avoid malicious
126 // code. Disallow any usage of "${". If that makes a false positive
127 // on a translation that legitimate contains "\\${" or other variations,
128 // we'll live with that rather than risk a false negative.
129 var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})");
130 var validMatches = validInterpolations.allMatches(escaped);
131 escapeInvalidMatches(Match m) {
132 var valid = validMatches.any((x) => x.start == m.start);
133 if (valid) {
134 return m.group(0);
135 } else {
136 return "\\${m.group(0)}";
137 }
138 } 375 }
139 return escaped.replaceAllMapped("\$", escapeInvalidMatches);
140 } 376 }
141 377
378 toString() => expanded();
379
142 /** 380 /**
143 * Generate code for this message, expecting it to be part of a map 381 * The name of the main argument, which is expected to have the value
144 * keyed by name with values the function that calls Intl.message. 382 * which is one of [attributeNames] and is used to decide which clause to use.
145 */ 383 */
146 String toCode(String locale) { 384 String mainArgument;
385
386 /**
387 * Return the list of attribute names to use when generating code. This
388 * may be different from [attributeNames] if there are multiple aliases
389 * that map to the same clause.
390 */
391 List<String> get codeAttributeNames;
392
393 String expanded([Function transform = _nullTransform]) {
394 fullMessageForClause(key) => key + '{' + transform(parent, this[key]) + '}';
395 var clauses = attributeNames
396 .where((key) => this[key] != null)
397 .map(fullMessageForClause);
398 return "{$mainArgument,$icuMessageName, ${clauses.join("")}}";
399 }
400
401 String toCode() {
147 var out = new StringBuffer(); 402 var out = new StringBuffer();
148 // These are statics because we want to closurize them into a map and 403 out.write('\${');
149 // that doesn't work for instance methods. 404 out.write(dartMessageName);
150 out.write('static $name('); 405 out.write('(');
151 out.write(arguments.join(", ")); 406 out.write(mainArgument);
152 out.write(') => Intl.message("${translations[locale]}");'); 407 var args = codeAttributeNames.where(
408 (attribute) => this[attribute] != null);
409 args.fold(out, (buffer, arg) => buffer..write(
410 ", $arg: '${this[arg].toCode()}'"));
411 out.write(")}");
153 return out.toString(); 412 return out.toString();
154 } 413 }
414 }
415
416 /**
417 * Represents a message send of [Intl.gender] inside a message that is to
418 * be internationalized. This corresponds to an ICU message syntax "select"
419 * with "male", "female", and "other" as the possible options.
420 */
421 class Gender extends SubMessage {
422
423 Gender();
424 /**
425 * Create a new IntlGender providing [mainArgument] and the list of possible
426 * clauses. Each clause is expected to be a list whose first element is a
427 * variable name and whose second element is either a String or
428 * a list of strings and IntlMessageSends or IntlVariableSubstitution.
429 */
430 Gender.from(mainArgument, List clauses, parent) :
431 super.from(mainArgument, clauses, parent);
432
433 Message female;
434 Message male;
435 Message other;
436
437 String get icuMessageName => "select";
438 String get dartMessageName => 'Intl.gender';
439
440 get attributeNames => ["female", "male", "other"];
441 get codeAttributeNames => attributeNames;
155 442
156 /** 443 /**
157 * Escape the string to be used in the name, as a map key. So no double quotes 444 * The node will have the attribute names as strings, so we translate
158 * and no interpolation. Assumes that the string has no existing escaping. 445 * between those and the fields of the class.
159 */ 446 */
160 String escapeForName(String s) { 447 void operator []=(attributeName, rawValue) {
161 var escaped1 = s.replaceAll('"', r'\"'); 448 var value = Message.from(rawValue, this);
162 var escaped2 = escaped1.replaceAll('\$', r'\$'); 449 switch (attributeName) {
163 return escaped2; 450 case "female" : female = value; return;
451 case "male" : male = value; return;
452 case "other" : other = value; return;
453 default: return;
454 }
164 } 455 }
456 Message operator [](String attributeName) {
457 switch (attributeName) {
458 case "female" : return female;
459 case "male" : return male;
460 case "other" : return other;
461 default: return other;
462 }
463 }
464 }
165 465
166 String toString() => 466 class Plural extends SubMessage {
167 "Intl.message(${fullMessage()}, $name, $description, $examples, " 467
168 "$arguments)"; 468 Plural();
169 } 469 Plural.from(mainArgument, clauses, parent) :
470 super.from(mainArgument, clauses, parent);
471
472 Message zero;
473 Message one;
474 Message two;
475 Message few;
476 Message many;
477 Message other;
478
479 String get icuMessageName => "plural";
480 String get dartMessageName => "Intl.plural";
481
482 get attributeNames => ["=0", "=1", "=2", "few", "many", "other"];
483 get codeAttributeNames => ["zero", "one", "two", "few", "many", "other"];
484
485 /**
486 * The node will have the attribute names as strings, so we translate
487 * between those and the fields of the class.
488 */
489 void operator []=(String attributeName, rawValue) {
490 var value = Message.from(rawValue, this);
491 switch (attributeName) {
492 case "zero" : zero = value; return;
493 case "=0" : zero = value; return;
494 case "one" : one = value; return;
495 case "=1" : one = value; return;
496 case "two" : two = value; return;
497 case "=2" : two = value; return;
498 case "few" : few = value; return;
499 case "many" : many = value; return;
500 case "other" : other = value; return;
501 default: return;
502 }
503 }
504
505 Message operator [](String attributeName) {
506 switch (attributeName) {
507 case "zero" : return zero;
508 case "=0" : return zero;
509 case "one" : return one;
510 case "=1" : return one;
511 case "two" : return two;
512 case "=2" : return two;
513 case "few" : return few;
514 case "many" : return many;
515 case "other" : return other;
516 default: return other;
517 }
518 }
519 }
520
OLDNEW
« no previous file with comments | « pkg/intl/lib/src/intl_helpers.dart ('k') | pkg/intl/pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698