OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * This provides classes to represent the internal structure of the | |
7 * arguments to `Intl.message`. It is used when parsing sources to extract | |
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. | |
33 */ | |
34 library intl_message; | |
35 | |
36 import 'package:analyzer/analyzer.dart'; | |
37 | |
38 /** A default function for the [Message.expanded] method. */ | |
39 _nullTransform(msg, chunk) => chunk; | |
40 | |
41 /** | |
42 * An abstract superclass for Intl.message/plural/gender calls in the | |
43 * program's source text. We | |
44 * assemble these into objects that can be used to write out some translation | |
45 * format and can also print themselves into code. | |
46 */ | |
47 abstract class Message { | |
48 | |
49 /** | |
50 * All [Message]s except a [MainMessage] are contained inside some parent, | |
51 * terminating at an Intl.message call which supplies the arguments we | |
52 * use for variable substitutions. | |
53 */ | |
54 Message parent; | |
55 | |
56 Message(this.parent); | |
57 | |
58 /** | |
59 * We find the arguments from the top-level [MainMessage] and use those to | |
60 * do variable substitutions. [MainMessage] overrides this to return | |
61 * the actual arguments. | |
62 */ | |
63 get arguments => parent == null ? const [] : parent.arguments; | |
64 | |
65 /** | |
66 * We find the examples from the top-level [MainMessage] and use those | |
67 * when writing out variables. [MainMessage] overrides this to return | |
68 * the actual examples. | |
69 */ | |
70 get examples => parent == null ? const [] : parent.examples; | |
71 | |
72 /** | |
73 * The name of the top-level [MainMessage]. | |
74 */ | |
75 String get name => parent == null ? '<unnamed>' : parent.name; | |
76 | |
77 String checkValidity(MethodInvocation node, List arguments, String outerName, | |
78 FormalParameterList outerArgs) { | |
79 var hasArgs = arguments.any( | |
80 (each) => each is NamedExpression && each.name.label.name == 'args'); | |
81 var hasParameters = !outerArgs.parameters.isEmpty; | |
82 if (!hasArgs && hasParameters) { | |
83 return "The 'args' argument for Intl.message must be specified"; | |
84 } | |
85 | |
86 var messageName = arguments.firstWhere((eachArg) => | |
87 eachArg is NamedExpression && eachArg.name.label.name == 'name', | |
88 orElse: () => null); | |
89 if (messageName == null) { | |
90 return "The 'name' argument for Intl.message must be specified"; | |
91 } | |
92 if (messageName.expression is! SimpleStringLiteral) { | |
93 return "The 'name' argument for Intl.message must be a simple string " | |
94 "literal."; | |
95 } | |
96 var hasOuterName = outerName != null; | |
97 var givenName = messageName.expression.value; | |
98 var simpleMatch = outerName == givenName; | |
99 ClassDeclaration classNode(n) { | |
100 if (n == null) return null; | |
101 if (n is ClassDeclaration) return n; | |
102 return classNode(n.parent); | |
103 } | |
104 var classDeclaration = classNode(node); | |
105 var classPlusMethod = classDeclaration == null | |
106 ? null | |
107 : "${classDeclaration.name.token.toString()}_$outerName"; | |
108 var classMatch = classPlusMethod != null && (givenName == classPlusMethod); | |
109 if (!(hasOuterName && (simpleMatch || classMatch))) { | |
110 return "The 'name' argument for Intl.message must match either" | |
111 "the name of the containing function or <className>_<methodName> (" | |
112 "'$givenName' vs. '$outerName')"; | |
113 } | |
114 var simpleArguments = arguments.where((each) => each is NamedExpression && | |
115 ["desc", "name"].contains(each.name.label.name)); | |
116 var values = simpleArguments.map((each) => each.expression).toList(); | |
117 for (var arg in values) { | |
118 if (arg is! StringLiteral) { | |
119 return ("Intl.message arguments must be string literals: $arg"); | |
120 } | |
121 } | |
122 return null; | |
123 } | |
124 | |
125 /** | |
126 * Turn a value, typically read from a translation file or created out of an | |
127 * AST for a source program, into the appropriate | |
128 * subclass. We expect to get literal Strings, variable substitutions | |
129 * represented by integers, things that are already MessageChunks and | |
130 * lists of the same. | |
131 */ | |
132 static Message from(value, Message parent) { | |
133 if (value is String) return new LiteralString(value, parent); | |
134 if (value is int) return new VariableSubstitution(value, parent); | |
135 if (value is Iterable) { | |
136 if (value.length == 1) return Message.from(value[0], parent); | |
137 var result = new CompositeMessage([], parent); | |
138 var items = value.map((x) => from(x, result)).toList(); | |
139 result.pieces.addAll(items); | |
140 return result; | |
141 } | |
142 // We assume this is already a Message. | |
143 value.parent = parent; | |
144 return value; | |
145 } | |
146 | |
147 /** | |
148 * Return a string representation of this message for use in generated Dart | |
149 * code. | |
150 */ | |
151 String toCode(); | |
152 | |
153 /** | |
154 * Escape the string for use in generated Dart code and validate that it | |
155 * doesn't doesn't contain any illegal interpolations. We only allow | |
156 * simple variables ("$foo", but not "${foo}") and Intl.gender/plural | |
157 * calls. | |
158 */ | |
159 String escapeAndValidateString(String value) { | |
160 const escapes = const { | |
161 r"\": r"\\", | |
162 '"': r'\"', | |
163 "\b": r"\b", | |
164 "\f": r"\f", | |
165 "\n": r"\n", | |
166 "\r": r"\r", | |
167 "\t": r"\t", | |
168 "\v": r"\v", | |
169 "'": r"\'", | |
170 }; | |
171 | |
172 _escape(String s) => (escapes[s] == null) ? s : escapes[s]; | |
173 | |
174 var escaped = value.splitMapJoin("", onNonMatch: _escape); | |
175 | |
176 // We don't allow any ${} expressions, only $variable to avoid malicious | |
177 // code. Disallow any usage of "${". If that makes a false positive | |
178 // on a translation that legitimately contains "\\${" or other variations, | |
179 // we'll live with that rather than risk a false negative. | |
180 var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})"); | |
181 var validMatches = validInterpolations.allMatches(escaped); | |
182 escapeInvalidMatches(Match m) { | |
183 var valid = validMatches.any((x) => x.start == m.start); | |
184 if (valid) { | |
185 return m.group(0); | |
186 } else { | |
187 return "\\${m.group(0)}"; | |
188 } | |
189 } | |
190 return escaped.replaceAllMapped("\$", escapeInvalidMatches); | |
191 } | |
192 | |
193 /** | |
194 * Expand this string out into a printed form. The function [f] will be | |
195 * applied to any sub-messages, allowing this to be used to generate a form | |
196 * suitable for a wide variety of translation file formats. | |
197 */ | |
198 String expanded([Function f]); | |
199 } | |
200 | |
201 /** | |
202 * Abstract class for messages with internal structure, representing the | |
203 * main Intl.message call, plurals, and genders. | |
204 */ | |
205 abstract class ComplexMessage extends Message { | |
206 ComplexMessage(parent) : super(parent); | |
207 | |
208 /** | |
209 * When we create these from strings or from AST nodes, we want to look up and | |
210 * set their attributes by string names, so we override the indexing operators | |
211 * so that they behave like maps with respect to those attribute names. | |
212 */ | |
213 operator [](x); | |
214 | |
215 /** | |
216 * When we create these from strings or from AST nodes, we want to look up and | |
217 * set their attributes by string names, so we override the indexing operators | |
218 * so that they behave like maps with respect to those attribute names. | |
219 */ | |
220 operator []=(x, y); | |
221 | |
222 List<String> get attributeNames; | |
223 | |
224 /** | |
225 * Return the name of the message type, as it will be generated into an | |
226 * ICU-type format. e.g. choice, select | |
227 */ | |
228 String get icuMessageName; | |
229 | |
230 /** | |
231 * Return the message name we would use for this when doing Dart code | |
232 * generation, e.g. "Intl.plural". | |
233 */ | |
234 String get dartMessageName; | |
235 } | |
236 | |
237 /** | |
238 * This represents a message chunk that is a list of multiple sub-pieces, | |
239 * each of which is in turn a [Message]. | |
240 */ | |
241 class CompositeMessage extends Message { | |
242 List<Message> pieces; | |
243 | |
244 CompositeMessage.withParent(parent) : super(parent); | |
245 CompositeMessage(this.pieces, ComplexMessage parent) : super(parent) { | |
246 pieces.forEach((x) => x.parent = this); | |
247 } | |
248 toCode() => pieces.map((each) => each.toCode()).join(''); | |
249 toString() => "CompositeMessage(" + pieces.toString() + ")"; | |
250 String expanded([Function f = _nullTransform]) => | |
251 pieces.map((chunk) => f(this, chunk)).join(""); | |
252 } | |
253 | |
254 /** Represents a simple constant string with no dynamic elements. */ | |
255 class LiteralString extends Message { | |
256 String string; | |
257 LiteralString(this.string, Message parent) : super(parent); | |
258 toCode() => escapeAndValidateString(string); | |
259 toString() => "Literal($string)"; | |
260 String expanded([Function f = _nullTransform]) => f(this, string); | |
261 } | |
262 | |
263 /** | |
264 * Represents an interpolation of a variable value in a message. We expect | |
265 * this to be specified as an [index] into the list of variables, or else | |
266 * as the name of a variable that exists in [arguments] and we will | |
267 * compute the variable name or the index based on the value of the other. | |
268 */ | |
269 class VariableSubstitution extends Message { | |
270 VariableSubstitution(this._index, Message parent) : super(parent); | |
271 | |
272 /** | |
273 * Create a substitution based on the name rather than the index. The name | |
274 * may have been used as all upper-case in the translation tool, so we | |
275 * save it separately and look it up case-insensitively once the parent | |
276 * (and its arguments) are definitely available. | |
277 */ | |
278 VariableSubstitution.named(String name, Message parent) : super(parent) { | |
279 _variableNameUpper = name.toUpperCase(); | |
280 } | |
281 | |
282 /** The index in the list of parameters of the containing function. */ | |
283 int _index; | |
284 int get index { | |
285 if (_index != null) return _index; | |
286 if (arguments.isEmpty) return null; | |
287 // We may have been given an all-uppercase version of the name, so compare | |
288 // case-insensitive. | |
289 _index = arguments | |
290 .map((x) => x.toUpperCase()) | |
291 .toList() | |
292 .indexOf(_variableNameUpper); | |
293 if (_index == -1) { | |
294 throw new ArgumentError( | |
295 "Cannot find parameter named '$_variableNameUpper' in " | |
296 "message named '$name'. Available " | |
297 "parameters are $arguments"); | |
298 } | |
299 return _index; | |
300 } | |
301 | |
302 /** | |
303 * The variable name we get from parsing. This may be an all uppercase version | |
304 * of the Dart argument name. | |
305 */ | |
306 String _variableNameUpper; | |
307 | |
308 /** | |
309 * The name of the variable in the parameter list of the containing function. | |
310 * Used when generating code for the interpolation. | |
311 */ | |
312 String get variableName => | |
313 _variableName == null ? _variableName = arguments[index] : _variableName; | |
314 String _variableName; | |
315 // Although we only allow simple variable references, we always enclose them | |
316 // in curly braces so that there's no possibility of ambiguity with | |
317 // surrounding text. | |
318 toCode() => "\${${variableName}}"; | |
319 toString() => "VariableSubstitution($index)"; | |
320 String expanded([Function f = _nullTransform]) => f(this, index); | |
321 } | |
322 | |
323 class MainMessage extends ComplexMessage { | |
324 MainMessage() : super(null); | |
325 | |
326 /** | |
327 * All the pieces of the message. When we go to print, these will | |
328 * all be expanded appropriately. The exact form depends on what we're | |
329 * printing it for See [expanded], [toCode]. | |
330 */ | |
331 List<Message> messagePieces = []; | |
332 | |
333 /** Verify that this looks like a correct Intl.message invocation. */ | |
334 String checkValidity(MethodInvocation node, List arguments, String outerName, | |
335 FormalParameterList outerArgs) { | |
336 if (arguments.first is! StringLiteral) { | |
337 return "Intl.message messages must be string literals"; | |
338 } | |
339 | |
340 return super.checkValidity(node, arguments, outerName, outerArgs); | |
341 } | |
342 | |
343 void addPieces(List<Message> messages) { | |
344 for (var each in messages) { | |
345 messagePieces.add(Message.from(each, this)); | |
346 } | |
347 } | |
348 | |
349 /** The description provided in the Intl.message call. */ | |
350 String description; | |
351 | |
352 /** The examples from the Intl.message call */ | |
353 Map<String, dynamic> examples; | |
354 | |
355 /** | |
356 * A field to disambiguate two messages that might have exactly the | |
357 * same text. The two messages will also need different names, but | |
358 * this can be used by machine translation tools to distinguish them. | |
359 */ | |
360 String meaning; | |
361 | |
362 /** | |
363 * The name, which may come from the function name, from the arguments | |
364 * to Intl.message, or we may just re-use the message. | |
365 */ | |
366 String _name; | |
367 | |
368 /** | |
369 * A placeholder for any other identifier that the translation format | |
370 * may want to use. | |
371 */ | |
372 String id; | |
373 | |
374 /** The arguments list from the Intl.message call. */ | |
375 List arguments; | |
376 | |
377 /** | |
378 * When generating code, we store translations for each locale | |
379 * associated with the original message. | |
380 */ | |
381 Map<String, String> translations = new Map(); | |
382 | |
383 /** | |
384 * If the message was not given a name, we use the entire message string as | |
385 * the name. | |
386 */ | |
387 String get name => _name == null ? computeName() : _name; | |
388 set name(String newName) { | |
389 _name = newName; | |
390 } | |
391 | |
392 String computeName() => name = expanded((msg, chunk) => ""); | |
393 | |
394 /** | |
395 * Return the full message, with any interpolation expressions transformed | |
396 * by [f] and all the results concatenated. The chunk argument to [f] may be | |
397 * either a String, an int or an object representing a more complex | |
398 * message entity. | |
399 * See [messagePieces]. | |
400 */ | |
401 String expanded([Function f = _nullTransform]) => | |
402 messagePieces.map((chunk) => f(this, chunk)).join(""); | |
403 | |
404 /** | |
405 * Record the translation for this message in the given locale, after | |
406 * suitably escaping it. | |
407 */ | |
408 void addTranslation(String locale, Message translated) { | |
409 translated.parent = this; | |
410 translations[locale] = translated.toCode(); | |
411 } | |
412 | |
413 toCode() => | |
414 throw new UnsupportedError("MainMessage.toCode requires a locale"); | |
415 | |
416 /** | |
417 * Generate code for this message, expecting it to be part of a map | |
418 * keyed by name with values the function that calls Intl.message. | |
419 */ | |
420 String toCodeForLocale(String locale) { | |
421 var out = new StringBuffer() | |
422 ..write('static $name(') | |
423 ..write(arguments.join(", ")) | |
424 ..write(') => "') | |
425 ..write(translations[locale]) | |
426 ..write('";'); | |
427 return out.toString(); | |
428 } | |
429 | |
430 /** | |
431 * The AST node will have the attribute names as strings, so we translate | |
432 * between those and the fields of the class. | |
433 */ | |
434 void operator []=(attributeName, value) { | |
435 switch (attributeName) { | |
436 case "desc": | |
437 description = value; | |
438 return; | |
439 case "examples": | |
440 examples = value; | |
441 return; | |
442 case "name": | |
443 name = value; | |
444 return; | |
445 // We use the actual args from the parser rather than what's given in the | |
446 // arguments to Intl.message. | |
447 case "args": | |
448 return; | |
449 case "meaning": | |
450 meaning = value; | |
451 return; | |
452 default: | |
453 return; | |
454 } | |
455 } | |
456 | |
457 /** | |
458 * The AST node will have the attribute names as strings, so we translate | |
459 * between those and the fields of the class. | |
460 */ | |
461 operator [](attributeName) { | |
462 switch (attributeName) { | |
463 case "desc": | |
464 return description; | |
465 case "examples": | |
466 return examples; | |
467 case "name": | |
468 return name; | |
469 // We use the actual args from the parser rather than what's given in the | |
470 // arguments to Intl.message. | |
471 case "args": | |
472 return []; | |
473 case "meaning": | |
474 return meaning; | |
475 default: | |
476 return null; | |
477 } | |
478 } | |
479 | |
480 // This is the top-level construct, so there's no meaningful ICU name. | |
481 get icuMessageName => ''; | |
482 | |
483 get dartMessageName => "message"; | |
484 | |
485 /** The parameters that the Intl.message call may provide. */ | |
486 get attributeNames => const ["name", "desc", "examples", "args", "meaning"]; | |
487 | |
488 String toString() => | |
489 "Intl.message(${expanded()}, $name, $description, $examples, $arguments)"; | |
490 } | |
491 | |
492 /** | |
493 * An abstract class to represent sub-sections of a message, primarily | |
494 * plurals and genders. | |
495 */ | |
496 abstract class SubMessage extends ComplexMessage { | |
497 SubMessage() : super(null); | |
498 | |
499 /** | |
500 * Creates the sub-message, given a list of [clauses] in the sort of form | |
501 * that we're likely to get them from parsing a translation file format, | |
502 * as a list of [key, value] where value may in turn be a list. | |
503 */ | |
504 SubMessage.from(this.mainArgument, List clauses, parent) : super(parent) { | |
505 for (var clause in clauses) { | |
506 this[clause.first] = (clause.last is List) ? clause.last : [clause.last]; | |
507 } | |
508 } | |
509 | |
510 toString() => expanded(); | |
511 | |
512 /** | |
513 * The name of the main argument, which is expected to have the value | |
514 * which is one of [attributeNames] and is used to decide which clause to use. | |
515 */ | |
516 String mainArgument; | |
517 | |
518 /** | |
519 * Return the arguments that affect this SubMessage as a map of | |
520 * argument names and values. | |
521 */ | |
522 Map argumentsOfInterestFor(MethodInvocation node) { | |
523 var basicArguments = node.argumentList.arguments; | |
524 var others = basicArguments.where((each) => each is NamedExpression); | |
525 return new Map.fromIterable(others, | |
526 key: (node) => node.name.label.token.value(), | |
527 value: (node) => node.expression); | |
528 } | |
529 | |
530 /** | |
531 * Return the list of attribute names to use when generating code. This | |
532 * may be different from [attributeNames] if there are multiple aliases | |
533 * that map to the same clause. | |
534 */ | |
535 List<String> get codeAttributeNames; | |
536 | |
537 String expanded([Function transform = _nullTransform]) { | |
538 fullMessageForClause(key) => | |
539 key + '{' + transform(parent, this[key]).toString() + '}'; | |
540 var clauses = attributeNames | |
541 .where((key) => this[key] != null) | |
542 .map(fullMessageForClause) | |
543 .toList(); | |
544 return "{$mainArgument,$icuMessageName, ${clauses.join("")}}"; | |
545 } | |
546 | |
547 String toCode() { | |
548 var out = new StringBuffer(); | |
549 out.write('\${'); | |
550 out.write(dartMessageName); | |
551 out.write('('); | |
552 out.write(mainArgument); | |
553 var args = codeAttributeNames.where((attribute) => this[attribute] != null); | |
554 args.fold( | |
555 out, (buffer, arg) => buffer..write(", $arg: '${this[arg].toCode()}'")); | |
556 out.write(")}"); | |
557 return out.toString(); | |
558 } | |
559 } | |
560 | |
561 /** | |
562 * Represents a message send of [Intl.gender] inside a message that is to | |
563 * be internationalized. This corresponds to an ICU message syntax "select" | |
564 * with "male", "female", and "other" as the possible options. | |
565 */ | |
566 class Gender extends SubMessage { | |
567 Gender(); | |
568 /** | |
569 * Create a new Gender providing [mainArgument] and the list of possible | |
570 * clauses. Each clause is expected to be a list whose first element is a | |
571 * variable name and whose second element is either a [String] or | |
572 * a list of strings and [Message] or [VariableSubstitution]. | |
573 */ | |
574 Gender.from(String mainArgument, List clauses, Message parent) | |
575 : super.from(mainArgument, clauses, parent); | |
576 | |
577 Message female; | |
578 Message male; | |
579 Message other; | |
580 | |
581 String get icuMessageName => "select"; | |
582 String get dartMessageName => 'Intl.gender'; | |
583 | |
584 get attributeNames => ["female", "male", "other"]; | |
585 get codeAttributeNames => attributeNames; | |
586 | |
587 /** | |
588 * The node will have the attribute names as strings, so we translate | |
589 * between those and the fields of the class. | |
590 */ | |
591 void operator []=(attributeName, rawValue) { | |
592 var value = Message.from(rawValue, this); | |
593 switch (attributeName) { | |
594 case "female": | |
595 female = value; | |
596 return; | |
597 case "male": | |
598 male = value; | |
599 return; | |
600 case "other": | |
601 other = value; | |
602 return; | |
603 default: | |
604 return; | |
605 } | |
606 } | |
607 Message operator [](String attributeName) { | |
608 switch (attributeName) { | |
609 case "female": | |
610 return female; | |
611 case "male": | |
612 return male; | |
613 case "other": | |
614 return other; | |
615 default: | |
616 return other; | |
617 } | |
618 } | |
619 } | |
620 | |
621 class Plural extends SubMessage { | |
622 Plural(); | |
623 Plural.from(String mainArgument, List clauses, Message parent) | |
624 : super.from(mainArgument, clauses, parent); | |
625 | |
626 Message zero; | |
627 Message one; | |
628 Message two; | |
629 Message few; | |
630 Message many; | |
631 Message other; | |
632 | |
633 String get icuMessageName => "plural"; | |
634 String get dartMessageName => "Intl.plural"; | |
635 | |
636 get attributeNames => ["=0", "=1", "=2", "few", "many", "other"]; | |
637 get codeAttributeNames => ["zero", "one", "two", "few", "many", "other"]; | |
638 | |
639 /** | |
640 * The node will have the attribute names as strings, so we translate | |
641 * between those and the fields of the class. | |
642 */ | |
643 void operator []=(String attributeName, rawValue) { | |
644 var value = Message.from(rawValue, this); | |
645 switch (attributeName) { | |
646 case "zero": | |
647 zero = value; | |
648 return; | |
649 case "=0": | |
650 zero = value; | |
651 return; | |
652 case "one": | |
653 one = value; | |
654 return; | |
655 case "=1": | |
656 one = value; | |
657 return; | |
658 case "two": | |
659 two = value; | |
660 return; | |
661 case "=2": | |
662 two = value; | |
663 return; | |
664 case "few": | |
665 few = value; | |
666 return; | |
667 case "many": | |
668 many = value; | |
669 return; | |
670 case "other": | |
671 other = value; | |
672 return; | |
673 default: | |
674 return; | |
675 } | |
676 } | |
677 | |
678 Message operator [](String attributeName) { | |
679 switch (attributeName) { | |
680 case "zero": | |
681 return zero; | |
682 case "=0": | |
683 return zero; | |
684 case "one": | |
685 return one; | |
686 case "=1": | |
687 return one; | |
688 case "two": | |
689 return two; | |
690 case "=2": | |
691 return two; | |
692 case "few": | |
693 return few; | |
694 case "many": | |
695 return many; | |
696 case "other": | |
697 return other; | |
698 default: | |
699 return other; | |
700 } | |
701 } | |
702 } | |
703 | |
704 /** | |
705 * Represents a message send of [Intl.select] inside a message that is to | |
706 * be internationalized. This corresponds to an ICU message syntax "select" | |
707 * with arbitrary options. | |
708 */ | |
709 class Select extends SubMessage { | |
710 Select(); | |
711 /** | |
712 * Create a new [Select] providing [mainArgument] and the list of possible | |
713 * clauses. Each clause is expected to be a list whose first element is a | |
714 * variable name and whose second element is either a String or | |
715 * a list of strings and [Message]s or [VariableSubstitution]s. | |
716 */ | |
717 Select.from(String mainArgument, List clauses, Message parent) | |
718 : super.from(mainArgument, clauses, parent); | |
719 | |
720 Map<String, Message> cases = new Map<String, Message>(); | |
721 | |
722 String get icuMessageName => "select"; | |
723 String get dartMessageName => 'Intl.select'; | |
724 | |
725 get attributeNames => cases.keys; | |
726 get codeAttributeNames => attributeNames; | |
727 | |
728 void operator []=(attributeName, rawValue) { | |
729 var value = Message.from(rawValue, this); | |
730 cases[attributeName] = value; | |
731 } | |
732 | |
733 Message operator [](String attributeName) { | |
734 var exact = cases[attributeName]; | |
735 return exact == null ? cases["other"] : exact; | |
736 } | |
737 | |
738 /** | |
739 * Return the arguments that we care about for the select. In this | |
740 * case they will all be passed in as a Map rather than as the named | |
741 * arguments used in Plural/Gender. | |
742 */ | |
743 Map argumentsOfInterestFor(MethodInvocation node) { | |
744 var casesArgument = node.argumentList.arguments[1]; | |
745 return new Map.fromIterable(casesArgument.entries, | |
746 key: (node) => node.key.value, value: (node) => node.value); | |
747 } | |
748 | |
749 /** | |
750 * Write out the generated representation of this message. This differs | |
751 * from Plural/Gender in that it prints a literal map rather than | |
752 * named arguments. | |
753 */ | |
754 String toCode() { | |
755 var out = new StringBuffer(); | |
756 out.write('\${'); | |
757 out.write(dartMessageName); | |
758 out.write('('); | |
759 out.write(mainArgument); | |
760 var args = codeAttributeNames; | |
761 out.write(", {"); | |
762 args.fold(out, | |
763 (buffer, arg) => buffer..write("'$arg': '${this[arg].toCode()}', ")); | |
764 out.write("})}"); | |
765 return out.toString(); | |
766 } | |
767 } | |
OLD | NEW |