Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / installer / util / prebuild / create_string_rc.py
blobfe9b6fe5aa66646d1b7fba4ebc616a13203dac4d
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', # Used in App Launcher registry.
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_FAILED',
57 'IDS_SAME_VERSION_REPAIR_FAILED',
58 'IDS_SETUP_PATCH_FAILED',
59 'IDS_INSTALL_OS_NOT_SUPPORTED',
60 'IDS_INSTALL_OS_ERROR',
61 'IDS_INSTALL_TEMP_DIR_FAILED',
62 'IDS_INSTALL_UNCOMPRESSION_FAILED',
63 'IDS_INSTALL_INVALID_ARCHIVE',
64 'IDS_INSTALL_INSUFFICIENT_RIGHTS',
65 'IDS_INSTALL_NO_PRODUCTS_TO_UPDATE',
66 'IDS_INSTALL_MULTI_INSTALLATION_EXISTS',
67 'IDS_OEM_MAIN_SHORTCUT_NAME',
68 'IDS_SHORTCUT_TOOLTIP',
69 'IDS_SHORTCUT_NEW_WINDOW',
70 'IDS_APP_LIST_SHORTCUT_NAME',
71 'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
72 'IDS_APP_SHORTCUTS_SUBDIR_NAME',
73 'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
74 'IDS_INBOUND_MDNS_RULE_NAME',
75 'IDS_INBOUND_MDNS_RULE_NAME_CANARY',
76 'IDS_INBOUND_MDNS_RULE_DESCRIPTION',
77 'IDS_INBOUND_MDNS_RULE_DESCRIPTION_CANARY',
80 # The ID of the first resource string.
81 FIRST_RESOURCE_ID = 1600
84 class GrdHandler(sax.handler.ContentHandler):
85 """Extracts selected strings from a .grd file.
87 Attributes:
88 messages: A dict mapping string identifiers to their corresponding messages.
89 """
90 def __init__(self, string_ids):
91 """Constructs a handler that reads selected strings from a .grd file.
93 The dict attribute |messages| is populated with the strings that are read.
95 Args:
96 string_ids: A list of message identifiers to extract.
97 """
98 sax.handler.ContentHandler.__init__(self)
99 self.messages = {}
100 self.__id_set = set(string_ids)
101 self.__message_name = None
102 self.__element_stack = []
103 self.__text_scraps = []
104 self.__characters_callback = None
106 def startElement(self, name, attrs):
107 self.__element_stack.append(name)
108 if name == 'message':
109 self.__OnOpenMessage(attrs.getValue('name'))
111 def endElement(self, name):
112 popped = self.__element_stack.pop()
113 assert popped == name
114 if name == 'message':
115 self.__OnCloseMessage()
117 def characters(self, content):
118 if self.__characters_callback:
119 self.__characters_callback(self.__element_stack[-1], content)
121 def __IsExtractingMessage(self):
122 """Returns True if a message is currently being extracted."""
123 return self.__message_name is not None
125 def __OnOpenMessage(self, message_name):
126 """Invoked at the start of a <message> with message's name."""
127 assert not self.__IsExtractingMessage()
128 self.__message_name = (message_name if message_name in self.__id_set
129 else None)
130 if self.__message_name:
131 self.__characters_callback = self.__OnMessageText
133 def __OnMessageText(self, containing_element, message_text):
134 """Invoked to handle a block of text for a message."""
135 if message_text and (containing_element == 'message' or
136 containing_element == 'ph'):
137 self.__text_scraps.append(message_text)
139 def __OnCloseMessage(self):
140 """Invoked at the end of a message."""
141 if self.__IsExtractingMessage():
142 self.messages[self.__message_name] = ''.join(self.__text_scraps).strip()
143 self.__message_name = None
144 self.__text_scraps = []
145 self.__characters_callback = None
148 class XtbHandler(sax.handler.ContentHandler):
149 """Extracts selected translations from an .xrd file.
151 Populates the |lang| and |translations| attributes with the language and
152 selected strings of an .xtb file. Instances may be re-used to read the same
153 set of translations from multiple .xtb files.
155 Attributes:
156 translations: A mapping of translation ids to strings.
157 lang: The language parsed from the .xtb file.
159 def __init__(self, translation_ids):
160 """Constructs an instance to parse the given strings from an .xtb file.
162 Args:
163 translation_ids: a mapping of translation ids to their string
164 identifiers for the translations to be extracted.
166 sax.handler.ContentHandler.__init__(self)
167 self.lang = None
168 self.translations = None
169 self.__translation_ids = translation_ids
170 self.__element_stack = []
171 self.__string_id = None
172 self.__text_scraps = []
173 self.__characters_callback = None
175 def startDocument(self):
176 # Clear the lang and translations since a new document is being parsed.
177 self.lang = ''
178 self.translations = {}
180 def startElement(self, name, attrs):
181 self.__element_stack.append(name)
182 # translationbundle is the document element, and hosts the lang id.
183 if len(self.__element_stack) == 1:
184 assert name == 'translationbundle'
185 self.__OnLanguage(attrs.getValue('lang'))
186 if name == 'translation':
187 self.__OnOpenTranslation(attrs.getValue('id'))
189 def endElement(self, name):
190 popped = self.__element_stack.pop()
191 assert popped == name
192 if name == 'translation':
193 self.__OnCloseTranslation()
195 def characters(self, content):
196 if self.__characters_callback:
197 self.__characters_callback(self.__element_stack[-1], content)
199 def __OnLanguage(self, lang):
200 self.lang = lang.replace('-', '_').upper()
202 def __OnOpenTranslation(self, translation_id):
203 assert self.__string_id is None
204 self.__string_id = self.__translation_ids.get(translation_id)
205 if self.__string_id is not None:
206 self.__characters_callback = self.__OnTranslationText
208 def __OnTranslationText(self, containing_element, message_text):
209 if message_text and containing_element == 'translation':
210 self.__text_scraps.append(message_text)
212 def __OnCloseTranslation(self):
213 if self.__string_id is not None:
214 self.translations[self.__string_id] = ''.join(self.__text_scraps).strip()
215 self.__string_id = None
216 self.__text_scraps = []
217 self.__characters_callback = None
220 class StringRcMaker(object):
221 """Makes .h and .rc files containing strings and translations."""
222 def __init__(self, name, inputs, outdir):
223 """Constructs a maker.
225 Args:
226 name: The base name of the generated files (e.g.,
227 'installer_util_strings').
228 inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
229 outdir: The directory into which the files will be generated.
231 self.name = name
232 self.inputs = inputs
233 self.outdir = outdir
235 def MakeFiles(self):
236 translated_strings = self.__ReadSourceAndTranslatedStrings()
237 self.__WriteRCFile(translated_strings)
238 self.__WriteHeaderFile(translated_strings)
240 class __TranslationData(object):
241 """A container of information about a single translation."""
242 def __init__(self, resource_id_str, language, translation):
243 self.resource_id_str = resource_id_str
244 self.language = language
245 self.translation = translation
247 def __cmp__(self, other):
248 """Allow __TranslationDatas to be sorted by id then by language."""
249 id_result = cmp(self.resource_id_str, other.resource_id_str)
250 return cmp(self.language, other.language) if id_result == 0 else id_result
252 def __ReadSourceAndTranslatedStrings(self):
253 """Reads the source strings and translations from all inputs."""
254 translated_strings = []
255 for grd_file, xtb_dir in self.inputs:
256 # Get the name of the grd file sans extension.
257 source_name = os.path.splitext(os.path.basename(grd_file))[0]
258 # Compute a glob for the translation files.
259 xtb_pattern = os.path.join(os.path.dirname(grd_file), xtb_dir,
260 '%s*.xtb' % source_name)
261 translated_strings.extend(
262 self.__ReadSourceAndTranslationsFrom(grd_file, glob.glob(xtb_pattern)))
263 translated_strings.sort()
264 return translated_strings
266 def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
267 """Reads source strings and translations for a .grd file.
269 Reads the source strings and all available translations for the messages
270 identified by STRING_IDS. The source string is used where translations are
271 missing.
273 Args:
274 grd_file: Path to a .grd file.
275 xtb_files: List of paths to .xtb files.
277 Returns:
278 An unsorted list of __TranslationData instances.
280 sax_parser = sax.make_parser()
282 # Read the source (en-US) string from the .grd file.
283 grd_handler = GrdHandler(STRING_IDS)
284 sax_parser.setContentHandler(grd_handler)
285 sax_parser.parse(grd_file)
286 source_strings = grd_handler.messages
288 # Manually put the source strings as en-US in the list of translated
289 # strings.
290 translated_strings = []
291 for string_id, message_text in source_strings.iteritems():
292 translated_strings.append(self.__TranslationData(string_id,
293 'EN_US',
294 message_text))
296 # Generate the message ID for each source string to correlate it with its
297 # translations in the .xtb files.
298 translation_ids = {
299 tclib.GenerateMessageId(message_text): string_id
300 for (string_id, message_text) in source_strings.iteritems()
303 # Gather the translated strings from the .xtb files. Use the en-US string
304 # for any message lacking a translation.
305 xtb_handler = XtbHandler(translation_ids)
306 sax_parser.setContentHandler(xtb_handler)
307 for xtb_filename in xtb_files:
308 sax_parser.parse(xtb_filename)
309 for string_id, message_text in source_strings.iteritems():
310 translated_string = xtb_handler.translations.get(string_id,
311 message_text)
312 translated_strings.append(self.__TranslationData(string_id,
313 xtb_handler.lang,
314 translated_string))
315 return translated_strings
317 def __WriteRCFile(self, translated_strings):
318 """Writes a resource file with the strings provided in |translated_strings|.
320 HEADER_TEXT = (
321 u'#include "%s.h"\n\n'
322 u'STRINGTABLE\n'
323 u'BEGIN\n'
324 ) % self.name
326 FOOTER_TEXT = (
327 u'END\n'
330 with io.open(os.path.join(self.outdir, self.name + '.rc'),
331 mode='w',
332 encoding='utf-16',
333 newline='\n') as outfile:
334 outfile.write(HEADER_TEXT)
335 for translation in translated_strings:
336 # Escape special characters for the rc file.
337 escaped_text = (translation.translation.replace('"', '""')
338 .replace('\t', '\\t')
339 .replace('\n', '\\n'))
340 outfile.write(u' %s "%s"\n' %
341 (translation.resource_id_str + '_' + translation.language,
342 escaped_text))
343 outfile.write(FOOTER_TEXT)
345 def __WriteHeaderFile(self, translated_strings):
346 """Writes a .h file with resource ids."""
347 # TODO(grt): Stream the lines to the file rather than building this giant
348 # list of lines first.
349 lines = []
350 do_languages_lines = ['\n#define DO_LANGUAGES']
351 installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
353 # Write the values for how the languages ids are offset.
354 seen_languages = set()
355 offset_id = 0
356 for translation_data in translated_strings:
357 lang = translation_data.language
358 if lang not in seen_languages:
359 seen_languages.add(lang)
360 lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
361 do_languages_lines.append(' HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
362 % (lang.replace('_', '-').lower(), lang))
363 offset_id += 1
364 else:
365 break
367 # Write the resource ids themselves.
368 resource_id = FIRST_RESOURCE_ID
369 for translation_data in translated_strings:
370 lines.append('#define %s %s' % (translation_data.resource_id_str + '_' +
371 translation_data.language,
372 resource_id))
373 resource_id += 1
375 # Write out base ID values.
376 for string_id in STRING_IDS:
377 lines.append('#define %s_BASE %s_%s' % (string_id,
378 string_id,
379 translated_strings[0].language))
380 installer_string_mapping_lines.append(' HANDLE_STRING(%s_BASE, %s)'
381 % (string_id, string_id))
383 with open(os.path.join(self.outdir, self.name + '.h'), 'wb') as outfile:
384 outfile.write('\n'.join(lines))
385 outfile.write('\n#ifndef RC_INVOKED')
386 outfile.write(' \\\n'.join(do_languages_lines))
387 outfile.write(' \\\n'.join(installer_string_mapping_lines))
388 # .rc files must end in a new line
389 outfile.write('\n#endif // ndef RC_INVOKED\n')
392 def ParseCommandLine():
393 def GrdPathAndXtbDirPair(string):
394 """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
396 parts = string.split(':')
397 if len(parts) is not 2:
398 raise argparse.ArgumentTypeError('%r is not grd_path:xtb_dir')
399 return (parts[0], parts[1])
401 parser = argparse.ArgumentParser(
402 description='Generate .h and .rc files for installer strings.')
403 parser.add_argument('-i', action='append',
404 type=GrdPathAndXtbDirPair,
405 required=True,
406 help='path to .grd file:relative path to .xtb dir',
407 metavar='GRDFILE:XTBDIR',
408 dest='inputs')
409 parser.add_argument('-o',
410 required=True,
411 help='output directory for generated .rc and .h files',
412 dest='outdir')
413 parser.add_argument('-n',
414 required=True,
415 help='base name of generated .rc and .h files',
416 dest='name')
417 return parser.parse_args()
420 def main():
421 args = ParseCommandLine()
422 StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
423 return 0
426 if '__main__' == __name__:
427 sys.exit(main())