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

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

Issue 2989763002: Update charted to 0.4.8 and roll (Closed)
Patch Set: Removed Cutch from list of reviewers Created 3 years, 4 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
« no previous file with comments | « packages/intl/lib/src/intl_helpers.dart ('k') | packages/intl/lib/src/lazy_locale_data.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « packages/intl/lib/src/intl_helpers.dart ('k') | packages/intl/lib/src/lazy_locale_data.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698