| Index: grit/tool/android2grd.py
|
| ===================================================================
|
| --- grit/tool/android2grd.py (revision 202)
|
| +++ grit/tool/android2grd.py (working copy)
|
| @@ -1,479 +0,0 @@
|
| -#!/usr/bin/env python
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""The 'grit android2grd' tool."""
|
| -
|
| -
|
| -import getopt
|
| -import os.path
|
| -import StringIO
|
| -from xml.dom import Node
|
| -import xml.dom.minidom
|
| -
|
| -import grit.node.empty
|
| -from grit.node import io
|
| -from grit.node import message
|
| -
|
| -from grit.tool import interface
|
| -
|
| -from grit import grd_reader
|
| -from grit import lazy_re
|
| -from grit import tclib
|
| -from grit import util
|
| -
|
| -
|
| -# The name of a string in strings.xml
|
| -_STRING_NAME = lazy_re.compile(r'[a-z0-9_]+\Z')
|
| -
|
| -# A string's character limit in strings.xml
|
| -_CHAR_LIMIT = lazy_re.compile(r'\[CHAR-LIMIT=(\d+)\]')
|
| -
|
| -# Finds String.Format() style format specifiers such as "%-5.2f".
|
| -_FORMAT_SPECIFIER = lazy_re.compile(
|
| - '%'
|
| - '([1-9][0-9]*\$|<)?' # argument_index
|
| - '([-#+ 0,(]*)' # flags
|
| - '([0-9]+)?' # width
|
| - '(\.[0-9]+)?' # precision
|
| - '([bBhHsScCdoxXeEfgGaAtT%n])') # conversion
|
| -
|
| -
|
| -class Android2Grd(interface.Tool):
|
| - """Tool for converting Android string.xml files into chrome Grd files.
|
| -
|
| -Usage: grit [global options] android2grd [OPTIONS] STRINGS_XML
|
| -
|
| -The Android2Grd tool will convert an Android strings.xml file (whose path is
|
| -specified by STRINGS_XML) and create a chrome style grd file containing the
|
| -relevant information.
|
| -
|
| -Because grd documents are much richer than strings.xml documents we supplement
|
| -the information required by grds using OPTIONS with sensible defaults.
|
| -
|
| -OPTIONS may be any of the following:
|
| -
|
| - --name FILENAME Specify the base FILENAME. This should be without
|
| - any file type suffix. By default
|
| - "chrome_android_strings" will be used.
|
| -
|
| - --languages LANGUAGES Comma separated list of ISO language codes (e.g.
|
| - en-US, en-GB, ru, zh-CN). These codes will be used
|
| - to determine the names of resource and translations
|
| - files that will be declared by the output grd file.
|
| -
|
| - --grd-dir GRD_DIR Specify where the resultant grd file
|
| - (FILENAME.grd) should be output. By default this
|
| - will be the present working directory.
|
| -
|
| - --header-dir HEADER_DIR Specify the location of the directory where grit
|
| - generated C++ headers (whose name will be
|
| - FILENAME.h) will be placed. Use an empty string to
|
| - disable rc generation. Default: empty.
|
| -
|
| - --rc-dir RC_DIR Specify the directory where resource files will
|
| - be located relative to grit build's output
|
| - directory. Use an empty string to disable rc
|
| - generation. Default: empty.
|
| -
|
| - --xml-dir XML_DIR Specify where to place localized strings.xml files
|
| - relative to grit build's output directory. For each
|
| - language xx a values-xx/strings.xml file will be
|
| - generated. Use an empty string to disable
|
| - strings.xml generation. Default: '.'.
|
| -
|
| - --xtb-dir XTB_DIR Specify where the xtb files containing translations
|
| - will be located relative to the grd file. Default:
|
| - '.'.
|
| -"""
|
| -
|
| - _NAME_FLAG = 'name'
|
| - _LANGUAGES_FLAG = 'languages'
|
| - _GRD_DIR_FLAG = 'grd-dir'
|
| - _RC_DIR_FLAG = 'rc-dir'
|
| - _HEADER_DIR_FLAG = 'header-dir'
|
| - _XTB_DIR_FLAG = 'xtb-dir'
|
| - _XML_DIR_FLAG = 'xml-dir'
|
| -
|
| - def __init__(self):
|
| - self.name = 'chrome_android_strings'
|
| - self.languages = []
|
| - self.grd_dir = '.'
|
| - self.rc_dir = None
|
| - self.xtb_dir = '.'
|
| - self.xml_res_dir = '.'
|
| - self.header_dir = None
|
| -
|
| - def ShortDescription(self):
|
| - """Returns a short description of the Android2Grd tool.
|
| -
|
| - Overridden from grit.interface.Tool
|
| -
|
| - Returns:
|
| - A string containing a short description of the android2grd tool.
|
| - """
|
| - return 'Converts Android string.xml files into Chrome grd files.'
|
| -
|
| - def ParseOptions(self, args):
|
| - """Set this objects and return all non-option arguments."""
|
| - flags = [
|
| - Android2Grd._NAME_FLAG,
|
| - Android2Grd._LANGUAGES_FLAG,
|
| - Android2Grd._GRD_DIR_FLAG,
|
| - Android2Grd._RC_DIR_FLAG,
|
| - Android2Grd._HEADER_DIR_FLAG,
|
| - Android2Grd._XTB_DIR_FLAG,
|
| - Android2Grd._XML_DIR_FLAG, ]
|
| - (opts, args) = getopt.getopt(args, None, ['%s=' % o for o in flags])
|
| -
|
| - for key, val in opts:
|
| - # Get rid of the preceding hypens.
|
| - k = key[2:]
|
| - if k == Android2Grd._NAME_FLAG:
|
| - self.name = val
|
| - elif k == Android2Grd._LANGUAGES_FLAG:
|
| - self.languages = val.split(',')
|
| - elif k == Android2Grd._GRD_DIR_FLAG:
|
| - self.grd_dir = val
|
| - elif k == Android2Grd._RC_DIR_FLAG:
|
| - self.rc_dir = val
|
| - elif k == Android2Grd._HEADER_DIR_FLAG:
|
| - self.header_dir = val
|
| - elif k == Android2Grd._XTB_DIR_FLAG:
|
| - self.xtb_dir = val
|
| - elif k == Android2Grd._XML_DIR_FLAG:
|
| - self.xml_res_dir = val
|
| - return args
|
| -
|
| - def Run(self, opts, args):
|
| - """Runs the Android2Grd tool.
|
| -
|
| - Inherited from grit.interface.Tool.
|
| -
|
| - Args:
|
| - opts: List of string arguments that should be parsed.
|
| - args: String containing the path of the strings.xml file to be converted.
|
| - """
|
| - args = self.ParseOptions(args)
|
| - if len(args) != 1:
|
| - print ('Tool requires one argument, the path to the Android '
|
| - 'strings.xml resource file to be converted.')
|
| - return 2
|
| - self.SetOptions(opts)
|
| -
|
| - android_path = args[0]
|
| -
|
| - # Read and parse the Android strings.xml file.
|
| - with open(android_path) as android_file:
|
| - android_dom = xml.dom.minidom.parse(android_file)
|
| -
|
| - # Do the hard work -- convert the Android dom to grd file contents.
|
| - grd_dom = self.AndroidDomToGrdDom(android_dom)
|
| - grd_string = unicode(grd_dom)
|
| -
|
| - # Write the grd string to a file in grd_dir.
|
| - grd_filename = self.name + '.grd'
|
| - grd_path = os.path.join(self.grd_dir, grd_filename)
|
| - with open(grd_path, 'w') as grd_file:
|
| - grd_file.write(grd_string)
|
| -
|
| - def AndroidDomToGrdDom(self, android_dom):
|
| - """Converts a strings.xml DOM into a DOM representing the contents of
|
| - a grd file.
|
| -
|
| - Args:
|
| - android_dom: A xml.dom.Document containing the contents of the Android
|
| - string.xml document.
|
| - Returns:
|
| - The DOM for the grd xml document produced by converting the Android DOM.
|
| - """
|
| -
|
| - # Start with a basic skeleton for the .grd file.
|
| - root = grd_reader.Parse(StringIO.StringIO(
|
| - '''<?xml version="1.0" encoding="UTF-8"?>
|
| - <grit base_dir="." latest_public_release="0"
|
| - current_release="1" source_lang_id="en">
|
| - <outputs />
|
| - <translations />
|
| - <release allow_pseudo="false" seq="1">
|
| - <messages fallback_to_english="true" />
|
| - </release>
|
| - </grit>'''), dir='.')
|
| - outputs = root.children[0]
|
| - translations = root.children[1]
|
| - messages = root.children[2].children[0]
|
| - assert (isinstance(messages, grit.node.empty.MessagesNode) and
|
| - isinstance(translations, grit.node.empty.TranslationsNode) and
|
| - isinstance(outputs, grit.node.empty.OutputsNode))
|
| -
|
| - if self.header_dir:
|
| - cpp_header = self.__CreateCppHeaderOutputNode(outputs, self.header_dir)
|
| - for lang in self.languages:
|
| - # Create an output element for each language.
|
| - if self.rc_dir:
|
| - self.__CreateRcOutputNode(outputs, lang, self.rc_dir)
|
| - if self.xml_res_dir:
|
| - self.__CreateAndroidXmlOutputNode(outputs, lang, self.xml_res_dir)
|
| - if lang != 'en':
|
| - self.__CreateFileNode(translations, lang)
|
| - # Convert all the strings.xml strings into grd messages.
|
| - self.__CreateMessageNodes(messages, android_dom.documentElement)
|
| -
|
| - return root
|
| -
|
| - def __CreateMessageNodes(self, messages, resources):
|
| - """Creates the <message> elements and adds them as children of <messages>.
|
| -
|
| - Args:
|
| - messages: the <messages> element in the strings.xml dom.
|
| - resources: the <resources> element in the grd dom.
|
| - """
|
| - # <string> elements contain the definition of the resource.
|
| - # The description of a <string> element is contained within the comment
|
| - # node element immediately preceeding the string element in question.
|
| - description = ''
|
| - for child in resources.childNodes:
|
| - if child.nodeType == Node.COMMENT_NODE:
|
| - # Remove leading/trailing whitespace; collapse consecutive whitespaces.
|
| - description = ' '.join(child.data.split())
|
| - elif child.nodeType == Node.ELEMENT_NODE:
|
| - if child.tagName != 'string':
|
| - print 'Warning: ignoring unknown tag <%s>' % child.tagName
|
| - else:
|
| - translatable = self.IsTranslatable(child)
|
| - raw_name = child.getAttribute('name')
|
| - if not _STRING_NAME.match(raw_name):
|
| - print 'Error: illegal string name: %s' % raw_name
|
| - grd_name = 'IDS_' + raw_name.upper()
|
| - # Transform the <string> node contents into a tclib.Message, taking
|
| - # care to handle whitespace transformations and escaped characters,
|
| - # and coverting <xliff:g> placeholders into <ph> placeholders.
|
| - msg = self.CreateTclibMessage(child)
|
| - msg_node = self.__CreateMessageNode(messages, grd_name, description,
|
| - msg, translatable)
|
| - messages.AddChild(msg_node)
|
| - # Reset the description once a message has been parsed.
|
| - description = ''
|
| -
|
| - def CreateTclibMessage(self, android_string):
|
| - """Transforms a <string/> element from strings.xml into a tclib.Message.
|
| -
|
| - Interprets whitespace, quotes, and escaped characters in the android_string
|
| - according to Android's formatting and styling rules for strings. Also
|
| - converts <xliff:g> placeholders into <ph> placeholders, e.g.:
|
| -
|
| - <xliff:g id="website" example="google.com">%s</xliff:g>
|
| - becomes
|
| - <ph name="website"><ex>google.com</ex>%s</ph>
|
| -
|
| - Returns:
|
| - The tclib.Message.
|
| - """
|
| - msg = tclib.Message()
|
| - current_text = '' # Accumulated text that hasn't yet been added to msg.
|
| - nodes = android_string.childNodes
|
| -
|
| - for i, node in enumerate(nodes):
|
| - # Handle text nodes.
|
| - if node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
|
| - current_text += node.data
|
| -
|
| - # Handle <xliff:g> and other tags.
|
| - elif node.nodeType == Node.ELEMENT_NODE:
|
| - if node.tagName == 'xliff:g':
|
| - assert node.hasAttribute('id'), 'missing id: ' + node.data()
|
| - placeholder_id = node.getAttribute('id')
|
| - placeholder_text = self.__FormatPlaceholderText(node)
|
| - placeholder_example = node.getAttribute('example')
|
| - if not placeholder_example:
|
| - print ('Info: placeholder does not contain an example: %s' %
|
| - node.toxml())
|
| - placeholder_example = placeholder_id.upper()
|
| - msg.AppendPlaceholder(tclib.Placeholder(placeholder_id,
|
| - placeholder_text, placeholder_example))
|
| - else:
|
| - print ('Warning: removing tag <%s> which must be inside a '
|
| - 'placeholder: %s' % (node.tagName, node.toxml()))
|
| - msg.AppendText(self.__FormatPlaceholderText(node))
|
| -
|
| - # Handle other nodes.
|
| - elif node.nodeType != Node.COMMENT_NODE:
|
| - assert False, 'Unknown node type: %s' % node.nodeType
|
| -
|
| - is_last_node = (i == len(nodes) - 1)
|
| - if (current_text and
|
| - (is_last_node or nodes[i + 1].nodeType == Node.ELEMENT_NODE)):
|
| - # For messages containing just text and comments (no xml tags) Android
|
| - # strips leading and trailing whitespace. We mimic that behavior.
|
| - if not msg.GetContent() and is_last_node:
|
| - current_text = current_text.strip()
|
| - msg.AppendText(self.__FormatAndroidString(current_text))
|
| - current_text = ''
|
| -
|
| - return msg
|
| -
|
| - def __FormatAndroidString(self, android_string, inside_placeholder=False):
|
| - r"""Returns android_string formatted for a .grd file.
|
| -
|
| - * Collapses consecutive whitespaces, except when inside double-quotes.
|
| - * Replaces \\, \n, \t, \", \' with \, newline, tab, ", '.
|
| - """
|
| - backslash_map = {'\\' : '\\', 'n' : '\n', 't' : '\t', '"' : '"', "'" : "'"}
|
| - is_quoted_section = False # True when we're inside double quotes.
|
| - is_backslash_sequence = False # True after seeing an unescaped backslash.
|
| - prev_char = ''
|
| - output = []
|
| - for c in android_string:
|
| - if is_backslash_sequence:
|
| - # Unescape \\, \n, \t, \", and \'.
|
| - assert c in backslash_map, 'Illegal escape sequence: \\%s' % c
|
| - output.append(backslash_map[c])
|
| - is_backslash_sequence = False
|
| - elif c == '\\':
|
| - is_backslash_sequence = True
|
| - elif c.isspace() and not is_quoted_section:
|
| - # Turn whitespace into ' ' and collapse consecutive whitespaces.
|
| - if not prev_char.isspace():
|
| - output.append(' ')
|
| - elif c == '"':
|
| - is_quoted_section = not is_quoted_section
|
| - else:
|
| - output.append(c)
|
| - prev_char = c
|
| - output = ''.join(output)
|
| -
|
| - if is_quoted_section:
|
| - print 'Warning: unbalanced quotes in string: %s' % android_string
|
| -
|
| - if is_backslash_sequence:
|
| - print 'Warning: trailing backslash in string: %s' % android_string
|
| -
|
| - # Check for format specifiers outside of placeholder tags.
|
| - if not inside_placeholder:
|
| - format_specifier = _FORMAT_SPECIFIER.search(output)
|
| - if format_specifier:
|
| - print ('Warning: format specifiers are not inside a placeholder '
|
| - '<xliff:g/> tag: %s' % output)
|
| -
|
| - return output
|
| -
|
| - def __FormatPlaceholderText(self, placeholder_node):
|
| - """Returns the text inside of an <xliff:g> placeholder node."""
|
| - text = []
|
| - for childNode in placeholder_node.childNodes:
|
| - if childNode.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
|
| - text.append(childNode.data)
|
| - elif childNode.nodeType != Node.COMMENT_NODE:
|
| - assert False, 'Unknown node type in ' + placeholder_node.toxml()
|
| - return self.__FormatAndroidString(''.join(text), inside_placeholder=True)
|
| -
|
| - def __CreateMessageNode(self, messages_node, grd_name, description, msg,
|
| - translatable):
|
| - """Creates and initializes a <message> element.
|
| -
|
| - Message elements correspond to Android <string> elements in that they
|
| - declare a string resource along with a programmatic id.
|
| - """
|
| - if not description:
|
| - print 'Warning: no description for %s' % grd_name
|
| - # Check that we actually fit within the character limit we've specified.
|
| - match = _CHAR_LIMIT.search(description)
|
| - if match:
|
| - char_limit = int(match.group(1))
|
| - msg_content = msg.GetRealContent()
|
| - if len(msg_content) > char_limit:
|
| - print ('Warning: char-limit for %s is %d, but length is %d: %s' %
|
| - (grd_name, char_limit, len(msg_content), msg_content))
|
| - return message.MessageNode.Construct(parent=messages_node,
|
| - name=grd_name,
|
| - message=msg,
|
| - desc=description,
|
| - translateable=translatable)
|
| -
|
| - def __CreateFileNode(self, translations_node, lang):
|
| - """Creates and initializes the <file> elements.
|
| -
|
| - File elements provide information on the location of translation files
|
| - (xtbs)
|
| - """
|
| - xtb_file = os.path.normpath(os.path.join(
|
| - self.xtb_dir, '%s_%s.xtb' % (self.name, lang)))
|
| - fnode = io.FileNode()
|
| - fnode.StartParsing(u'file', translations_node)
|
| - fnode.HandleAttribute('path', xtb_file)
|
| - fnode.HandleAttribute('lang', lang)
|
| - fnode.EndParsing()
|
| - translations_node.AddChild(fnode)
|
| - return fnode
|
| -
|
| - def __CreateCppHeaderOutputNode(self, outputs_node, header_dir):
|
| - """Creates the <output> element corresponding to the generated c header."""
|
| - header_file_name = os.path.join(header_dir, self.name + '.h')
|
| - header_node = io.OutputNode()
|
| - header_node.StartParsing(u'output', outputs_node)
|
| - header_node.HandleAttribute('filename', header_file_name)
|
| - header_node.HandleAttribute('type', 'rc_header')
|
| - emit_node = io.EmitNode()
|
| - emit_node.StartParsing(u'emit', header_node)
|
| - emit_node.HandleAttribute('emit_type', 'prepend')
|
| - emit_node.EndParsing()
|
| - header_node.AddChild(emit_node)
|
| - header_node.EndParsing()
|
| - outputs_node.AddChild(header_node)
|
| - return header_node
|
| -
|
| - def __CreateRcOutputNode(self, outputs_node, lang, rc_dir):
|
| - """Creates the <output> element corresponding to various rc file output."""
|
| - rc_file_name = self.name + '_' + lang + ".rc"
|
| - rc_path = os.path.join(rc_dir, rc_file_name)
|
| - node = io.OutputNode()
|
| - node.StartParsing(u'output', outputs_node)
|
| - node.HandleAttribute('filename', rc_path)
|
| - node.HandleAttribute('lang', lang)
|
| - node.HandleAttribute('type', 'rc_all')
|
| - node.EndParsing()
|
| - outputs_node.AddChild(node)
|
| - return node
|
| -
|
| - def __CreateAndroidXmlOutputNode(self, outputs_node, locale, xml_res_dir):
|
| - """Creates the <output> element corresponding to various rc file output."""
|
| - # Need to check to see if the locale has a region, e.g. the GB in en-GB.
|
| - # When a locale has a region Android expects the region to be prefixed
|
| - # with an 'r'. For example for en-GB Android expects a values-en-rGB
|
| - # directory. Also, Android expects nb, tl, in, iw, ji as the language
|
| - # codes for Norwegian, Tagalog/Filipino, Indonesian, Hebrew, and Yiddish:
|
| - # http://developer.android.com/reference/java/util/Locale.html
|
| - if locale == 'es-419':
|
| - android_locale = 'es-rUS'
|
| - else:
|
| - android_lang, dash, region = locale.partition('-')
|
| - lang_map = {'no': 'nb', 'fil': 'tl', 'id': 'in', 'he': 'iw', 'yi': 'ji'}
|
| - android_lang = lang_map.get(android_lang, android_lang)
|
| - android_locale = android_lang + ('-r' + region if region else '')
|
| - values = 'values-' + android_locale if android_locale != 'en' else 'values'
|
| - xml_path = os.path.normpath(os.path.join(
|
| - xml_res_dir, values, 'strings.xml'))
|
| -
|
| - node = io.OutputNode()
|
| - node.StartParsing(u'output', outputs_node)
|
| - node.HandleAttribute('filename', xml_path)
|
| - node.HandleAttribute('lang', locale)
|
| - node.HandleAttribute('type', 'android')
|
| - node.EndParsing()
|
| - outputs_node.AddChild(node)
|
| - return node
|
| -
|
| - def IsTranslatable(self, android_string):
|
| - """Determines if a <string> element is a candidate for translation.
|
| -
|
| - A <string> element is by default translatable unless otherwise marked.
|
| - """
|
| - if android_string.hasAttribute('translatable'):
|
| - value = android_string.getAttribute('translatable').lower()
|
| - if value not in ('true', 'false'):
|
| - print 'Warning: translatable attribute has invalid value: %s' % value
|
| - return value == 'true'
|
| - else:
|
| - return True
|
| -
|
|
|