Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / installer / util / prebuild / create_string_rc.py
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())