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

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

Powered by Google App Engine
This is Rietveld 408576698