Home | History | Annotate | Download | only in prebuild
      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 """Generates .h and .rc files for installer strings. Run "python
      7 create_string_rc.py" for usage details.
      8 
      9 This script generates an rc file and header (NAME.{rc,h}) to be included in
     10 setup.exe. The rc file includes translations for strings pulled from the given
     11 .grd file(s) and their corresponding localized .xtb files.
     12 
     13 The header file includes IDs for each string, but also has values to allow
     14 getting a string based on a language offset.  For example, the header file
     15 looks like this:
     16 
     17 #define IDS_L10N_OFFSET_AR 0
     18 #define IDS_L10N_OFFSET_BG 1
     19 #define IDS_L10N_OFFSET_CA 2
     20 ...
     21 #define IDS_L10N_OFFSET_ZH_TW 41
     22 
     23 #define IDS_MY_STRING_AR 1600
     24 #define IDS_MY_STRING_BG 1601
     25 ...
     26 #define IDS_MY_STRING_BASE IDS_MY_STRING_AR
     27 
     28 This allows us to lookup an an ID for a string by adding IDS_MY_STRING_BASE and
     29 IDS_L10N_OFFSET_* for the language we are interested in.
     30 """
     31 
     32 import argparse
     33 import glob
     34 import io
     35 import os
     36 import sys
     37 from xml import sax
     38 
     39 BASEDIR = os.path.dirname(os.path.abspath(__file__))
     40 sys.path.append(os.path.join(BASEDIR, '../../../../tools/grit'))
     41 sys.path.append(os.path.join(BASEDIR, '../../../../tools/python'))
     42 
     43 from grit.extern import tclib
     44 
     45 # The IDs of strings we want to import from the .grd files and include in
     46 # setup.exe's resources.
     47 STRING_IDS = [
     48   'IDS_PRODUCT_NAME',
     49   'IDS_SXS_SHORTCUT_NAME',
     50   'IDS_PRODUCT_APP_LAUNCHER_NAME',
     51   'IDS_PRODUCT_BINARIES_NAME',
     52   'IDS_PRODUCT_DESCRIPTION',
     53   'IDS_UNINSTALL_CHROME',
     54   'IDS_ABOUT_VERSION_COMPANY_NAME',
     55   'IDS_INSTALL_HIGHER_VERSION',
     56   'IDS_INSTALL_HIGHER_VERSION_APP_LAUNCHER',
     57   'IDS_INSTALL_FAILED',
     58   'IDS_SAME_VERSION_REPAIR_FAILED',
     59   'IDS_SETUP_PATCH_FAILED',
     60   'IDS_INSTALL_OS_NOT_SUPPORTED',
     61   'IDS_INSTALL_OS_ERROR',
     62   'IDS_INSTALL_TEMP_DIR_FAILED',
     63   'IDS_INSTALL_UNCOMPRESSION_FAILED',
     64   'IDS_INSTALL_INVALID_ARCHIVE',
     65   'IDS_INSTALL_INSUFFICIENT_RIGHTS',
     66   'IDS_INSTALL_NO_PRODUCTS_TO_UPDATE',
     67   'IDS_INSTALL_MULTI_INSTALLATION_EXISTS',
     68   'IDS_INSTALL_INCONSISTENT_UPDATE_POLICY',
     69   'IDS_OEM_MAIN_SHORTCUT_NAME',
     70   'IDS_SHORTCUT_TOOLTIP',
     71   'IDS_SHORTCUT_NEW_WINDOW',
     72   'IDS_APP_LAUNCHER_PRODUCT_DESCRIPTION',
     73   'IDS_APP_LAUNCHER_SHORTCUT_TOOLTIP',
     74   'IDS_UNINSTALL_APP_LAUNCHER',
     75   'IDS_APP_LIST_SHORTCUT_NAME',
     76   'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
     77   'IDS_APP_SHORTCUTS_SUBDIR_NAME',
     78   'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
     79   'IDS_INBOUND_MDNS_RULE_NAME',
     80   'IDS_INBOUND_MDNS_RULE_NAME_CANARY',
     81   'IDS_INBOUND_MDNS_RULE_DESCRIPTION',
     82   'IDS_INBOUND_MDNS_RULE_DESCRIPTION_CANARY',
     83 ]
     84 
     85 # The ID of the first resource string.
     86 FIRST_RESOURCE_ID = 1600
     87 
     88 
     89 class GrdHandler(sax.handler.ContentHandler):
     90   """Extracts selected strings from a .grd file.
     91 
     92   Attributes:
     93     messages: A dict mapping string identifiers to their corresponding messages.
     94   """
     95   def __init__(self, string_ids):
     96     """Constructs a handler that reads selected strings from a .grd file.
     97 
     98     The dict attribute |messages| is populated with the strings that are read.
     99 
    100     Args:
    101       string_ids: A list of message identifiers to extract.
    102     """
    103     sax.handler.ContentHandler.__init__(self)
    104     self.messages = {}
    105     self.__id_set = set(string_ids)
    106     self.__message_name = None
    107     self.__element_stack = []
    108     self.__text_scraps = []
    109     self.__characters_callback = None
    110 
    111   def startElement(self, name, attrs):
    112     self.__element_stack.append(name)
    113     if name == 'message':
    114       self.__OnOpenMessage(attrs.getValue('name'))
    115 
    116   def endElement(self, name):
    117     popped = self.__element_stack.pop()
    118     assert popped == name
    119     if name == 'message':
    120       self.__OnCloseMessage()
    121 
    122   def characters(self, content):
    123     if self.__characters_callback:
    124       self.__characters_callback(self.__element_stack[-1], content)
    125 
    126   def __IsExtractingMessage(self):
    127     """Returns True if a message is currently being extracted."""
    128     return self.__message_name is not None
    129 
    130   def __OnOpenMessage(self, message_name):
    131     """Invoked at the start of a <message> with message's name."""
    132     assert not self.__IsExtractingMessage()
    133     self.__message_name = (message_name if message_name in self.__id_set
    134                            else None)
    135     if self.__message_name:
    136       self.__characters_callback = self.__OnMessageText
    137 
    138   def __OnMessageText(self, containing_element, message_text):
    139     """Invoked to handle a block of text for a message."""
    140     if message_text and (containing_element == 'message' or
    141                          containing_element == 'ph'):
    142       self.__text_scraps.append(message_text)
    143 
    144   def __OnCloseMessage(self):
    145     """Invoked at the end of a message."""
    146     if self.__IsExtractingMessage():
    147       self.messages[self.__message_name] = ''.join(self.__text_scraps).strip()
    148       self.__message_name = None
    149       self.__text_scraps = []
    150       self.__characters_callback = None
    151 
    152 
    153 class XtbHandler(sax.handler.ContentHandler):
    154   """Extracts selected translations from an .xrd file.
    155 
    156   Populates the |lang| and |translations| attributes with the language and
    157   selected strings of an .xtb file. Instances may be re-used to read the same
    158   set of translations from multiple .xtb files.
    159 
    160   Attributes:
    161     translations: A mapping of translation ids to strings.
    162     lang: The language parsed from the .xtb file.
    163   """
    164   def __init__(self, translation_ids):
    165     """Constructs an instance to parse the given strings from an .xtb file.
    166 
    167     Args:
    168       translation_ids: a mapping of translation ids to their string
    169         identifiers for the translations to be extracted.
    170     """
    171     sax.handler.ContentHandler.__init__(self)
    172     self.lang = None
    173     self.translations = None
    174     self.__translation_ids = translation_ids
    175     self.__element_stack = []
    176     self.__string_id = None
    177     self.__text_scraps = []
    178     self.__characters_callback = None
    179 
    180   def startDocument(self):
    181     # Clear the lang and translations since a new document is being parsed.
    182     self.lang = ''
    183     self.translations = {}
    184 
    185   def startElement(self, name, attrs):
    186     self.__element_stack.append(name)
    187     # translationbundle is the document element, and hosts the lang id.
    188     if len(self.__element_stack) == 1:
    189       assert name == 'translationbundle'
    190       self.__OnLanguage(attrs.getValue('lang'))
    191     if name == 'translation':
    192       self.__OnOpenTranslation(attrs.getValue('id'))
    193 
    194   def endElement(self, name):
    195     popped = self.__element_stack.pop()
    196     assert popped == name
    197     if name == 'translation':
    198       self.__OnCloseTranslation()
    199 
    200   def characters(self, content):
    201     if self.__characters_callback:
    202       self.__characters_callback(self.__element_stack[-1], content)
    203 
    204   def __OnLanguage(self, lang):
    205     self.lang = lang.replace('-', '_').upper()
    206 
    207   def __OnOpenTranslation(self, translation_id):
    208     assert self.__string_id is None
    209     self.__string_id = self.__translation_ids.get(translation_id)
    210     if self.__string_id is not None:
    211       self.__characters_callback = self.__OnTranslationText
    212 
    213   def __OnTranslationText(self, containing_element, message_text):
    214     if message_text and containing_element == 'translation':
    215       self.__text_scraps.append(message_text)
    216 
    217   def __OnCloseTranslation(self):
    218     if self.__string_id is not None:
    219       self.translations[self.__string_id] = ''.join(self.__text_scraps).strip()
    220       self.__string_id = None
    221       self.__text_scraps = []
    222       self.__characters_callback = None
    223 
    224 
    225 class StringRcMaker(object):
    226   """Makes .h and .rc files containing strings and translations."""
    227   def __init__(self, name, inputs, outdir):
    228     """Constructs a maker.
    229 
    230     Args:
    231       name: The base name of the generated files (e.g.,
    232         'installer_util_strings').
    233       inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
    234       outdir: The directory into which the files will be generated.
    235     """
    236     self.name = name
    237     self.inputs = inputs
    238     self.outdir = outdir
    239 
    240   def MakeFiles(self):
    241     translated_strings = self.__ReadSourceAndTranslatedStrings()
    242     self.__WriteRCFile(translated_strings)
    243     self.__WriteHeaderFile(translated_strings)
    244 
    245   class __TranslationData(object):
    246     """A container of information about a single translation."""
    247     def __init__(self, resource_id_str, language, translation):
    248       self.resource_id_str = resource_id_str
    249       self.language = language
    250       self.translation = translation
    251 
    252     def __cmp__(self, other):
    253       """Allow __TranslationDatas to be sorted by id then by language."""
    254       id_result = cmp(self.resource_id_str, other.resource_id_str)
    255       return cmp(self.language, other.language) if id_result == 0 else id_result
    256 
    257   def __ReadSourceAndTranslatedStrings(self):
    258     """Reads the source strings and translations from all inputs."""
    259     translated_strings = []
    260     for grd_file, xtb_dir in self.inputs:
    261       # Get the name of the grd file sans extension.
    262       source_name = os.path.splitext(os.path.basename(grd_file))[0]
    263       # Compute a glob for the translation files.
    264       xtb_pattern = os.path.join(os.path.dirname(grd_file), xtb_dir,
    265                                  '%s*.xtb' % source_name)
    266       translated_strings.extend(
    267         self.__ReadSourceAndTranslationsFrom(grd_file, glob.glob(xtb_pattern)))
    268     translated_strings.sort()
    269     return translated_strings
    270 
    271   def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
    272     """Reads source strings and translations for a .grd file.
    273 
    274     Reads the source strings and all available translations for the messages
    275     identified by STRING_IDS. The source string is used where translations are
    276     missing.
    277 
    278     Args:
    279       grd_file: Path to a .grd file.
    280       xtb_files: List of paths to .xtb files.
    281 
    282     Returns:
    283       An unsorted list of __TranslationData instances.
    284     """
    285     sax_parser = sax.make_parser()
    286 
    287     # Read the source (en-US) string from the .grd file.
    288     grd_handler = GrdHandler(STRING_IDS)
    289     sax_parser.setContentHandler(grd_handler)
    290     sax_parser.parse(grd_file)
    291     source_strings = grd_handler.messages
    292 
    293     # Manually put the source strings as en-US in the list of translated
    294     # strings.
    295     translated_strings = []
    296     for string_id, message_text in source_strings.iteritems():
    297       translated_strings.append(self.__TranslationData(string_id,
    298                                                        'EN_US',
    299                                                        message_text))
    300 
    301     # Generate the message ID for each source string to correlate it with its
    302     # translations in the .xtb files.
    303     translation_ids = {
    304       tclib.GenerateMessageId(message_text): string_id
    305       for (string_id, message_text) in source_strings.iteritems()
    306     }
    307 
    308     # Gather the translated strings from the .xtb files. Use the en-US string
    309     # for any message lacking a translation.
    310     xtb_handler = XtbHandler(translation_ids)
    311     sax_parser.setContentHandler(xtb_handler)
    312     for xtb_filename in xtb_files:
    313       sax_parser.parse(xtb_filename)
    314       for string_id, message_text in source_strings.iteritems():
    315         translated_string = xtb_handler.translations.get(string_id,
    316                                                          message_text)
    317         translated_strings.append(self.__TranslationData(string_id,
    318                                                          xtb_handler.lang,
    319                                                          translated_string))
    320     return translated_strings
    321 
    322   def __WriteRCFile(self, translated_strings):
    323     """Writes a resource file with the strings provided in |translated_strings|.
    324     """
    325     HEADER_TEXT = (
    326       u'#include "%s.h"\n\n'
    327       u'STRINGTABLE\n'
    328       u'BEGIN\n'
    329       ) % self.name
    330 
    331     FOOTER_TEXT = (
    332       u'END\n'
    333     )
    334 
    335     with io.open(os.path.join(self.outdir, self.name + '.rc'),
    336                  mode='w',
    337                  encoding='utf-16',
    338                  newline='\n') as outfile:
    339       outfile.write(HEADER_TEXT)
    340       for translation in translated_strings:
    341         # Escape special characters for the rc file.
    342         escaped_text = (translation.translation.replace('"', '""')
    343                        .replace('\t', '\\t')
    344                        .replace('\n', '\\n'))
    345         outfile.write(u'  %s "%s"\n' %
    346                       (translation.resource_id_str + '_' + translation.language,
    347                        escaped_text))
    348       outfile.write(FOOTER_TEXT)
    349 
    350   def __WriteHeaderFile(self, translated_strings):
    351     """Writes a .h file with resource ids."""
    352     # TODO(grt): Stream the lines to the file rather than building this giant
    353     # list of lines first.
    354     lines = []
    355     do_languages_lines = ['\n#define DO_LANGUAGES']
    356     installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
    357 
    358     # Write the values for how the languages ids are offset.
    359     seen_languages = set()
    360     offset_id = 0
    361     for translation_data in translated_strings:
    362       lang = translation_data.language
    363       if lang not in seen_languages:
    364         seen_languages.add(lang)
    365         lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
    366         do_languages_lines.append('  HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
    367                                   % (lang.replace('_', '-').lower(), lang))
    368         offset_id += 1
    369       else:
    370         break
    371 
    372     # Write the resource ids themselves.
    373     resource_id = FIRST_RESOURCE_ID
    374     for translation_data in translated_strings:
    375       lines.append('#define %s %s' % (translation_data.resource_id_str + '_' +
    376                                       translation_data.language,
    377                                       resource_id))
    378       resource_id += 1
    379 
    380     # Write out base ID values.
    381     for string_id in STRING_IDS:
    382       lines.append('#define %s_BASE %s_%s' % (string_id,
    383                                               string_id,
    384                                               translated_strings[0].language))
    385       installer_string_mapping_lines.append('  HANDLE_STRING(%s_BASE, %s)'
    386                                             % (string_id, string_id))
    387 
    388     with open(os.path.join(self.outdir, self.name + '.h'), 'wb') as outfile:
    389       outfile.write('\n'.join(lines))
    390       outfile.write('\n#ifndef RC_INVOKED')
    391       outfile.write(' \\\n'.join(do_languages_lines))
    392       outfile.write(' \\\n'.join(installer_string_mapping_lines))
    393       # .rc files must end in a new line
    394       outfile.write('\n#endif  // ndef RC_INVOKED\n')
    395 
    396 
    397 def ParseCommandLine():
    398   def GrdPathAndXtbDirPair(string):
    399     """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
    400     """
    401     parts = string.split(':')
    402     if len(parts) is not 2:
    403       raise argparse.ArgumentTypeError('%r is not grd_path:xtb_dir')
    404     return (parts[0], parts[1])
    405 
    406   parser = argparse.ArgumentParser(
    407     description='Generate .h and .rc files for installer strings.')
    408   parser.add_argument('-i', action='append',
    409                       type=GrdPathAndXtbDirPair,
    410                       required=True,
    411                       help='path to .grd file:relative path to .xtb dir',
    412                       metavar='GRDFILE:XTBDIR',
    413                       dest='inputs')
    414   parser.add_argument('-o',
    415                       required=True,
    416                       help='output directory for generated .rc and .h files',
    417                       dest='outdir')
    418   parser.add_argument('-n',
    419                       required=True,
    420                       help='base name of generated .rc and .h files',
    421                       dest='name')
    422   return parser.parse_args()
    423 
    424 
    425 def main():
    426   args = ParseCommandLine()
    427   StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
    428   return 0
    429 
    430 
    431 if '__main__' == __name__:
    432   sys.exit(main())
    433