Chromium Code Reviews| Index: pkg/intl/lib/src/intl_message.dart |
| diff --git a/pkg/intl/lib/src/intl_message.dart b/pkg/intl/lib/src/intl_message.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..aadb4eefee44dfb9252da04eb1edbca16d0a1e49 |
| --- /dev/null |
| +++ b/pkg/intl/lib/src/intl_message.dart |
| @@ -0,0 +1,163 @@ |
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +/** |
| + * This provides the class IntlMessage to represent an occurence of |
| + * [Intl.message] in a program. It is used when parsing sources to extract |
| + * messages or to generate code for message substitution. |
| + */ |
| +library intl_message; |
| + |
| +/** |
| + * Represents an occurence of Intl.message in the program's source text. We |
|
Emily Fortuna
2013/03/13 18:54:43
nit: you have an extra space starting this line of
Alan Knight
2013/03/14 17:49:05
Done. It's 2013, where's my autoformatter?
|
| + * assemble it into an object that can be used to write out some translation |
| + * format and can also print itself into code. |
| + */ |
| +class IntlMessage { |
| + |
| + /** |
| + * The message, which may be just one string, or may be several strings, |
| + * some of which are interpolation expressions. The output format must |
| + * determine what to do with the |
|
Emily Fortuna
2013/03/13 18:54:43
unfinished comment
Alan Knight
2013/03/14 17:49:05
Done.
|
| + */ |
| + List<String> messagePieces; |
| + |
| + String description; |
| + |
| + /** The examples from the Intl.message call */ |
| + String examples; |
| + |
| + /** The name, which may come from the function name, from the arguments |
|
Emily Fortuna
2013/03/13 18:54:43
comment formatting. (also, end in period, here and
Alan Knight
2013/03/14 17:49:05
Done.
|
| + * to Intl.message, or we may just re-use the message |
| + */ |
| + String _name; |
| + |
| + /** The arguments parameter from the Intl.message call */ |
| + List<String> arguments; |
| + |
| + /** |
| + * A placeholder for any other identifier that the translation format |
| + * may want to use. |
| + */ |
| + String id; |
| + |
| + /** |
| + * When generating code, we store translations for each locale |
| + * associated with the original message. |
| + */ |
| + Map<String, String> translations = new Map(); |
| + |
| + IntlMessage(); |
| + |
| + /** |
| + * If the message was not given a name, we use the entire message string as |
| + * the name. |
| + */ |
| + get name => _name == null ? computeName() : _name; |
| + set name(x) => _name = x; |
| + computeName() => name = fullMessage((msg, chunk) => ""); |
| + |
| + /** |
| + * Return the full message, with any interpolation expressions transformed |
| + * by [f] and all the results concatenated. |
| + */ |
| + String fullMessage([Function f]) { |
| + var transform = f == null ? (msg, chunk) => chunk : f; |
| + var out = new StringBuffer(); |
| + messagePieces.map((chunk) => transform(this, chunk)).forEach(out.write); |
| + return out.toString(); |
| + } |
| + |
| + /** |
| + * The node will have the attribute names as strings, so we translate |
| + * between those and the fields of the class. |
| + */ |
| + operator []=(attributeName, value) { |
| + switch (attributeName) { |
| + case "desc" : description = value; return; |
| + case "examples" : examples = value; return; |
| + case "name" : name = value; return; |
| + // We use the actual args from the parser rather than what's given in the |
| + // arguments to Intl.message. |
| + case "args" : return; |
| + default: return; |
| + } |
| + } |
| + |
| + /** |
| + * Record the translation for this message in the given locale, after |
| + * suitably escaping it. |
| + */ |
| + addTranslation(locale, value) => |
| + translations[locale] = escapeAndValidate(locale, value); |
| + |
| + /** |
| + * Escape the string and validate that it doesn't contain any interpolations |
| + * more complex than including a simple variable value. |
| + */ |
| + escapeAndValidate(String locale, String s) { |
| + const escapes = const { |
| + r"\" : r"\\", |
| + '"' : r'\"', |
| + "\b" : r"\b", |
| + "\f" : r"\f", |
| + "\n" : r"\n", |
| + "\r" : r"\r", |
| + "\t" : r"\t", |
| + "\v" : r"\v" |
| + }; |
| + |
| + _escape(String s) => (escapes[s] == null) ? s : escapes[s]; |
| + |
| + // We know that we'll be enclosing the string in double-quotes, so we need |
| + // to escape those, but not single-quotes. In addition we must escape |
| + // backslashes, newlines, and other formatting characters. |
| + var escaped = s.splitMapJoin("", onNonMatch: _escape); |
| + print(escaped); |
| + |
| + // We don't allow any ${} expressions, only $variable to avoid malicious |
| + // code. Disallow any usage of "${". If that makes a false positive |
| + // on a translation that legitimate contains "\\${" or other variations, |
| + // we'll live with that rather than risk a false negative. |
| + var validInterpolations = new RegExp(r"(\$\w+)|(\${\w+})"); |
| + var validMatches = validInterpolations.allMatches(escaped); |
| + escapeInvalidMatches(Match m) { |
| + var valid = validMatches.any((x) => x.start == m.start); |
| + if (valid) { |
| + return m.group(0); |
| + } else { |
| + return "\\${m.group(0)}"; |
| + } |
| + } |
| + return escaped.replaceAllMapped("\$", escapeInvalidMatches); |
| + } |
| + |
| + /** |
| + * Generate code for this message, expecting it to be part of a map |
| + * keyed by name with values the function that calls Intl.message. |
| + */ |
| + toCode(String locale) { |
|
Emily Fortuna
2013/03/13 18:54:43
return types, please
Alan Knight
2013/03/14 17:49:05
Done.
|
| + var out = new StringBuffer(); |
| + // These are statics because we want to closurize them into a map and |
| + // that doesn't work for instance methods. |
| + out.write('static $name('); |
| + out.write(arguments.join(", ")); |
| + out.write(') => Intl.message("${translations[locale]}");'); |
| + return out.toString(); |
| + } |
| + |
| + /** |
| + * Escape the string to be used in the name, as a map key. So no double quotes |
| + * and no interpolation. Assumes that the string has no existing escaping. |
| + */ |
| + escapeForName(String s) { |
| + var escaped1 = s.replaceAll('"', r'\"'); |
| + var escaped2 = escaped1.replaceAll('\$', r'\$'); |
| + return escaped2; |
| + } |
| + |
| + toString() => |
| + "Intl.message(${fullMessage()}, $name, $description, $examples, " |
| + "$arguments)"; |
| +} |