Adding instrumentation to locate the source of jankiness
[chromium-blink-merge.git] / chrome / installer / util / prebuild / create_string_rc.py
blob96ca2ab15c072a8720b9df1c5c3a6e35d7b5d640
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.
6 """Generates .h and .rc files for installer strings. Run "python
7 create_string_rc.py" for usage details.
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.
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:
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
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
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 """
32 import argparse
33 import glob
34 import io
35 import os
36 import sys
37 from xml import sax
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'))
43 from grit.extern import tclib
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',
85 # The ID of the first resource string.
86 FIRST_RESOURCE_ID = 1600
89 class GrdHandler(sax.handler.ContentHandler):
90 """Extracts selected strings from a .grd file.
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.
98 The dict attribute |messages| is populated with the strings that are read.
100 Args:
101 string_ids: A list of message identifiers to extract.
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
111 def startElement(self, name, attrs):
112 self.__element_stack.append(name)
113 if name == 'message':
114 self.__OnOpenMessage(attrs.getValue('name'))
116 def endElement(self, name):
117 popped = self.__element_stack.pop()
118 assert popped == name
119 if name == 'message':
120 self.__OnCloseMessage()
122 def characters(self, content):
123 if self.__characters_callback:
124 self.__characters_callback(self.__element_stack[-1], content)
126 def __IsExtractingMessage(self):
127 """Returns True if a message is currently being extracted."""
128 return self.__message_name is not None
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
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)
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
153 class XtbHandler(sax.handler.ContentHandler):
154 """Extracts selected translations from an .xrd file.
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.
160 Attributes:
161 translations: A mapping of translation ids to strings.
162 lang: The language parsed from the .xtb file.
164 def __init__(self, translation_ids):
165 """Constructs an instance to parse the given strings from an .xtb file.
167 Args:
168 translation_ids: a mapping of translation ids to their string
169 identifiers for the translations to be extracted.
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
180 def startDocument(self):
181 # Clear the lang and translations since a new document is being parsed.
182 self.lang = ''
183 self.translations = {}
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'))
194 def endElement(self, name):
195 popped = self.__element_stack.pop()
196 assert popped == name
197 if name == 'translation':
198 self.__OnCloseTranslation()
200 def characters(self, content):
201 if self.__characters_callback:
202 self.__characters_callback(self.__element_stack[-1], content)
204 def __OnLanguage(self, lang):
205 self.lang = lang.replace('-', '_').upper()
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
213 def __OnTranslationText(self, containing_element, message_text):
214 if message_text and containing_element == 'translation':
215 self.__text_scraps.append(message_text)
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
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.
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.
236 self.name = name
237 self.inputs = inputs
238 self.outdir = outdir
240 def MakeFiles(self):
241 translated_strings = self.__ReadSourceAndTranslatedStrings()
242 self.__WriteRCFile(translated_strings)
243 self.__WriteHeaderFile(translated_strings)
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
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
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
271 def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
272 """Reads source strings and translations for a .grd file.
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.
278 Args:
279 grd_file: Path to a .grd file.
280 xtb_files: List of paths to .xtb files.
282 Returns:
283 An unsorted list of __TranslationData instances.
285 sax_parser = sax.make_parser()
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
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))
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()
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
322 def __WriteRCFile(self, translated_strings):
323 """Writes a resource file with the strings provided in |translated_strings|.
325 HEADER_TEXT = (
326 u'#include "%s.h"\n\n'
327 u'STRINGTABLE\n'
328 u'BEGIN\n'
329 ) % self.name
331 FOOTER_TEXT = (
332 u'END\n'
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)
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']
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
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
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))
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')
397 def ParseCommandLine():
398 def GrdPathAndXtbDirPair(string):
399 """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
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])
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()
425 def main():
426 args = ParseCommandLine()
427 StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
428 return 0
431 if '__main__' == __name__:
432 sys.exit(main())