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

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

Issue 12733003: Adds facilities for extracting Intl.message calls and generating code from translations (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 9 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
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 /**
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
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
10 * [IntlMessage] class that holds the extracted data and [parseString]
11 * and [parseFile] methods which
12 * can extract messages that conform to the expected pattern:
13 * (parameters) => Intl.message("Message $parameters", desc: ...);
14 * It uses the analyzer_experimental package to do the parsing, so may
15 * break if there are changes to the API that provides.
Emily Fortuna 2013/03/13 18:54:43 typo: "that it"
Alan Knight 2013/03/14 17:49:05 Done.
16 * An example can be found in test/message_extraction/extract_to_json.dart
17 *
18 * Note that this does not understand how to follow part directives, so it
19 * has to explicitly be given all the files that it needs. A typical use case
20 * is to run it on all .dart files in a directory.
21 */
22 library extract_messages;
23
24 import 'package:analyzer-experimental/src/generated/ast.dart';
25 import 'package:analyzer-experimental/src/generated/error.dart';
26 import 'package:analyzer-experimental/src/generated/java_core.dart';
27 import 'package:analyzer-experimental/src/generated/parser.dart';
28 import 'package:analyzer-experimental/src/generated/scanner.dart';
29 import 'package:analyzer-experimental/src/generated/source.dart';
30 import 'package:analyzer-experimental/src/generated/utilities_dart.dart';
31 import 'dart:io';
32 import 'package:intl/src/intl_message.dart';
33
34 /**
35 * Parse the dart program represented in [sourceCode] and return a Map from
36 * message names to [IntlMessage] instances. The [origin] is a string
37 * describing where the source came from, and is used in error messages.
38 */
39 Map<String, IntlMessage> parseString(String sourceCode, [String origin]) {
40 var errorListener = new _ErrorCollector();
41 var scanner = new StringScanner(null, sourceCode, errorListener);
42 var token = scanner.tokenize();
43 var parser = new Parser(null, errorListener);
44 var unit = parser.parseCompilationUnit(token);
45 unit.lineInfo = new LineInfo(scanner.lineStarts);
46
47 var visitor = new MessageFindingVisitor(unit, origin);
48 unit.accept(visitor);
49 for (var error in errorListener.errors) {
50 print(error);
51 }
52 return visitor.messages;
53 }
54
55 /**
56 * Parse the source of the Dart program file [file] and return a Map from
57 * message names to [IntlMessage] instances.
58 */
59 Map<String, IntlMessage> parseFile(File file) {
60 var sourceCode = file.readAsStringSync();
61 return parseString(sourceCode, file.path);
62 }
63
64 /**
65 * An error handler for parsing. Error handling is currently very primitive.
66 */
67 class _ErrorCollector extends AnalysisErrorListener {
68 List<AnalysisError> errors;
69 _ErrorCollector() : errors = new List<AnalysisError>();
70 onError(error) => errors.add(error);
71 }
72
73 /**
74 * This visits the program source nodes looking for Intl.message uses
75 * that conform to its pattern and then finding the
76 */
77 class MessageFindingVisitor extends GeneralizingASTVisitor {
78
79 /**
80 * The root of the compilation unit, and the first node we visit. We hold
81 * on to this for error reporting, as it can give us line numbers of other
82 * nodes.
83 */
84 final CompilationUnit root;
85
86 /**
87 * An arbitrary string describing where the source code came from. Most
88 * obviously, this could be a file path. We use this when reporting
89 * invalid messages.
90 */
91 final String origin;
92
93 MessageFindingVisitor(this.root, this.origin);
94
95 /**
96 * Accumulates the messages we have found.
97 */
98 final messages = new Map<String, IntlMessage>();
99
100 /**
101 * We keep track of the data from the last MethodDeclaration,
102 * FunctionDeclaration or FunctionExpression that we saw on the way down,
103 * as that will be the nearest parent of the Intl.message invocation.
104 */
105 FormalParameterList parameters;
106 String name;
107
108 /** Return true if [node] matches the pattern we expect for Intl.message() */
109 bool looksLikeIntlMessage(MethodInvocation node) {
110 if (node.methodName.name != "message") return false;
111 if (!(node.target is SimpleIdentifier)) return false;
112 SimpleIdentifier target = node.target;
113 if (target.token.toString() != "Intl") return false;
114 return true;
115 }
116
117 /**
118 * Returns a String describing why the node is invalid, or null if no
119 * reason is found, so it's presumed valid.
120 */
121 String checkValidity(MethodInvocation node) {
122 // The containing function cannot have named parameters.
123 if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) {
124 return "Named parameters on message functions are not supported.";
125 }
126 var arguments = node.argumentList.arguments;
127 if (!(arguments.first is StringLiteral)) {
128 return "Intl.message messages must be string literals";
129 }
130 var namedArguments = arguments.skip(1);
131 // This seems unlikely to happen, but make sure all are NamedExpression
132 // before doing the tests below.
133 if (!namedArguments.every((each) => each is NamedExpression)) {
134 return "Message arguments except the message must be named";
135 }
136 var notArgs = namedArguments.where(
137 (each) => each.name.label.name != 'args');
138 var values = notArgs.map((each) => each.expression).toList();
139 if (!values.every((each) => each is SimpleStringLiteral)) {
140 "Intl.message arguments must be simple string literals";
141 }
142 if (!notArgs.any((each) => each.name.label.name == 'name')) {
143 return "The 'name' argument for Intl.message must be specified";
144 }
145 var hasArgs = namedArguments.any((each) => each.name.label.name == 'args');
146 var hasParameters = !parameters.parameters.isEmpty;
147 if (!hasArgs && hasParameters) {
148 return "The 'args' argument for Intl.message must be specified";
149 }
150 return null;
151 }
152
153 /**
154 * Record the parameters of the function or method declaration we last
155 * encountered before seeing the Intl.message call.
156 */
157 visitMethodDeclaration(MethodDeclaration node) {
158 parameters = node.parameters;
159 String name = node.name.name;
160 super.visitMethodDeclaration(node);
161 }
162
163 /**
164 * Record the parameters of the function or method declaration we last
165 * encountered before seeing the Intl.message call.
166 */
167 visitFunctionExpression(FunctionExpression node) {
168 parameters = node.parameters;
169 name = null;
170 super.visitFunctionExpression(node);
171 }
172
173 /**
174 * Record the parameters of the function or method declaration we last
175 * encountered before seeing the Intl.message call.
176 */
177 visitFunctionDeclaration(FunctionDeclaration node) {
178 parameters = node.functionExpression.parameters;
179 name = node.name.name;
180 super.visitFunctionDeclaration(node);
181 }
182
183 /**
184 * Examine method invocations to see if they look like calls to Intl.message.
185 */
186 visitMethodInvocation(MethodInvocation node) {
187 addIntlMessage(node);
188 return super.visitNode(node);
189 }
190
191 /**
192 * Check that the node looks like an Intl.message invocation, and create
193 * the [IntlMessage] object from it and store it in [messages].
194 */
195 addIntlMessage(MethodInvocation node) {
196 if (!looksLikeIntlMessage(node)) return;
197 var reason = checkValidity(node);
198 if (!(reason == null)) {
199 print("Skipping invalid Intl.message invocation\n <$node>");
200 print(" reason: $reason");
201 reportErrorLocation(node);
202 return;
203 }
204 var message = messageFromMethodInvocation(node);
205 if (message != null) messages[message.name] = message;
206 }
207
208 /**
209 * Create an IntlMessage from [node] using the name and
210 * parameters of the last function/method declaration we encountered
211 * and the parameters to the Intl.message call.
212 */
213 messageFromMethodInvocation(MethodInvocation node) {
214 var message = new IntlMessage();
215 message.name = name;
216 message.arguments = parameters.parameters.elements.map(
217 (x) => x.identifier.name).toList();
218 try {
219 node.accept(new MessageVisitor(message));
220 } on IntlMessageExtractionException catch (e) {
221 message = null;
222 print("Error $e");
223 print("Processing <$node>");
224 reportErrorLocation(node);
225 }
226 return message;
227 }
228
229 reportErrorLocation(ASTNode node) {
230 if (origin != null) print("from $origin");
231 LineInfo info = root.lineInfo;
232 if (info != null) {
233 LineInfo_Location line = info.getLocation(node.offset);
234 print("line: ${line.lineNumber}, column: ${line.columnNumber}");
235 }
236 }
237 }
238
239 /**
240 * Given a node that looks like an invocation of Intl.message, extract out
241 * the message and the parameters and store them in [target].
242 */
243 class MessageVisitor extends GeneralizingASTVisitor {
244 IntlMessage target;
245
246 MessageVisitor(IntlMessage this.target);
247
248 /**
249 * Extract out the message string. If it's an interpolation, turn it into
250 * a single string with interpolation characters.
251 */
252 visitArgumentList(ArgumentList node) {
253 var interpolation = new InterpolationVisitor(target);
254 node.arguments.elements.first.accept(interpolation);
255 target.messagePieces = interpolation.pieces;
256 super.visitArgumentList(node);
257 }
258
259 /**
260 * Find the values of all the named arguments, remove quotes, and save them
261 * into [target].
262 */
263 visitNamedExpression(NamedExpression node) {
264 var name = node.name.label.name;
265 var exp = node.expression;
266 var string = exp is SimpleStringLiteral ? exp.value : exp.toString();
267 target[name] = string;
268 super.visitNamedExpression(node);
269 }
270 }
271
272 /**
273 * Given an interpolation, find all of its chunks, validate that they are only
274 * simple interpolations, and keep track of the chunks so that other parts
275 * of the program can deal with the interpolations and the simple string
276 * sections separately.
277 */
278 class InterpolationVisitor extends GeneralizingASTVisitor {
279 IntlMessage message;
280
281 InterpolationVisitor(this.message);
282
283 List pieces = [];
284 String get extractedMessage => pieces.join();
285
286 visitSimpleStringLiteral(SimpleStringLiteral node) {
287 pieces.add(node.value);
288 super.visitSimpleStringLiteral(node);
289 }
290
291 visitInterpolationString(InterpolationString node) {
292 pieces.add(node.value);
293 super.visitInterpolationString(node);
294 }
295
296 // TODO(alanknight): The limitation to simple identifiers is important
297 // to avoid letting translators write arbitrary code, but is a problem
298 // for plurals.
299 visitInterpolationExpression(InterpolationExpression node) {
300 if (node.expression is! SimpleIdentifier) {
301 throw new IntlMessageExtractionException(
302 "Only simple identifiers are allowed in message "
303 "interpolation expressions.\nError at $node");
304 }
305 var index = arguments.indexOf(node.expression.toString());
306 if (index == -1) {
307 throw new IntlMessageExtractionException(
308 "Cannot find argument ${node.expression}");
309 }
310 pieces.add(index);
311 super.visitInterpolationExpression(node);
312 }
313
314 List get arguments => message.arguments;
315 }
316
317 /**
318 * Exception thrown when we cannot process a message properly.
319 */
320 class IntlMessageExtractionException implements Exception {
321 /**
322 * A message describing the error.
323 */
324 final String message;
325
326 /**
327 * Creates a new exception with an optional error [message].
328 */
329 const IntlMessageExtractionException([this.message = ""]);
330
331 String toString() => "IntlMessageExtractionException: $message";
332 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698