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

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

Powered by Google App Engine
This is Rietveld 408576698