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

Side by Side Diff: grit/tool/android2grd.py

Issue 1442863002: Remove contents of grit's SVN repository. (Closed) Base URL: http://grit-i18n.googlecode.com/svn/trunk/
Patch Set: Created 5 years, 1 month 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 | « grit/tool/__init__.py ('k') | grit/tool/android2grd_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """The 'grit android2grd' tool."""
7
8
9 import getopt
10 import os.path
11 import StringIO
12 from xml.dom import Node
13 import xml.dom.minidom
14
15 import grit.node.empty
16 from grit.node import io
17 from grit.node import message
18
19 from grit.tool import interface
20
21 from grit import grd_reader
22 from grit import lazy_re
23 from grit import tclib
24 from grit import util
25
26
27 # The name of a string in strings.xml
28 _STRING_NAME = lazy_re.compile(r'[a-z0-9_]+\Z')
29
30 # A string's character limit in strings.xml
31 _CHAR_LIMIT = lazy_re.compile(r'\[CHAR-LIMIT=(\d+)\]')
32
33 # Finds String.Format() style format specifiers such as "%-5.2f".
34 _FORMAT_SPECIFIER = lazy_re.compile(
35 '%'
36 '([1-9][0-9]*\$|<)?' # argument_index
37 '([-#+ 0,(]*)' # flags
38 '([0-9]+)?' # width
39 '(\.[0-9]+)?' # precision
40 '([bBhHsScCdoxXeEfgGaAtT%n])') # conversion
41
42
43 class Android2Grd(interface.Tool):
44 """Tool for converting Android string.xml files into chrome Grd files.
45
46 Usage: grit [global options] android2grd [OPTIONS] STRINGS_XML
47
48 The Android2Grd tool will convert an Android strings.xml file (whose path is
49 specified by STRINGS_XML) and create a chrome style grd file containing the
50 relevant information.
51
52 Because grd documents are much richer than strings.xml documents we supplement
53 the information required by grds using OPTIONS with sensible defaults.
54
55 OPTIONS may be any of the following:
56
57 --name FILENAME Specify the base FILENAME. This should be without
58 any file type suffix. By default
59 "chrome_android_strings" will be used.
60
61 --languages LANGUAGES Comma separated list of ISO language codes (e.g.
62 en-US, en-GB, ru, zh-CN). These codes will be used
63 to determine the names of resource and translations
64 files that will be declared by the output grd file.
65
66 --grd-dir GRD_DIR Specify where the resultant grd file
67 (FILENAME.grd) should be output. By default this
68 will be the present working directory.
69
70 --header-dir HEADER_DIR Specify the location of the directory where grit
71 generated C++ headers (whose name will be
72 FILENAME.h) will be placed. Use an empty string to
73 disable rc generation. Default: empty.
74
75 --rc-dir RC_DIR Specify the directory where resource files will
76 be located relative to grit build's output
77 directory. Use an empty string to disable rc
78 generation. Default: empty.
79
80 --xml-dir XML_DIR Specify where to place localized strings.xml files
81 relative to grit build's output directory. For each
82 language xx a values-xx/strings.xml file will be
83 generated. Use an empty string to disable
84 strings.xml generation. Default: '.'.
85
86 --xtb-dir XTB_DIR Specify where the xtb files containing translations
87 will be located relative to the grd file. Default:
88 '.'.
89 """
90
91 _NAME_FLAG = 'name'
92 _LANGUAGES_FLAG = 'languages'
93 _GRD_DIR_FLAG = 'grd-dir'
94 _RC_DIR_FLAG = 'rc-dir'
95 _HEADER_DIR_FLAG = 'header-dir'
96 _XTB_DIR_FLAG = 'xtb-dir'
97 _XML_DIR_FLAG = 'xml-dir'
98
99 def __init__(self):
100 self.name = 'chrome_android_strings'
101 self.languages = []
102 self.grd_dir = '.'
103 self.rc_dir = None
104 self.xtb_dir = '.'
105 self.xml_res_dir = '.'
106 self.header_dir = None
107
108 def ShortDescription(self):
109 """Returns a short description of the Android2Grd tool.
110
111 Overridden from grit.interface.Tool
112
113 Returns:
114 A string containing a short description of the android2grd tool.
115 """
116 return 'Converts Android string.xml files into Chrome grd files.'
117
118 def ParseOptions(self, args):
119 """Set this objects and return all non-option arguments."""
120 flags = [
121 Android2Grd._NAME_FLAG,
122 Android2Grd._LANGUAGES_FLAG,
123 Android2Grd._GRD_DIR_FLAG,
124 Android2Grd._RC_DIR_FLAG,
125 Android2Grd._HEADER_DIR_FLAG,
126 Android2Grd._XTB_DIR_FLAG,
127 Android2Grd._XML_DIR_FLAG, ]
128 (opts, args) = getopt.getopt(args, None, ['%s=' % o for o in flags])
129
130 for key, val in opts:
131 # Get rid of the preceding hypens.
132 k = key[2:]
133 if k == Android2Grd._NAME_FLAG:
134 self.name = val
135 elif k == Android2Grd._LANGUAGES_FLAG:
136 self.languages = val.split(',')
137 elif k == Android2Grd._GRD_DIR_FLAG:
138 self.grd_dir = val
139 elif k == Android2Grd._RC_DIR_FLAG:
140 self.rc_dir = val
141 elif k == Android2Grd._HEADER_DIR_FLAG:
142 self.header_dir = val
143 elif k == Android2Grd._XTB_DIR_FLAG:
144 self.xtb_dir = val
145 elif k == Android2Grd._XML_DIR_FLAG:
146 self.xml_res_dir = val
147 return args
148
149 def Run(self, opts, args):
150 """Runs the Android2Grd tool.
151
152 Inherited from grit.interface.Tool.
153
154 Args:
155 opts: List of string arguments that should be parsed.
156 args: String containing the path of the strings.xml file to be converted.
157 """
158 args = self.ParseOptions(args)
159 if len(args) != 1:
160 print ('Tool requires one argument, the path to the Android '
161 'strings.xml resource file to be converted.')
162 return 2
163 self.SetOptions(opts)
164
165 android_path = args[0]
166
167 # Read and parse the Android strings.xml file.
168 with open(android_path) as android_file:
169 android_dom = xml.dom.minidom.parse(android_file)
170
171 # Do the hard work -- convert the Android dom to grd file contents.
172 grd_dom = self.AndroidDomToGrdDom(android_dom)
173 grd_string = unicode(grd_dom)
174
175 # Write the grd string to a file in grd_dir.
176 grd_filename = self.name + '.grd'
177 grd_path = os.path.join(self.grd_dir, grd_filename)
178 with open(grd_path, 'w') as grd_file:
179 grd_file.write(grd_string)
180
181 def AndroidDomToGrdDom(self, android_dom):
182 """Converts a strings.xml DOM into a DOM representing the contents of
183 a grd file.
184
185 Args:
186 android_dom: A xml.dom.Document containing the contents of the Android
187 string.xml document.
188 Returns:
189 The DOM for the grd xml document produced by converting the Android DOM.
190 """
191
192 # Start with a basic skeleton for the .grd file.
193 root = grd_reader.Parse(StringIO.StringIO(
194 '''<?xml version="1.0" encoding="UTF-8"?>
195 <grit base_dir="." latest_public_release="0"
196 current_release="1" source_lang_id="en">
197 <outputs />
198 <translations />
199 <release allow_pseudo="false" seq="1">
200 <messages fallback_to_english="true" />
201 </release>
202 </grit>'''), dir='.')
203 outputs = root.children[0]
204 translations = root.children[1]
205 messages = root.children[2].children[0]
206 assert (isinstance(messages, grit.node.empty.MessagesNode) and
207 isinstance(translations, grit.node.empty.TranslationsNode) and
208 isinstance(outputs, grit.node.empty.OutputsNode))
209
210 if self.header_dir:
211 cpp_header = self.__CreateCppHeaderOutputNode(outputs, self.header_dir)
212 for lang in self.languages:
213 # Create an output element for each language.
214 if self.rc_dir:
215 self.__CreateRcOutputNode(outputs, lang, self.rc_dir)
216 if self.xml_res_dir:
217 self.__CreateAndroidXmlOutputNode(outputs, lang, self.xml_res_dir)
218 if lang != 'en':
219 self.__CreateFileNode(translations, lang)
220 # Convert all the strings.xml strings into grd messages.
221 self.__CreateMessageNodes(messages, android_dom.documentElement)
222
223 return root
224
225 def __CreateMessageNodes(self, messages, resources):
226 """Creates the <message> elements and adds them as children of <messages>.
227
228 Args:
229 messages: the <messages> element in the strings.xml dom.
230 resources: the <resources> element in the grd dom.
231 """
232 # <string> elements contain the definition of the resource.
233 # The description of a <string> element is contained within the comment
234 # node element immediately preceeding the string element in question.
235 description = ''
236 for child in resources.childNodes:
237 if child.nodeType == Node.COMMENT_NODE:
238 # Remove leading/trailing whitespace; collapse consecutive whitespaces.
239 description = ' '.join(child.data.split())
240 elif child.nodeType == Node.ELEMENT_NODE:
241 if child.tagName != 'string':
242 print 'Warning: ignoring unknown tag <%s>' % child.tagName
243 else:
244 translatable = self.IsTranslatable(child)
245 raw_name = child.getAttribute('name')
246 if not _STRING_NAME.match(raw_name):
247 print 'Error: illegal string name: %s' % raw_name
248 grd_name = 'IDS_' + raw_name.upper()
249 # Transform the <string> node contents into a tclib.Message, taking
250 # care to handle whitespace transformations and escaped characters,
251 # and coverting <xliff:g> placeholders into <ph> placeholders.
252 msg = self.CreateTclibMessage(child)
253 msg_node = self.__CreateMessageNode(messages, grd_name, description,
254 msg, translatable)
255 messages.AddChild(msg_node)
256 # Reset the description once a message has been parsed.
257 description = ''
258
259 def CreateTclibMessage(self, android_string):
260 """Transforms a <string/> element from strings.xml into a tclib.Message.
261
262 Interprets whitespace, quotes, and escaped characters in the android_string
263 according to Android's formatting and styling rules for strings. Also
264 converts <xliff:g> placeholders into <ph> placeholders, e.g.:
265
266 <xliff:g id="website" example="google.com">%s</xliff:g>
267 becomes
268 <ph name="website"><ex>google.com</ex>%s</ph>
269
270 Returns:
271 The tclib.Message.
272 """
273 msg = tclib.Message()
274 current_text = '' # Accumulated text that hasn't yet been added to msg.
275 nodes = android_string.childNodes
276
277 for i, node in enumerate(nodes):
278 # Handle text nodes.
279 if node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
280 current_text += node.data
281
282 # Handle <xliff:g> and other tags.
283 elif node.nodeType == Node.ELEMENT_NODE:
284 if node.tagName == 'xliff:g':
285 assert node.hasAttribute('id'), 'missing id: ' + node.data()
286 placeholder_id = node.getAttribute('id')
287 placeholder_text = self.__FormatPlaceholderText(node)
288 placeholder_example = node.getAttribute('example')
289 if not placeholder_example:
290 print ('Info: placeholder does not contain an example: %s' %
291 node.toxml())
292 placeholder_example = placeholder_id.upper()
293 msg.AppendPlaceholder(tclib.Placeholder(placeholder_id,
294 placeholder_text, placeholder_example))
295 else:
296 print ('Warning: removing tag <%s> which must be inside a '
297 'placeholder: %s' % (node.tagName, node.toxml()))
298 msg.AppendText(self.__FormatPlaceholderText(node))
299
300 # Handle other nodes.
301 elif node.nodeType != Node.COMMENT_NODE:
302 assert False, 'Unknown node type: %s' % node.nodeType
303
304 is_last_node = (i == len(nodes) - 1)
305 if (current_text and
306 (is_last_node or nodes[i + 1].nodeType == Node.ELEMENT_NODE)):
307 # For messages containing just text and comments (no xml tags) Android
308 # strips leading and trailing whitespace. We mimic that behavior.
309 if not msg.GetContent() and is_last_node:
310 current_text = current_text.strip()
311 msg.AppendText(self.__FormatAndroidString(current_text))
312 current_text = ''
313
314 return msg
315
316 def __FormatAndroidString(self, android_string, inside_placeholder=False):
317 r"""Returns android_string formatted for a .grd file.
318
319 * Collapses consecutive whitespaces, except when inside double-quotes.
320 * Replaces \\, \n, \t, \", \' with \, newline, tab, ", '.
321 """
322 backslash_map = {'\\' : '\\', 'n' : '\n', 't' : '\t', '"' : '"', "'" : "'"}
323 is_quoted_section = False # True when we're inside double quotes.
324 is_backslash_sequence = False # True after seeing an unescaped backslash.
325 prev_char = ''
326 output = []
327 for c in android_string:
328 if is_backslash_sequence:
329 # Unescape \\, \n, \t, \", and \'.
330 assert c in backslash_map, 'Illegal escape sequence: \\%s' % c
331 output.append(backslash_map[c])
332 is_backslash_sequence = False
333 elif c == '\\':
334 is_backslash_sequence = True
335 elif c.isspace() and not is_quoted_section:
336 # Turn whitespace into ' ' and collapse consecutive whitespaces.
337 if not prev_char.isspace():
338 output.append(' ')
339 elif c == '"':
340 is_quoted_section = not is_quoted_section
341 else:
342 output.append(c)
343 prev_char = c
344 output = ''.join(output)
345
346 if is_quoted_section:
347 print 'Warning: unbalanced quotes in string: %s' % android_string
348
349 if is_backslash_sequence:
350 print 'Warning: trailing backslash in string: %s' % android_string
351
352 # Check for format specifiers outside of placeholder tags.
353 if not inside_placeholder:
354 format_specifier = _FORMAT_SPECIFIER.search(output)
355 if format_specifier:
356 print ('Warning: format specifiers are not inside a placeholder '
357 '<xliff:g/> tag: %s' % output)
358
359 return output
360
361 def __FormatPlaceholderText(self, placeholder_node):
362 """Returns the text inside of an <xliff:g> placeholder node."""
363 text = []
364 for childNode in placeholder_node.childNodes:
365 if childNode.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
366 text.append(childNode.data)
367 elif childNode.nodeType != Node.COMMENT_NODE:
368 assert False, 'Unknown node type in ' + placeholder_node.toxml()
369 return self.__FormatAndroidString(''.join(text), inside_placeholder=True)
370
371 def __CreateMessageNode(self, messages_node, grd_name, description, msg,
372 translatable):
373 """Creates and initializes a <message> element.
374
375 Message elements correspond to Android <string> elements in that they
376 declare a string resource along with a programmatic id.
377 """
378 if not description:
379 print 'Warning: no description for %s' % grd_name
380 # Check that we actually fit within the character limit we've specified.
381 match = _CHAR_LIMIT.search(description)
382 if match:
383 char_limit = int(match.group(1))
384 msg_content = msg.GetRealContent()
385 if len(msg_content) > char_limit:
386 print ('Warning: char-limit for %s is %d, but length is %d: %s' %
387 (grd_name, char_limit, len(msg_content), msg_content))
388 return message.MessageNode.Construct(parent=messages_node,
389 name=grd_name,
390 message=msg,
391 desc=description,
392 translateable=translatable)
393
394 def __CreateFileNode(self, translations_node, lang):
395 """Creates and initializes the <file> elements.
396
397 File elements provide information on the location of translation files
398 (xtbs)
399 """
400 xtb_file = os.path.normpath(os.path.join(
401 self.xtb_dir, '%s_%s.xtb' % (self.name, lang)))
402 fnode = io.FileNode()
403 fnode.StartParsing(u'file', translations_node)
404 fnode.HandleAttribute('path', xtb_file)
405 fnode.HandleAttribute('lang', lang)
406 fnode.EndParsing()
407 translations_node.AddChild(fnode)
408 return fnode
409
410 def __CreateCppHeaderOutputNode(self, outputs_node, header_dir):
411 """Creates the <output> element corresponding to the generated c header."""
412 header_file_name = os.path.join(header_dir, self.name + '.h')
413 header_node = io.OutputNode()
414 header_node.StartParsing(u'output', outputs_node)
415 header_node.HandleAttribute('filename', header_file_name)
416 header_node.HandleAttribute('type', 'rc_header')
417 emit_node = io.EmitNode()
418 emit_node.StartParsing(u'emit', header_node)
419 emit_node.HandleAttribute('emit_type', 'prepend')
420 emit_node.EndParsing()
421 header_node.AddChild(emit_node)
422 header_node.EndParsing()
423 outputs_node.AddChild(header_node)
424 return header_node
425
426 def __CreateRcOutputNode(self, outputs_node, lang, rc_dir):
427 """Creates the <output> element corresponding to various rc file output."""
428 rc_file_name = self.name + '_' + lang + ".rc"
429 rc_path = os.path.join(rc_dir, rc_file_name)
430 node = io.OutputNode()
431 node.StartParsing(u'output', outputs_node)
432 node.HandleAttribute('filename', rc_path)
433 node.HandleAttribute('lang', lang)
434 node.HandleAttribute('type', 'rc_all')
435 node.EndParsing()
436 outputs_node.AddChild(node)
437 return node
438
439 def __CreateAndroidXmlOutputNode(self, outputs_node, locale, xml_res_dir):
440 """Creates the <output> element corresponding to various rc file output."""
441 # Need to check to see if the locale has a region, e.g. the GB in en-GB.
442 # When a locale has a region Android expects the region to be prefixed
443 # with an 'r'. For example for en-GB Android expects a values-en-rGB
444 # directory. Also, Android expects nb, tl, in, iw, ji as the language
445 # codes for Norwegian, Tagalog/Filipino, Indonesian, Hebrew, and Yiddish:
446 # http://developer.android.com/reference/java/util/Locale.html
447 if locale == 'es-419':
448 android_locale = 'es-rUS'
449 else:
450 android_lang, dash, region = locale.partition('-')
451 lang_map = {'no': 'nb', 'fil': 'tl', 'id': 'in', 'he': 'iw', 'yi': 'ji'}
452 android_lang = lang_map.get(android_lang, android_lang)
453 android_locale = android_lang + ('-r' + region if region else '')
454 values = 'values-' + android_locale if android_locale != 'en' else 'values'
455 xml_path = os.path.normpath(os.path.join(
456 xml_res_dir, values, 'strings.xml'))
457
458 node = io.OutputNode()
459 node.StartParsing(u'output', outputs_node)
460 node.HandleAttribute('filename', xml_path)
461 node.HandleAttribute('lang', locale)
462 node.HandleAttribute('type', 'android')
463 node.EndParsing()
464 outputs_node.AddChild(node)
465 return node
466
467 def IsTranslatable(self, android_string):
468 """Determines if a <string> element is a candidate for translation.
469
470 A <string> element is by default translatable unless otherwise marked.
471 """
472 if android_string.hasAttribute('translatable'):
473 value = android_string.getAttribute('translatable').lower()
474 if value not in ('true', 'false'):
475 print 'Warning: translatable attribute has invalid value: %s' % value
476 return value == 'true'
477 else:
478 return True
479
OLDNEW
« no previous file with comments | « grit/tool/__init__.py ('k') | grit/tool/android2grd_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698