| 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 is for use in extracting messages from a Dart program | 6 * This is for use in extracting messages from a Dart program |
| 7 * using the Intl.message() mechanism and writing them to a file for | 7 * using the Intl.message() mechanism and writing them to a file for |
| 8 * translation. This provides only the stub of a mechanism, because it | 8 * translation. This provides only the stub of a mechanism, because it |
| 9 * doesn't define how the file should be written. It provides an | 9 * doesn't define how the file should be written. It provides an |
| 10 * [IntlMessage] class that holds the extracted data and [parseString] | 10 * [IntlMessage] class that holds the extracted data and [parseString] |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 85 /** | 85 /** |
| 86 * We keep track of the data from the last MethodDeclaration, | 86 * We keep track of the data from the last MethodDeclaration, |
| 87 * FunctionDeclaration or FunctionExpression that we saw on the way down, | 87 * FunctionDeclaration or FunctionExpression that we saw on the way down, |
| 88 * as that will be the nearest parent of the Intl.message invocation. | 88 * as that will be the nearest parent of the Intl.message invocation. |
| 89 */ | 89 */ |
| 90 FormalParameterList parameters; | 90 FormalParameterList parameters; |
| 91 String name; | 91 String name; |
| 92 | 92 |
| 93 /** Return true if [node] matches the pattern we expect for Intl.message() */ | 93 /** Return true if [node] matches the pattern we expect for Intl.message() */ |
| 94 bool looksLikeIntlMessage(MethodInvocation node) { | 94 bool looksLikeIntlMessage(MethodInvocation node) { |
| 95 if (node.methodName.name != "message") return false; | 95 const validNames = const ["message", "plural", "gender"]; |
| 96 if (!validNames.contains(node.methodName.name)) return false; |
| 96 if (!(node.target is SimpleIdentifier)) return false; | 97 if (!(node.target is SimpleIdentifier)) return false; |
| 97 SimpleIdentifier target = node.target; | 98 SimpleIdentifier target = node.target; |
| 98 if (target.token.toString() != "Intl") return false; | 99 if (target.token.toString() != "Intl") return false; |
| 99 return true; | 100 return true; |
| 100 } | 101 } |
| 101 | 102 |
| 102 /** | 103 /** |
| 103 * Returns a String describing why the node is invalid, or null if no | 104 * Returns a String describing why the node is invalid, or null if no |
| 104 * reason is found, so it's presumed valid. | 105 * reason is found, so it's presumed valid. |
| 105 */ | 106 */ |
| 106 String checkValidity(MethodInvocation node) { | 107 String checkValidity(MethodInvocation node) { |
| 107 // The containing function cannot have named parameters. | 108 // The containing function cannot have named parameters. |
| 108 if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) { | 109 if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) { |
| 109 return "Named parameters on message functions are not supported."; | 110 return "Named parameters on message functions are not supported."; |
| 110 } | 111 } |
| 111 var arguments = node.argumentList.arguments; | 112 var arguments = node.argumentList.arguments; |
| 112 if (!(arguments.first is StringLiteral)) { | 113 |
| 113 return "Intl.message messages must be string literals"; | 114 if (node.methodName.name == 'message') { |
| 115 if (!(arguments.first is StringLiteral)) { |
| 116 return "Intl.message messages must be string literals"; |
| 117 } |
| 114 } | 118 } |
| 119 |
| 115 var namedArguments = arguments.skip(1); | 120 var namedArguments = arguments.skip(1); |
| 116 // This seems unlikely to happen, but make sure all are NamedExpression | 121 // This seems unlikely to happen, but make sure all are NamedExpression |
| 117 // before doing the tests below. | 122 // before doing the tests below. |
| 118 if (!namedArguments.every((each) => each is NamedExpression)) { | 123 if (!namedArguments.every((each) => each is NamedExpression)) { |
| 119 return "Message arguments except the message must be named"; | 124 return "Message arguments except the message must be named"; |
| 120 } | 125 } |
| 121 var notArgs = namedArguments.where( | 126 var notArgs = namedArguments.where( |
| 122 (each) => each.name.label.name != 'args'); | 127 (each) => each.name.label.name != 'args'); |
| 123 var values = notArgs.map((each) => each.expression).toList(); | 128 var values = notArgs.map((each) => each.expression).toList(); |
| 124 if (!values.every((each) => each is SimpleStringLiteral)) { | 129 if (!values.every((each) => each is SimpleStringLiteral)) { |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 160 * encountered before seeing the Intl.message call. | 165 * encountered before seeing the Intl.message call. |
| 161 */ | 166 */ |
| 162 void visitFunctionDeclaration(FunctionDeclaration node) { | 167 void visitFunctionDeclaration(FunctionDeclaration node) { |
| 163 parameters = node.functionExpression.parameters; | 168 parameters = node.functionExpression.parameters; |
| 164 name = node.name.name; | 169 name = node.name.name; |
| 165 super.visitFunctionDeclaration(node); | 170 super.visitFunctionDeclaration(node); |
| 166 } | 171 } |
| 167 | 172 |
| 168 /** | 173 /** |
| 169 * Examine method invocations to see if they look like calls to Intl.message. | 174 * Examine method invocations to see if they look like calls to Intl.message. |
| 175 * If we've found one, stop recursing. This is important because we can have |
| 176 * Intl.message(...Intl.plural...) and we don't want to treat the inner |
| 177 * plural as if it was an outermost message. |
| 170 */ | 178 */ |
| 171 void visitMethodInvocation(MethodInvocation node) { | 179 void visitMethodInvocation(MethodInvocation node) { |
| 172 addIntlMessage(node); | 180 if (!addIntlMessage(node)) { |
| 173 return super.visitNode(node); | 181 return super.visitMethodInvocation(node); |
| 182 } |
| 174 } | 183 } |
| 175 | 184 |
| 176 /** | 185 /** |
| 177 * Check that the node looks like an Intl.message invocation, and create | 186 * Check that the node looks like an Intl.message invocation, and create |
| 178 * the [IntlMessage] object from it and store it in [messages]. | 187 * the [IntlMessage] object from it and store it in [messages]. Return true |
| 188 * if we successfully extracted a message and should stop looking. Return |
| 189 * false if we didn't, so should continue recursing. |
| 179 */ | 190 */ |
| 180 void addIntlMessage(MethodInvocation node) { | 191 bool addIntlMessage(MethodInvocation node) { |
| 181 if (!looksLikeIntlMessage(node)) return; | 192 if (!looksLikeIntlMessage(node)) return false; |
| 182 var reason = checkValidity(node); | 193 var reason = checkValidity(node); |
| 183 if (reason != null && !suppressWarnings) { | 194 if (reason != null) { |
| 184 print("Skipping invalid Intl.message invocation\n <$node>"); | 195 if (!suppressWarnings) { |
| 185 print(" reason: $reason"); | 196 print("Skipping invalid Intl.message invocation\n <$node>"); |
| 186 _reportErrorLocation(node); | 197 print(" reason: $reason"); |
| 187 return; | 198 _reportErrorLocation(node); |
| 199 } |
| 200 // We found one, but it's not valid. Stop recursing. |
| 201 return true; |
| 188 } | 202 } |
| 189 var message = messageFromMethodInvocation(node); | 203 var message; |
| 204 if (node.methodName.name == "message") { |
| 205 message = messageFromIntlMessageCall(node); |
| 206 } else { |
| 207 message = messageFromDirectPluralOrGenderCall(node); |
| 208 } |
| 190 if (message != null) messages[message.name] = message; | 209 if (message != null) messages[message.name] = message; |
| 210 return true; |
| 191 } | 211 } |
| 192 | 212 |
| 193 /** | 213 /** |
| 194 * Create an IntlMessage from [node] using the name and | 214 * Create a MainMessage from [node] using the name and |
| 195 * parameters of the last function/method declaration we encountered | 215 * parameters of the last function/method declaration we encountered, |
| 196 * and the parameters to the Intl.message call. | 216 * and the values we get by calling [extract]. We set those values |
| 217 * by calling [setAttribute]. This is the common parts between |
| 218 * [messageFromIntlMessageCall] and [messageFromDirectPluralOrGenderCall]. |
| 197 */ | 219 */ |
| 198 MainMessage messageFromMethodInvocation(MethodInvocation node) { | 220 MainMessage _messageFromNode(MethodInvocation node, Function extract, |
| 221 Function setAttribute) { |
| 199 var message = new MainMessage(); | 222 var message = new MainMessage(); |
| 200 message.name = name; | 223 message.name = name; |
| 201 message.arguments = parameters.parameters.elements.map( | 224 message.arguments = parameters.parameters.elements.map( |
| 202 (x) => x.identifier.name).toList(); | 225 (x) => x.identifier.name).toList(); |
| 203 var arguments = node.argumentList.arguments.elements; | 226 var arguments = node.argumentList.arguments.elements; |
| 204 try { | 227 extract(message, arguments); |
| 205 var interpolation = new InterpolationVisitor(message); | 228 |
| 206 arguments.first.accept(interpolation); | |
| 207 message.messagePieces.addAll(interpolation.pieces); | |
| 208 } on IntlMessageExtractionException catch (e) { | |
| 209 message = null; | |
| 210 print("Error $e"); | |
| 211 print("Processing <$node>"); | |
| 212 _reportErrorLocation(node); | |
| 213 } | |
| 214 for (NamedExpression namedArgument in arguments.skip(1)) { | 229 for (NamedExpression namedArgument in arguments.skip(1)) { |
| 215 var name = namedArgument.name.label.name; | 230 var name = namedArgument.name.label.name; |
| 216 var exp = namedArgument.expression; | 231 var exp = namedArgument.expression; |
| 217 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); | 232 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); |
| 218 message[name] = string; | 233 setAttribute(message, name, string); |
| 219 } | 234 } |
| 220 return message; | 235 return message; |
| 221 } | 236 } |
| 237 |
| 238 /** |
| 239 * Create a MainMessage from [node] using the name and |
| 240 * parameters of the last function/method declaration we encountered |
| 241 * and the parameters to the Intl.message call. |
| 242 */ |
| 243 MainMessage messageFromIntlMessageCall(MethodInvocation node) { |
| 244 |
| 245 void extractFromIntlCall(MainMessage message, List arguments) { |
| 246 try { |
| 247 var interpolation = new InterpolationVisitor(message); |
| 248 arguments.first.accept(interpolation); |
| 249 message.messagePieces.addAll(interpolation.pieces); |
| 250 } on IntlMessageExtractionException catch (e) { |
| 251 message = null; |
| 252 print("Error $e"); |
| 253 print("Processing <$node>"); |
| 254 _reportErrorLocation(node); |
| 255 } |
| 256 } |
| 257 |
| 258 void setValue(MainMessage message, String fieldName, String fieldValue) { |
| 259 message[fieldName] = fieldValue; |
| 260 } |
| 261 |
| 262 return _messageFromNode(node, extractFromIntlCall, setValue); |
| 263 } |
| 264 |
| 265 /** |
| 266 * Create a MainMessage from [node] using the name and |
| 267 * parameters of the last function/method declaration we encountered |
| 268 * and the parameters to the Intl.plural or Intl.gender call. |
| 269 */ |
| 270 MainMessage messageFromDirectPluralOrGenderCall(MethodInvocation node) { |
| 271 var pluralOrGender; |
| 272 |
| 273 void extractFromPluralOrGender(MainMessage message, _) { |
| 274 var visitor = new PluralAndGenderVisitor(message.messagePieces, message); |
| 275 node.accept(visitor); |
| 276 pluralOrGender = message.messagePieces.last; |
| 277 } |
| 278 |
| 279 void setAttribute(MainMessage msg, String fieldName, String fieldValue) { |
| 280 if (["name", "desc", "examples", "args"].contains(fieldName)) { |
| 281 msg[fieldName] = fieldValue; |
| 282 } else { |
| 283 pluralOrGender[fieldName] = fieldValue; |
| 284 } |
| 285 } |
| 286 return _messageFromNode(node, extractFromPluralOrGender, setAttribute); |
| 287 } |
| 222 } | 288 } |
| 223 | 289 |
| 224 /** | 290 /** |
| 225 * Given an interpolation, find all of its chunks, validate that they are only | 291 * Given an interpolation, find all of its chunks, validate that they are only |
| 226 * simple variable substitutions or else Intl.plural/gender calls, | 292 * simple variable substitutions or else Intl.plural/gender calls, |
| 227 * and keep track of the pieces of text so that other parts | 293 * and keep track of the pieces of text so that other parts |
| 228 * of the program can deal with the simple string sections and the generated | 294 * of the program can deal with the simple string sections and the generated |
| 229 * parts separately. Note that this is a SimpleASTVisitor, so it only | 295 * parts separately. Note that this is a SimpleASTVisitor, so it only |
| 230 * traverses one level of children rather than automatically recursing. If we | 296 * traverses one level of children rather than automatically recursing. If we |
| 231 * find a plural or gender, which requires recursion, we do it with a separate | 297 * find a plural or gender, which requires recursion, we do it with a separate |
| (...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 318 // TODO(alanknight): Provide better errors for malformed expressions. | 384 // TODO(alanknight): Provide better errors for malformed expressions. |
| 319 if (!looksLikePluralOrGender(node.expression)) return; | 385 if (!looksLikePluralOrGender(node.expression)) return; |
| 320 var reason = checkValidity(node.expression); | 386 var reason = checkValidity(node.expression); |
| 321 if (reason != null) throw reason; | 387 if (reason != null) throw reason; |
| 322 var message = messageFromMethodInvocation(node.expression); | 388 var message = messageFromMethodInvocation(node.expression); |
| 323 foundPluralOrGender = true; | 389 foundPluralOrGender = true; |
| 324 pieces.add(message); | 390 pieces.add(message); |
| 325 super.visitInterpolationExpression(node); | 391 super.visitInterpolationExpression(node); |
| 326 } | 392 } |
| 327 | 393 |
| 328 /** Return true if [node] matches the pattern we expect for Intl.message() */ | 394 visitMethodInvocation(MethodInvocation node) { |
| 395 pieces.add(messageFromMethodInvocation(node)); |
| 396 super.visitMethodInvocation(node); |
| 397 } |
| 398 |
| 399 /** Return true if [node] matches the pattern for plural or gender message.*/ |
| 329 bool looksLikePluralOrGender(MethodInvocation node) { | 400 bool looksLikePluralOrGender(MethodInvocation node) { |
| 330 if (!["plural", "gender"].contains(node.methodName.name)) return false; | 401 if (!["plural", "gender"].contains(node.methodName.name)) return false; |
| 331 if (!(node.target is SimpleIdentifier)) return false; | 402 if (!(node.target is SimpleIdentifier)) return false; |
| 332 SimpleIdentifier target = node.target; | 403 SimpleIdentifier target = node.target; |
| 333 if (target.token.toString() != "Intl") return false; | 404 if (target.token.toString() != "Intl") return false; |
| 334 return true; | 405 return true; |
| 335 } | 406 } |
| 336 | 407 |
| 337 /** | 408 /** |
| 338 * Returns a String describing why the node is invalid, or null if no | 409 * Returns a String describing why the node is invalid, or null if no |
| 339 * reason is found, so it's presumed valid. | 410 * reason is found, so it's presumed valid. |
| 340 */ | 411 */ |
| 341 String checkValidity(MethodInvocation node) { | 412 String checkValidity(MethodInvocation node) { |
| 342 // TODO(alanknight): Add reasonable validity checks. | 413 // TODO(alanknight): Add reasonable validity checks. |
| 343 } | 414 } |
| 344 | 415 |
| 345 /** | 416 /** |
| 346 * Create a MainMessage from [node] using the name and | 417 * Create a MainMessage from [node] using the name and |
| 347 * parameters of the last function/method declaration we encountered | 418 * parameters of the last function/method declaration we encountered |
| 348 * and the parameters to the Intl.message call. | 419 * and the parameters to the Intl.message call. |
| 349 */ | 420 */ |
| 350 messageFromMethodInvocation(MethodInvocation node) { | 421 Message messageFromMethodInvocation(MethodInvocation node) { |
| 351 var message; | 422 var message; |
| 352 if (node.methodName.name == "gender") { | 423 if (node.methodName.name == "gender") { |
| 353 message = new Gender(); | 424 message = new Gender(); |
| 354 } else if (node.methodName.name == "plural") { | 425 } else if (node.methodName.name == "plural") { |
| 355 message = new Plural(); | 426 message = new Plural(); |
| 356 } else { | 427 } else { |
| 357 throw new IntlMessageExtractionException("Invalid plural/gender message"); | 428 throw new IntlMessageExtractionException("Invalid plural/gender message"); |
| 358 } | 429 } |
| 359 message.parent = parent; | 430 message.parent = parent; |
| 360 | 431 |
| (...skipping 30 matching lines...) Expand all Loading... |
| 391 */ | 462 */ |
| 392 final String message; | 463 final String message; |
| 393 | 464 |
| 394 /** | 465 /** |
| 395 * Creates a new exception with an optional error [message]. | 466 * Creates a new exception with an optional error [message]. |
| 396 */ | 467 */ |
| 397 const IntlMessageExtractionException([this.message = ""]); | 468 const IntlMessageExtractionException([this.message = ""]); |
| 398 | 469 |
| 399 String toString() => "IntlMessageExtractionException: $message"; | 470 String toString() => "IntlMessageExtractionException: $message"; |
| 400 } | 471 } |
| OLD | NEW |