OLD | NEW |
---|---|
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 | |
OLD | NEW |