Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / installer / util / prebuild / create_string_rc.py
index 85fde1c..edd0fa7 100755 (executable)
@@ -3,9 +3,12 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-"""This script generates an rc file and header (setup_strings.{rc,h}) to be
-included in setup.exe. The rc file includes translations for strings pulled
-from generated_resource.grd and the localized .xtb files.
+"""Generates .h and .rc files for installer strings. Run "python
+create_string_rc.py" for usage details.
+
+This script generates an rc file and header (NAME.{rc,h}) to be included in
+setup.exe. The rc file includes translations for strings pulled from the given
+.grd file(s) and their corresponding localized .xtb files.
 
 The header file includes IDs for each string, but also has values to allow
 getting a string based on a language offset.  For example, the header file
@@ -26,38 +29,33 @@ This allows us to lookup an an ID for a string by adding IDS_MY_STRING_BASE and
 IDS_L10N_OFFSET_* for the language we are interested in.
 """
 
+import argparse
 import glob
+import io
 import os
 import sys
-from xml.dom import minidom
+from xml import sax
 
-# We are expected to use ../../../../third_party/python_24/python.exe
-from google import path_utils
+BASEDIR = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(os.path.join(BASEDIR, '../../../../tools/grit'))
+sys.path.append(os.path.join(BASEDIR, '../../../../tools/python'))
 
-# Quick hack to fix the path.
-sys.path.append(os.path.abspath('../../tools/grit'))
-sys.path.append(os.path.abspath('../tools/grit'))
 from grit.extern import tclib
 
-# The IDs of strings we want to import from generated_resources.grd and include
-# in setup.exe's resources.
-kStringIds = [
+# The IDs of strings we want to import from the .grd files and include in
+# setup.exe's resources.
+STRING_IDS = [
   'IDS_PRODUCT_NAME',
   'IDS_SXS_SHORTCUT_NAME',
   'IDS_PRODUCT_APP_LAUNCHER_NAME',
   'IDS_PRODUCT_BINARIES_NAME',
   'IDS_PRODUCT_DESCRIPTION',
-  'IDS_PRODUCT_FRAME_NAME',
   'IDS_UNINSTALL_CHROME',
   'IDS_ABOUT_VERSION_COMPANY_NAME',
   'IDS_INSTALL_HIGHER_VERSION',
   'IDS_INSTALL_HIGHER_VERSION_APP_LAUNCHER',
-  'IDS_INSTALL_HIGHER_VERSION_CF',
-  'IDS_INSTALL_HIGHER_VERSION_CB_CF',
-  'IDS_INSTALL_SYSTEM_LEVEL_EXISTS',
   'IDS_INSTALL_FAILED',
   'IDS_SAME_VERSION_REPAIR_FAILED',
-  'IDS_SAME_VERSION_REPAIR_FAILED_CF',
   'IDS_SETUP_PATCH_FAILED',
   'IDS_INSTALL_OS_NOT_SUPPORTED',
   'IDS_INSTALL_OS_ERROR',
@@ -68,9 +66,7 @@ kStringIds = [
   'IDS_INSTALL_NO_PRODUCTS_TO_UPDATE',
   'IDS_UNINSTALL_COMPLETE',
   'IDS_INSTALL_DIR_IN_USE',
-  'IDS_INSTALL_NON_MULTI_INSTALLATION_EXISTS',
   'IDS_INSTALL_MULTI_INSTALLATION_EXISTS',
-  'IDS_INSTALL_READY_MODE_REQUIRES_CHROME',
   'IDS_INSTALL_INCONSISTENT_UPDATE_POLICY',
   'IDS_OEM_MAIN_SHORTCUT_NAME',
   'IDS_SHORTCUT_TOOLTIP',
@@ -80,176 +76,355 @@ kStringIds = [
   'IDS_UNINSTALL_APP_LAUNCHER',
   'IDS_APP_LIST_SHORTCUT_NAME',
   'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
+  'IDS_APP_SHORTCUTS_SUBDIR_NAME',
+  'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
 ]
 
 # The ID of the first resource string.
-kFirstResourceID = 1600
-
-
-class TranslationStruct:
-  """A helper struct that holds information about a single translation."""
-  def __init__(self, resource_id_str, language, translation):
-    self.resource_id_str = resource_id_str
-    self.language = language
-    self.translation = translation
-
-  def __cmp__(self, other):
-    """Allow TranslationStructs to be sorted by id."""
-    id_result = cmp(self.resource_id_str, other.resource_id_str)
-    return cmp(self.language, other.language) if id_result == 0 else id_result
-
-
-def CollectTranslatedStrings(branding):
-  """Collects all the translations for all the strings specified by kStringIds.
-  Returns a list of tuples of (string_id, language, translated string). The
-  list is sorted by language codes."""
-  strings_file = 'app/chromium_strings.grd'
-  translation_files = 'chromium_strings*.xtb'
-  if branding == 'Chrome':
-    strings_file = 'app/google_chrome_strings.grd'
-    translation_files = 'google_chrome_strings*.xtb'
-  kGeneratedResourcesPath = os.path.join(path_utils.ScriptDir(), '..', '..',
-                                         '..', strings_file)
-  kTranslationDirectory = os.path.join(path_utils.ScriptDir(), '..', '..',
-                                       '..', 'app', 'resources')
-  kTranslationFiles = glob.glob(os.path.join(kTranslationDirectory,
-                                             translation_files))
-
-  # Get the strings out of generated_resources.grd.
-  dom = minidom.parse(kGeneratedResourcesPath)
-  # message_nodes is a list of message dom nodes corresponding to the string
-  # ids we care about.  We want to make sure that this list is in the same
-  # order as kStringIds so we can associate them together.
-  message_nodes = []
-  all_message_nodes = dom.getElementsByTagName('message')
-  for string_id in kStringIds:
-    message_nodes.append([x for x in all_message_nodes if
-                          x.getAttribute('name') == string_id][0])
-  message_texts = [node.firstChild.nodeValue.strip() for node in message_nodes]
-
-  # Generate the message ID of the string to correlate it with its translations
-  # in the xtb files.
-  translation_ids = [tclib.GenerateMessageId(text) for text in message_texts]
-
-  # Manually put _EN_US in the list of translated strings because it doesn't
-  # have a .xtb file.
-  translated_strings = []
-  for string_id, message_text in zip(kStringIds, message_texts):
-    translated_strings.append(TranslationStruct(string_id,
-                                                'EN_US',
-                                                message_text))
-
-  # Gather the translated strings from the .xtb files.  If an .xtb file doesn't
-  # have the string we want, use the en-US string.
-  for xtb_filename in kTranslationFiles:
-    dom = minidom.parse(xtb_filename)
-    language = dom.documentElement.getAttribute('lang')
-    language = language.replace('-', '_').upper()
-    translation_nodes = {}
-    for translation_node in dom.getElementsByTagName('translation'):
-      translation_id = translation_node.getAttribute('id')
-      if translation_id in translation_ids:
-        translation_nodes[translation_id] = (translation_node.firstChild
-                                                             .nodeValue
-                                                             .strip())
-    for i, string_id in enumerate(kStringIds):
-      translated_string = translation_nodes.get(translation_ids[i],
-                                                message_texts[i])
-      translated_strings.append(TranslationStruct(string_id,
-                                                  language,
-                                                  translated_string))
-
-  translated_strings.sort()
-  return translated_strings
-
-
-def WriteRCFile(translated_strings, out_filename):
-  """Writes a resource (rc) file with all the language strings provided in
-  |translated_strings|."""
-  kHeaderText = (
-    u'#include "%s.h"\n\n'
-    u'STRINGTABLE\n'
-    u'BEGIN\n'
-  ) % os.path.basename(out_filename)
-  kFooterText = (
-    u'END\n'
-  )
-  lines = [kHeaderText]
-  for translation_struct in translated_strings:
-    # Escape special characters for the rc file.
-    translation = (translation_struct.translation.replace('"', '""')
-                                                 .replace('\t', '\\t')
-                                                 .replace('\n', '\\n'))
-    lines.append(u'  %s "%s"\n' % (translation_struct.resource_id_str + '_'
-                                       + translation_struct.language,
-                                   translation))
-  lines.append(kFooterText)
-  outfile = open(out_filename + '.rc', 'wb')
-  outfile.write(''.join(lines).encode('utf-16'))
-  outfile.close()
-
-
-def WriteHeaderFile(translated_strings, out_filename):
-  """Writes a .h file with resource ids.  This file can be included by the
-  executable to refer to identifiers."""
-  lines = []
-  do_languages_lines = ['\n#define DO_LANGUAGES']
-  installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
-
-  # Write the values for how the languages ids are offset.
-  seen_languages = set()
-  offset_id = 0
-  for translation_struct in translated_strings:
-    lang = translation_struct.language
-    if lang not in seen_languages:
-      seen_languages.add(lang)
-      lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
-      do_languages_lines.append('  HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
-                                % (lang.replace('_', '-').lower(), lang))
-      offset_id += 1
-    else:
-      break
-
-  # Write the resource ids themselves.
-  resource_id = kFirstResourceID
-  for translation_struct in translated_strings:
-    lines.append('#define %s %s' % (translation_struct.resource_id_str + '_'
-                                        + translation_struct.language,
-                                    resource_id))
-    resource_id += 1
-
-  # Write out base ID values.
-  for string_id in kStringIds:
-    lines.append('#define %s_BASE %s_%s' % (string_id,
-                                            string_id,
-                                            translated_strings[0].language))
-    installer_string_mapping_lines.append('  HANDLE_STRING(%s_BASE, %s)'
-                                          % (string_id, string_id))
-
-  outfile = open(out_filename, 'wb')
-  outfile.write('\n'.join(lines))
-  outfile.write('\n#ifndef RC_INVOKED')
-  outfile.write(' \\\n'.join(do_languages_lines))
-  outfile.write(' \\\n'.join(installer_string_mapping_lines))
-  # .rc files must end in a new line
-  outfile.write('\n#endif  // ndef RC_INVOKED\n')
-  outfile.close()
-
-
-def main(argv):
-  # TODO: Use optparse to parse command line flags.
-  if len(argv) < 2:
-    print 'Usage:\n  %s <output_directory> [branding]' % argv[0]
-    return 1
-  branding = ''
-  if (len(sys.argv) > 2):
-    branding = argv[2]
-  translated_strings = CollectTranslatedStrings(branding)
-  kFilebase = os.path.join(argv[1], 'installer_util_strings')
-  WriteRCFile(translated_strings, kFilebase)
-  WriteHeaderFile(translated_strings, kFilebase + '.h')
+FIRST_RESOURCE_ID = 1600
+
+
+class GrdHandler(sax.handler.ContentHandler):
+  """Extracts selected strings from a .grd file.
+
+  Attributes:
+    messages: A dict mapping string identifiers to their corresponding messages.
+  """
+  def __init__(self, string_ids):
+    """Constructs a handler that reads selected strings from a .grd file.
+
+    The dict attribute |messages| is populated with the strings that are read.
+
+    Args:
+      string_ids: A list of message identifiers to extract.
+    """
+    sax.handler.ContentHandler.__init__(self)
+    self.messages = {}
+    self.__id_set = set(string_ids)
+    self.__message_name = None
+    self.__element_stack = []
+    self.__text_scraps = []
+    self.__characters_callback = None
+
+  def startElement(self, name, attrs):
+    self.__element_stack.append(name)
+    if name == 'message':
+      self.__OnOpenMessage(attrs.getValue('name'))
+
+  def endElement(self, name):
+    popped = self.__element_stack.pop()
+    assert popped == name
+    if name == 'message':
+      self.__OnCloseMessage()
+
+  def characters(self, content):
+    if self.__characters_callback:
+      self.__characters_callback(self.__element_stack[-1], content)
+
+  def __IsExtractingMessage(self):
+    """Returns True if a message is currently being extracted."""
+    return self.__message_name is not None
+
+  def __OnOpenMessage(self, message_name):
+    """Invoked at the start of a <message> with message's name."""
+    assert not self.__IsExtractingMessage()
+    self.__message_name = (message_name if message_name in self.__id_set
+                           else None)
+    if self.__message_name:
+      self.__characters_callback = self.__OnMessageText
+
+  def __OnMessageText(self, containing_element, message_text):
+    """Invoked to handle a block of text for a message."""
+    if message_text and (containing_element == 'message' or
+                         containing_element == 'ph'):
+      self.__text_scraps.append(message_text)
+
+  def __OnCloseMessage(self):
+    """Invoked at the end of a message."""
+    if self.__IsExtractingMessage():
+      self.messages[self.__message_name] = ''.join(self.__text_scraps).strip()
+      self.__message_name = None
+      self.__text_scraps = []
+      self.__characters_callback = None
+
+
+class XtbHandler(sax.handler.ContentHandler):
+  """Extracts selected translations from an .xrd file.
+
+  Populates the |lang| and |translations| attributes with the language and
+  selected strings of an .xtb file. Instances may be re-used to read the same
+  set of translations from multiple .xtb files.
+
+  Attributes:
+    translations: A mapping of translation ids to strings.
+    lang: The language parsed from the .xtb file.
+  """
+  def __init__(self, translation_ids):
+    """Constructs an instance to parse the given strings from an .xtb file.
+
+    Args:
+      translation_ids: a mapping of translation ids to their string
+        identifiers for the translations to be extracted.
+    """
+    sax.handler.ContentHandler.__init__(self)
+    self.lang = None
+    self.translations = None
+    self.__translation_ids = translation_ids
+    self.__element_stack = []
+    self.__string_id = None
+    self.__text_scraps = []
+    self.__characters_callback = None
+
+  def startDocument(self):
+    # Clear the lang and translations since a new document is being parsed.
+    self.lang = ''
+    self.translations = {}
+
+  def startElement(self, name, attrs):
+    self.__element_stack.append(name)
+    # translationbundle is the document element, and hosts the lang id.
+    if len(self.__element_stack) == 1:
+      assert name == 'translationbundle'
+      self.__OnLanguage(attrs.getValue('lang'))
+    if name == 'translation':
+      self.__OnOpenTranslation(attrs.getValue('id'))
+
+  def endElement(self, name):
+    popped = self.__element_stack.pop()
+    assert popped == name
+    if name == 'translation':
+      self.__OnCloseTranslation()
+
+  def characters(self, content):
+    if self.__characters_callback:
+      self.__characters_callback(self.__element_stack[-1], content)
+
+  def __OnLanguage(self, lang):
+    self.lang = lang.replace('-', '_').upper()
+
+  def __OnOpenTranslation(self, translation_id):
+    assert self.__string_id is None
+    self.__string_id = self.__translation_ids.get(translation_id)
+    if self.__string_id is not None:
+      self.__characters_callback = self.__OnTranslationText
+
+  def __OnTranslationText(self, containing_element, message_text):
+    if message_text and containing_element == 'translation':
+      self.__text_scraps.append(message_text)
+
+  def __OnCloseTranslation(self):
+    if self.__string_id is not None:
+      self.translations[self.__string_id] = ''.join(self.__text_scraps).strip()
+      self.__string_id = None
+      self.__text_scraps = []
+      self.__characters_callback = None
+
+
+class StringRcMaker(object):
+  """Makes .h and .rc files containing strings and translations."""
+  def __init__(self, name, inputs, outdir):
+    """Constructs a maker.
+
+    Args:
+      name: The base name of the generated files (e.g.,
+        'installer_util_strings').
+      inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
+      outdir: The directory into which the files will be generated.
+    """
+    self.name = name
+    self.inputs = inputs
+    self.outdir = outdir
+
+  def MakeFiles(self):
+    translated_strings = self.__ReadSourceAndTranslatedStrings()
+    self.__WriteRCFile(translated_strings)
+    self.__WriteHeaderFile(translated_strings)
+
+  class __TranslationData(object):
+    """A container of information about a single translation."""
+    def __init__(self, resource_id_str, language, translation):
+      self.resource_id_str = resource_id_str
+      self.language = language
+      self.translation = translation
+
+    def __cmp__(self, other):
+      """Allow __TranslationDatas to be sorted by id then by language."""
+      id_result = cmp(self.resource_id_str, other.resource_id_str)
+      return cmp(self.language, other.language) if id_result == 0 else id_result
+
+  def __ReadSourceAndTranslatedStrings(self):
+    """Reads the source strings and translations from all inputs."""
+    translated_strings = []
+    for grd_file, xtb_dir in self.inputs:
+      # Get the name of the grd file sans extension.
+      source_name = os.path.splitext(os.path.basename(grd_file))[0]
+      # Compute a glob for the translation files.
+      xtb_pattern = os.path.join(os.path.dirname(grd_file), xtb_dir,
+                                 '%s*.xtb' % source_name)
+      translated_strings.extend(
+        self.__ReadSourceAndTranslationsFrom(grd_file, glob.glob(xtb_pattern)))
+    translated_strings.sort()
+    return translated_strings
+
+  def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
+    """Reads source strings and translations for a .grd file.
+
+    Reads the source strings and all available translations for the messages
+    identified by STRING_IDS. The source string is used where translations are
+    missing.
+
+    Args:
+      grd_file: Path to a .grd file.
+      xtb_files: List of paths to .xtb files.
+
+    Returns:
+      An unsorted list of __TranslationData instances.
+    """
+    sax_parser = sax.make_parser()
+
+    # Read the source (en-US) string from the .grd file.
+    grd_handler = GrdHandler(STRING_IDS)
+    sax_parser.setContentHandler(grd_handler)
+    sax_parser.parse(grd_file)
+    source_strings = grd_handler.messages
+
+    # Manually put the source strings as en-US in the list of translated
+    # strings.
+    translated_strings = []
+    for string_id, message_text in source_strings.iteritems():
+      translated_strings.append(self.__TranslationData(string_id,
+                                                       'EN_US',
+                                                       message_text))
+
+    # Generate the message ID for each source string to correlate it with its
+    # translations in the .xtb files.
+    translation_ids = {
+      tclib.GenerateMessageId(message_text): string_id
+      for (string_id, message_text) in source_strings.iteritems()
+    }
+
+    # Gather the translated strings from the .xtb files. Use the en-US string
+    # for any message lacking a translation.
+    xtb_handler = XtbHandler(translation_ids)
+    sax_parser.setContentHandler(xtb_handler)
+    for xtb_filename in xtb_files:
+      sax_parser.parse(xtb_filename)
+      for string_id, message_text in source_strings.iteritems():
+        translated_string = xtb_handler.translations.get(string_id,
+                                                         message_text)
+        translated_strings.append(self.__TranslationData(string_id,
+                                                         xtb_handler.lang,
+                                                         translated_string))
+    return translated_strings
+
+  def __WriteRCFile(self, translated_strings):
+    """Writes a resource file with the strings provided in |translated_strings|.
+    """
+    HEADER_TEXT = (
+      u'#include "%s.h"\n\n'
+      u'STRINGTABLE\n'
+      u'BEGIN\n'
+      ) % self.name
+
+    FOOTER_TEXT = (
+      u'END\n'
+    )
+
+    with io.open(os.path.join(self.outdir, self.name + '.rc'),
+                 mode='w',
+                 encoding='utf-16',
+                 newline='\n') as outfile:
+      outfile.write(HEADER_TEXT)
+      for translation in translated_strings:
+        # Escape special characters for the rc file.
+        escaped_text = (translation.translation.replace('"', '""')
+                       .replace('\t', '\\t')
+                       .replace('\n', '\\n'))
+        outfile.write(u'  %s "%s"\n' %
+                      (translation.resource_id_str + '_' + translation.language,
+                       escaped_text))
+      outfile.write(FOOTER_TEXT)
+
+  def __WriteHeaderFile(self, translated_strings):
+    """Writes a .h file with resource ids."""
+    # TODO(grt): Stream the lines to the file rather than building this giant
+    # list of lines first.
+    lines = []
+    do_languages_lines = ['\n#define DO_LANGUAGES']
+    installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
+
+    # Write the values for how the languages ids are offset.
+    seen_languages = set()
+    offset_id = 0
+    for translation_data in translated_strings:
+      lang = translation_data.language
+      if lang not in seen_languages:
+        seen_languages.add(lang)
+        lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
+        do_languages_lines.append('  HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
+                                  % (lang.replace('_', '-').lower(), lang))
+        offset_id += 1
+      else:
+        break
+
+    # Write the resource ids themselves.
+    resource_id = FIRST_RESOURCE_ID
+    for translation_data in translated_strings:
+      lines.append('#define %s %s' % (translation_data.resource_id_str + '_' +
+                                      translation_data.language,
+                                      resource_id))
+      resource_id += 1
+
+    # Write out base ID values.
+    for string_id in STRING_IDS:
+      lines.append('#define %s_BASE %s_%s' % (string_id,
+                                              string_id,
+                                              translated_strings[0].language))
+      installer_string_mapping_lines.append('  HANDLE_STRING(%s_BASE, %s)'
+                                            % (string_id, string_id))
+
+    with open(os.path.join(self.outdir, self.name + '.h'), 'wb') as outfile:
+      outfile.write('\n'.join(lines))
+      outfile.write('\n#ifndef RC_INVOKED')
+      outfile.write(' \\\n'.join(do_languages_lines))
+      outfile.write(' \\\n'.join(installer_string_mapping_lines))
+      # .rc files must end in a new line
+      outfile.write('\n#endif  // ndef RC_INVOKED\n')
+
+
+def ParseCommandLine():
+  def GrdPathAndXtbDirPair(string):
+    """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
+    """
+    parts = string.split(':')
+    if len(parts) is not 2:
+      raise argparse.ArgumentTypeError('%r is not grd_path:xtb_dir')
+    return (parts[0], parts[1])
+
+  parser = argparse.ArgumentParser(
+    description='Generate .h and .rc files for installer strings.')
+  parser.add_argument('-i', action='append',
+                      type=GrdPathAndXtbDirPair,
+                      required=True,
+                      help='path to .grd file:relative path to .xtb dir',
+                      metavar='GRDFILE:XTBDIR',
+                      dest='inputs')
+  parser.add_argument('-o',
+                      required=True,
+                      help='output directory for generated .rc and .h files',
+                      dest='outdir')
+  parser.add_argument('-n',
+                      required=True,
+                      help='base name of generated .rc and .h files',
+                      dest='name')
+  return parser.parse_args()
+
+
+def main():
+  args = ParseCommandLine()
+  StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
   return 0
 
 
 if '__main__' == __name__:
-  sys.exit(main(sys.argv))
+  sys.exit(main())