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

Side by Side Diff: pkg/intl/lib/extract_messages.dart

Issue 18543009: Plurals and Genders (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Fix typo Created 7 years, 5 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 | Annotate | Revision Log
« no previous file with comments | « no previous file | pkg/intl/lib/generate_localized.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 18 matching lines...) Expand all
29 /** 29 /**
30 * If this is true, print warnings for skipped messages. Otherwise, warnings 30 * If this is true, print warnings for skipped messages. Otherwise, warnings
31 * are suppressed. 31 * are suppressed.
32 */ 32 */
33 bool suppressWarnings = false; 33 bool suppressWarnings = false;
34 34
35 /** 35 /**
36 * Parse the source of the Dart program file [file] and return a Map from 36 * Parse the source of the Dart program file [file] and return a Map from
37 * message names to [IntlMessage] instances. 37 * message names to [IntlMessage] instances.
38 */ 38 */
39 Map<String, IntlMessage> parseFile(File file) { 39 Map<String, MainMessage> parseFile(File file) {
40 var unit = parseDartFile(file.path); 40 _root = parseDartFile(file.path);
41 var visitor = new MessageFindingVisitor(unit, file.path); 41 _origin = file.path;
42 unit.accept(visitor); 42 var visitor = new MessageFindingVisitor();
43 _root.accept(visitor);
43 return visitor.messages; 44 return visitor.messages;
44 } 45 }
45 46
46 /** 47 /**
48 * The root of the compilation unit, and the first node we visit. We hold
49 * on to this for error reporting, as it can give us line numbers of other
50 * nodes.
51 */
52 CompilationUnit _root;
53
54 /**
55 * An arbitrary string describing where the source code came from. Most
56 * obviously, this could be a file path. We use this when reporting
57 * invalid messages.
58 */
59 String _origin;
60
61 void _reportErrorLocation(ASTNode node) {
62 if (_origin != null) print(" from $_origin");
63 var info = _root.lineInfo;
64 if (info != null) {
65 var line = info.getLocation(node.offset);
66 print(" line: ${line.lineNumber}, column: ${line.columnNumber}");
67 }
68 }
69
70 /**
47 * This visits the program source nodes looking for Intl.message uses 71 * This visits the program source nodes looking for Intl.message uses
48 * that conform to its pattern and then finding the 72 * that conform to its pattern and then creating the corresponding
73 * IntlMessage objects. We have to find both the enclosing function, and
74 * the Intl.message invocation.
49 */ 75 */
50 class MessageFindingVisitor extends GeneralizingASTVisitor { 76 class MessageFindingVisitor extends GeneralizingASTVisitor {
51 77
52 /** 78 MessageFindingVisitor();
53 * The root of the compilation unit, and the first node we visit. We hold
54 * on to this for error reporting, as it can give us line numbers of other
55 * nodes.
56 */
57 final CompilationUnit root;
58 79
59 /** 80 /**
60 * An arbitrary string describing where the source code came from. Most 81 * Accumulates the messages we have found, keyed by name.
61 * obviously, this could be a file path. We use this when reporting
62 * invalid messages.
63 */ 82 */
64 final String origin; 83 final Map<String, MainMessage> messages = new Map<String, MainMessage>();
65
66 MessageFindingVisitor(this.root, this.origin);
67
68 /**
69 * Accumulates the messages we have found.
70 */
71 final Map<String, IntlMessage> messages = new Map<String, IntlMessage>();
72 84
73 /** 85 /**
74 * We keep track of the data from the last MethodDeclaration, 86 * We keep track of the data from the last MethodDeclaration,
75 * FunctionDeclaration or FunctionExpression that we saw on the way down, 87 * FunctionDeclaration or FunctionExpression that we saw on the way down,
76 * 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.
77 */ 89 */
78 FormalParameterList parameters; 90 FormalParameterList parameters;
79 String name; 91 String name;
80 92
81 /** 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() */
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after
164 /** 176 /**
165 * Check that the node looks like an Intl.message invocation, and create 177 * Check that the node looks like an Intl.message invocation, and create
166 * the [IntlMessage] object from it and store it in [messages]. 178 * the [IntlMessage] object from it and store it in [messages].
167 */ 179 */
168 void addIntlMessage(MethodInvocation node) { 180 void addIntlMessage(MethodInvocation node) {
169 if (!looksLikeIntlMessage(node)) return; 181 if (!looksLikeIntlMessage(node)) return;
170 var reason = checkValidity(node); 182 var reason = checkValidity(node);
171 if (reason != null && !suppressWarnings) { 183 if (reason != null && !suppressWarnings) {
172 print("Skipping invalid Intl.message invocation\n <$node>"); 184 print("Skipping invalid Intl.message invocation\n <$node>");
173 print(" reason: $reason"); 185 print(" reason: $reason");
174 reportErrorLocation(node); 186 _reportErrorLocation(node);
175 return; 187 return;
176 } 188 }
177 var message = messageFromMethodInvocation(node); 189 var message = messageFromMethodInvocation(node);
178 if (message != null) messages[message.name] = message; 190 if (message != null) messages[message.name] = message;
179 } 191 }
180 192
181 /** 193 /**
182 * Create an IntlMessage from [node] using the name and 194 * Create an IntlMessage from [node] using the name and
183 * parameters of the last function/method declaration we encountered 195 * parameters of the last function/method declaration we encountered
184 * and the parameters to the Intl.message call. 196 * and the parameters to the Intl.message call.
185 */ 197 */
186 IntlMessage messageFromMethodInvocation(MethodInvocation node) { 198 MainMessage messageFromMethodInvocation(MethodInvocation node) {
187 var message = new IntlMessage(); 199 var message = new MainMessage();
188 message.name = name; 200 message.name = name;
189 message.arguments = parameters.parameters.elements.map( 201 message.arguments = parameters.parameters.elements.map(
190 (x) => x.identifier.name).toList(); 202 (x) => x.identifier.name).toList();
203 var arguments = node.argumentList.arguments.elements;
191 try { 204 try {
192 node.accept(new MessageVisitor(message)); 205 var interpolation = new InterpolationVisitor(message);
206 arguments.first.accept(interpolation);
207 message.messagePieces.addAll(interpolation.pieces);
193 } on IntlMessageExtractionException catch (e) { 208 } on IntlMessageExtractionException catch (e) {
194 message = null; 209 message = null;
195 print("Error $e"); 210 print("Error $e");
196 print("Processing <$node>"); 211 print("Processing <$node>");
197 reportErrorLocation(node); 212 _reportErrorLocation(node);
213 }
214 for (NamedExpression namedArgument in arguments.skip(1)) {
215 var name = namedArgument.name.label.name;
216 var exp = namedArgument.expression;
217 var string = exp is SimpleStringLiteral ? exp.value : exp.toString();
218 message[name] = string;
198 } 219 }
199 return message; 220 return message;
200 } 221 }
201
202 void reportErrorLocation(ASTNode node) {
203 if (origin != null) print(" from $origin");
204 var info = root.lineInfo;
205 if (info != null) {
206 var line = info.getLocation(node.offset);
207 print(" line: ${line.lineNumber}, column: ${line.columnNumber}");
208 }
209 }
210 }
211
212 /**
213 * Given a node that looks like an invocation of Intl.message, extract out
214 * the message and the parameters and store them in [target].
215 */
216 class MessageVisitor extends GeneralizingASTVisitor {
217 IntlMessage target;
218
219 MessageVisitor(IntlMessage this.target);
220
221 /**
222 * Extract out the message string. If it's an interpolation, turn it into
223 * a single string with interpolation characters.
224 */
225 void visitArgumentList(ArgumentList node) {
226 var interpolation = new InterpolationVisitor(target);
227 node.arguments.elements.first.accept(interpolation);
228 target.messagePieces = interpolation.pieces;
229 super.visitArgumentList(node);
230 }
231
232 /**
233 * Find the values of all the named arguments, remove quotes, and save them
234 * into [target].
235 */
236 void visitNamedExpression(NamedExpression node) {
237 var name = node.name.label.name;
238 var exp = node.expression;
239 var string = exp is SimpleStringLiteral ? exp.value : exp.toString();
240 target[name] = string;
241 super.visitNamedExpression(node);
242 }
243 } 222 }
244 223
245 /** 224 /**
246 * Given an interpolation, find all of its chunks, validate that they are only 225 * Given an interpolation, find all of its chunks, validate that they are only
247 * simple interpolations, and keep track of the chunks so that other parts 226 * simple variable substitutions or else Intl.plural/gender calls,
248 * of the program can deal with the interpolations and the simple string 227 * and keep track of the pieces of text so that other parts
249 * sections separately. 228 * 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
230 * 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
232 * special-purpose visitor.
250 */ 233 */
251 class InterpolationVisitor extends GeneralizingASTVisitor { 234 class InterpolationVisitor extends SimpleASTVisitor {
252 IntlMessage message; 235 Message message;
253 236
254 InterpolationVisitor(this.message); 237 InterpolationVisitor(this.message);
255 238
256 List pieces = []; 239 List pieces = [];
257 String get extractedMessage => pieces.join(); 240 String get extractedMessage => pieces.join();
258 241
242 void visitAdjacentStrings(AdjacentStrings node) {
243 node.visitChildren(this);
244 super.visitAdjacentStrings(node);
245 }
246
247 void visitStringInterpolation(StringInterpolation node) {
248 node.visitChildren(this);
249 super.visitStringInterpolation(node);
250 }
251
259 void visitSimpleStringLiteral(SimpleStringLiteral node) { 252 void visitSimpleStringLiteral(SimpleStringLiteral node) {
260 pieces.add(node.value); 253 pieces.add(node.value);
261 super.visitSimpleStringLiteral(node); 254 super.visitSimpleStringLiteral(node);
262 } 255 }
263 256
264 void visitInterpolationString(InterpolationString node) { 257 void visitInterpolationString(InterpolationString node) {
265 pieces.add(node.value); 258 pieces.add(node.value);
266 super.visitInterpolationString(node); 259 super.visitInterpolationString(node);
267 } 260 }
268 261
269 // TODO(alanknight): The limitation to simple identifiers is important
270 // to avoid letting translators write arbitrary code, but is a problem
271 // for plurals.
272 void visitInterpolationExpression(InterpolationExpression node) { 262 void visitInterpolationExpression(InterpolationExpression node) {
273 if (node.expression is! SimpleIdentifier) { 263 if (node.expression is SimpleIdentifier) {
264 return handleSimpleInterpolation(node);
265 } else {
266 return lookForPluralOrGender(node);
267 }
268 // Note that we never end up calling super.
269 }
270
271 lookForPluralOrGender(InterpolationExpression node) {
272 var visitor = new PluralAndGenderVisitor(pieces, message);
273 node.accept(visitor);
274 if (!visitor.foundPluralOrGender) {
274 throw new IntlMessageExtractionException( 275 throw new IntlMessageExtractionException(
275 "Only simple identifiers are allowed in message " 276 "Only simple identifiers and Intl.plural/gender/select expressions "
276 "interpolation expressions.\nError at $node"); 277 "are allowed in message "
278 "interpolation expressions.\nError at $node");
277 } 279 }
280 }
281
282 void handleSimpleInterpolation(InterpolationExpression node) {
278 var index = arguments.indexOf(node.expression.toString()); 283 var index = arguments.indexOf(node.expression.toString());
279 if (index == -1) { 284 if (index == -1) {
280 throw new IntlMessageExtractionException( 285 throw new IntlMessageExtractionException(
281 "Cannot find argument ${node.expression}"); 286 "Cannot find argument ${node.expression}");
282 } 287 }
283 pieces.add(index); 288 pieces.add(index);
284 super.visitInterpolationExpression(node);
285 } 289 }
286 290
287 List get arguments => message.arguments; 291 List get arguments => message.arguments;
288 } 292 }
289 293
290 /** 294 /**
295 * A visitor to extract information from Intl.plural/gender sends. Note that
296 * this is a SimpleASTVisitor, so it doesn't automatically recurse. So this
297 * needs to be called where we expect a plural or gender immediately below.
298 */
299 class PluralAndGenderVisitor extends SimpleASTVisitor {
300 /**
301 * A plural or gender always exists in the context of a parent message,
302 * which could in turn also be a plural or gender.
303 */
304 ComplexMessage parent;
305
306 /**
307 * The pieces of the message. We are given an initial version of this
308 * from our parent and we add to it as we find additional information.
309 */
310 List pieces;
311
312 PluralAndGenderVisitor(this.pieces, this.parent) : super() {}
313
314 /** This will be set to true if we find a plural or gender. */
315 bool foundPluralOrGender = false;
316
317 visitInterpolationExpression(InterpolationExpression node) {
318 // TODO(alanknight): Provide better errors for malformed expressions.
319 if (!looksLikePluralOrGender(node.expression)) return;
320 var reason = checkValidity(node.expression);
321 if (reason != null) throw reason;
322 var message = messageFromMethodInvocation(node.expression);
323 foundPluralOrGender = true;
324 pieces.add(message);
325 super.visitInterpolationExpression(node);
326 }
327
328 /** Return true if [node] matches the pattern we expect for Intl.message() */
329 bool looksLikePluralOrGender(MethodInvocation node) {
330 if (!["plural", "gender"].contains(node.methodName.name)) return false;
331 if (!(node.target is SimpleIdentifier)) return false;
332 SimpleIdentifier target = node.target;
333 if (target.token.toString() != "Intl") return false;
334 return true;
335 }
336
337 /**
338 * Returns a String describing why the node is invalid, or null if no
339 * reason is found, so it's presumed valid.
340 */
341 String checkValidity(MethodInvocation node) {
342 // TODO(alanknight): Add reasonable validity checks.
343 }
344
345 /**
346 * Create a MainMessage from [node] using the name and
347 * parameters of the last function/method declaration we encountered
348 * and the parameters to the Intl.message call.
349 */
350 messageFromMethodInvocation(MethodInvocation node) {
351 var message;
352 if (node.methodName.name == "gender") {
353 message = new Gender();
354 } else if (node.methodName.name == "plural") {
355 message = new Plural();
356 } else {
357 throw new IntlMessageExtractionException("Invalid plural/gender message");
358 }
359 message.parent = parent;
360
361 var arguments = node.argumentList.arguments.elements;
362 for (var arg in arguments.where((each) => each is NamedExpression)) {
363 try {
364 var interpolation = new InterpolationVisitor(message);
365 arg.expression.accept(interpolation);
366 message[arg.name.label.token.toString()] = interpolation.pieces;
367 } on IntlMessageExtractionException catch (e) {
368 message = null;
369 print("Error $e");
370 print("Processing <$node>");
371 _reportErrorLocation(node);
372 }
373 }
374 var mainArg = node.argumentList.arguments.elements.firstWhere(
375 (each) => each is! NamedExpression);
376 if (mainArg is SimpleStringLiteral) {
377 message.mainArgument = mainArg.toString();
378 } else {
379 message.mainArgument = mainArg.name;
380 }
381 return message;
382 }
383 }
384
385 /**
291 * Exception thrown when we cannot process a message properly. 386 * Exception thrown when we cannot process a message properly.
292 */ 387 */
293 class IntlMessageExtractionException implements Exception { 388 class IntlMessageExtractionException implements Exception {
294 /** 389 /**
295 * A message describing the error. 390 * A message describing the error.
296 */ 391 */
297 final String message; 392 final String message;
298 393
299 /** 394 /**
300 * Creates a new exception with an optional error [message]. 395 * Creates a new exception with an optional error [message].
301 */ 396 */
302 const IntlMessageExtractionException([this.message = ""]); 397 const IntlMessageExtractionException([this.message = ""]);
303 398
304 String toString() => "IntlMessageExtractionException: $message"; 399 String toString() => "IntlMessageExtractionException: $message";
305 } 400 }
OLDNEW
« no previous file with comments | « no previous file | pkg/intl/lib/generate_localized.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698