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