Do not announce robot account token before account ID is available
[chromium-blink-merge.git] / chrome / installer / util / prebuild / create_string_rc.py
blob8c4bf225eb795a8d43655b26b7945d26be6e074e
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_INSTALL_INCONSISTENT_UPDATE_POLICY',
68 'IDS_OEM_MAIN_SHORTCUT_NAME',
69 'IDS_SHORTCUT_TOOLTIP',
70 'IDS_SHORTCUT_NEW_WINDOW',
71 'IDS_APP_LIST_SHORTCUT_NAME',
72 'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
73 'IDS_APP_SHORTCUTS_SUBDIR_NAME',
74 'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
75 'IDS_INBOUND_MDNS_RULE_NAME',
76 'IDS_INBOUND_MDNS_RULE_NAME_CANARY',
77 'IDS_INBOUND_MDNS_RULE_DESCRIPTION',
78 'IDS_INBOUND_MDNS_RULE_DESCRIPTION_CANARY',
81 # The ID of the first resource string.
82 FIRST_RESOURCE_ID = 1600
85 class GrdHandler(sax.handler.ContentHandler):
86 """Extracts selected strings from a .grd file.
88 Attributes:
89 messages: A dict mapping string identifiers to their corresponding messages.
90 """
91 def __init__(self, string_ids):
92 """Constructs a handler that reads selected strings from a .grd file.
94 The dict attribute |messages| is populated with the strings that are read.
96 Args:
97 string_ids: A list of message identifiers to extract.
98 """
99 sax.handler.ContentHandler.__init__(self)
100 self.messages = {}
101 self.__id_set = set(string_ids)
102 self.__message_name = None
103 self.__element_stack = []
104 self.__text_scraps = []
105 self.__characters_callback = None
107 def startElement(self, name, attrs):
108 self.__element_stack.append(name)
109 if name == 'message':
110 self.__OnOpenMessage(attrs.getValue('name'))
112 def endElement(self, name):
113 popped = self.__element_stack.pop()
114 assert popped == name
115 if name == 'message':
116 self.__OnCloseMessage()
118 def characters(self, content):
119 if self.__characters_callback:
120 self.__characters_callback(self.__element_stack[-1], content)
122 def __IsExtractingMessage(self):
123 """Returns True if a message is currently being extracted."""
124 return self.__message_name is not None
126 def __OnOpenMessage(self, message_name):
127 """Invoked at the start of a <message> with message's name."""
128 assert not self.__IsExtractingMessage()
129 self.__message_name = (message_name if message_name in self.__id_set
130 else None)
131 if self.__message_name:
132 self.__characters_callback = self.__OnMessageText
134 def __OnMessageText(self, containing_element, message_text):
135 """Invoked to handle a block of text for a message."""
136 if message_text and (containing_element == 'message' or
137 containing_element == 'ph'):
138 self.__text_scraps.append(message_text)
140 def __OnCloseMessage(self):
141 """Invoked at the end of a message."""
142 if self.__IsExtractingMessage():
143 self.messages[self.__message_name] = ''.join(self.__text_scraps).strip()
144 self.__message_name = None
145 self.__text_scraps = []
146 self.__characters_callback = None
149 class XtbHandler(sax.handler.ContentHandler):
150 """Extracts selected translations from an .xrd file.
152 Populates the |lang| and |translations| attributes with the language and
153 selected strings of an .xtb file. Instances may be re-used to read the same
154 set of translations from multiple .xtb files.
156 Attributes:
157 translations: A mapping of translation ids to strings.
158 lang: The language parsed from the .xtb file.
160 def __init__(self, translation_ids):
161 """Constructs an instance to parse the given strings from an .xtb file.
163 Args:
164 translation_ids: a mapping of translation ids to their string
165 identifiers for the translations to be extracted.
167 sax.handler.ContentHandler.__init__(self)
168 self.lang = None
169 self.translations = None
170 self.__translation_ids = translation_ids
171 self.__element_stack = []
172 self.__string_id = None
173 self.__text_scraps = []
174 self.__characters_callback = None
176 def startDocument(self):
177 # Clear the lang and translations since a new document is being parsed.
178 self.lang = ''
179 self.translations = {}
181 def startElement(self, name, attrs):
182 self.__element_stack.append(name)
183 # translationbundle is the document element, and hosts the lang id.
184 if len(self.__element_stack) == 1:
185 assert name == 'translationbundle'
186 self.__OnLanguage(attrs.getValue('lang'))
187 if name == 'translation':
188 self.__OnOpenTranslation(attrs.getValue('id'))
190 def endElement(self, name):
191 popped = self.__element_stack.pop()
192 assert popped == name
193 if name == 'translation':
194 self.__OnCloseTranslation()
196 def characters(self, content):
197 if self.__characters_callback:
198 self.__characters_callback(self.__element_stack[-1], content)
200 def __OnLanguage(self, lang):
201 self.lang = lang.replace('-', '_').upper()
203 def __OnOpenTranslation(self, translation_id):
204 assert self.__string_id is None
205 self.__string_id = self.__translation_ids.get(translation_id)
206 if self.__string_id is not None:
207 self.__characters_callback = self.__OnTranslationText
209 def __OnTranslationText(self, containing_element, message_text):
210 if message_text and containing_element == 'translation':
211 self.__text_scraps.append(message_text)
213 def __OnCloseTranslation(self):
214 if self.__string_id is not None:
215 self.translations[self.__string_id] = ''.join(self.__text_scraps).strip()
216 self.__string_id = None
217 self.__text_scraps = []
218 self.__characters_callback = None
221 class StringRcMaker(object):
222 """Makes .h and .rc files containing strings and translations."""
223 def __init__(self, name, inputs, outdir):
224 """Constructs a maker.
226 Args:
227 name: The base name of the generated files (e.g.,
228 'installer_util_strings').
229 inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
230 outdir: The directory into which the files will be generated.
232 self.name = name
233 self.inputs = inputs
234 self.outdir = outdir
236 def MakeFiles(self):
237 translated_strings = self.__ReadSourceAndTranslatedStrings()
238 self.__WriteRCFile(translated_strings)
239 self.__WriteHeaderFile(translated_strings)
241 class __TranslationData(object):
242 """A container of information about a single translation."""
243 def __init__(self, resource_id_str, language, translation):
244 self.resource_id_str = resource_id_str
245 self.language = language
246 self.translation = translation
248 def __cmp__(self, other):
249 """Allow __TranslationDatas to be sorted by id then by language."""
250 id_result = cmp(self.resource_id_str, other.resource_id_str)
251 return cmp(self.language, other.language) if id_result == 0 else id_result
253 def __ReadSourceAndTranslatedStrings(self):
254 """Reads the source strings and translations from all inputs."""
255 translated_strings = []
256 for grd_file, xtb_dir in self.inputs:
257 # Get the name of the grd file sans extension.
258 source_name = os.path.splitext(os.path.basename(grd_file))[0]
259 # Compute a glob for the translation files.
260 xtb_pattern = os.path.join(os.path.dirname(grd_file), xtb_dir,
261 '%s*.xtb' % source_name)
262 translated_strings.extend(
263 self.__ReadSourceAndTranslationsFrom(grd_file, glob.glob(xtb_pattern)))
264 translated_strings.sort()
265 return translated_strings
267 def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
268 """Reads source strings and translations for a .grd file.
270 Reads the source strings and all available translations for the messages
271 identified by STRING_IDS. The source string is used where translations are
272 missing.
274 Args:
275 grd_file: Path to a .grd file.
276 xtb_files: List of paths to .xtb files.
278 Returns:
279 An unsorted list of __TranslationData instances.
281 sax_parser = sax.make_parser()
283 # Read the source (en-US) string from the .grd file.
284 grd_handler = GrdHandler(STRING_IDS)
285 sax_parser.setContentHandler(grd_handler)
286 sax_parser.parse(grd_file)
287 source_strings = grd_handler.messages
289 # Manually put the source strings as en-US in the list of translated
290 # strings.
291 translated_strings = []
292 for string_id, message_text in source_strings.iteritems():
293 translated_strings.append(self.__TranslationData(string_id,
294 'EN_US',
295 message_text))
297 # Generate the message ID for each source string to correlate it with its
298 # translations in the .xtb files.
299 translation_ids = {
300 tclib.GenerateMessageId(message_text): string_id
301 for (string_id, message_text) in source_strings.iteritems()
304 # Gather the translated strings from the .xtb files. Use the en-US string
305 # for any message lacking a translation.
306 xtb_handler = XtbHandler(translation_ids)
307 sax_parser.setContentHandler(xtb_handler)
308 for xtb_filename in xtb_files:
309 sax_parser.parse(xtb_filename)
310 for string_id, message_text in source_strings.iteritems():
311 translated_string = xtb_handler.translations.get(string_id,
312 message_text)
313 translated_strings.append(self.__TranslationData(string_id,
314 xtb_handler.lang,
315 translated_string))
316 return translated_strings
318 def __WriteRCFile(self, translated_strings):
319 """Writes a resource file with the strings provided in |translated_strings|.
321 HEADER_TEXT = (
322 u'#include "%s.h"\n\n'
323 u'STRINGTABLE\n'
324 u'BEGIN\n'
325 ) % self.name
327 FOOTER_TEXT = (
328 u'END\n'
331 with io.open(os.path.join(self.outdir, self.name + '.rc'),
332 mode='w',
333 encoding='utf-16',
334 newline='\n') as outfile:
335 outfile.write(HEADER_TEXT)
336 for translation in translated_strings:
337 # Escape special characters for the rc file.
338 escaped_text = (translation.translation.replace('"', '""')
339 .replace('\t', '\\t')
340 .replace('\n', '\\n'))
341 outfile.write(u' %s "%s"\n' %
342 (translation.resource_id_str + '_' + translation.language,
343 escaped_text))
344 outfile.write(FOOTER_TEXT)
346 def __WriteHeaderFile(self, translated_strings):
347 """Writes a .h file with resource ids."""
348 # TODO(grt): Stream the lines to the file rather than building this giant
349 # list of lines first.
350 lines = []
351 do_languages_lines = ['\n#define DO_LANGUAGES']
352 installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
354 # Write the values for how the languages ids are offset.
355 seen_languages = set()
356 offset_id = 0
357 for translation_data in translated_strings:
358 lang = translation_data.language
359 if lang not in seen_languages:
360 seen_languages.add(lang)
361 lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
362 do_languages_lines.append(' HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
363 % (lang.replace('_', '-').lower(), lang))
364 offset_id += 1
365 else:
366 break
368 # Write the resource ids themselves.
369 resource_id = FIRST_RESOURCE_ID
370 for translation_data in translated_strings:
371 lines.append('#define %s %s' % (translation_data.resource_id_str + '_' +
372 translation_data.language,
373 resource_id))
374 resource_id += 1
376 # Write out base ID values.
377 for string_id in STRING_IDS:
378 lines.append('#define %s_BASE %s_%s' % (string_id,
379 string_id,
380 translated_strings[0].language))
381 installer_string_mapping_lines.append(' HANDLE_STRING(%s_BASE, %s)'
382 % (string_id, string_id))
384 with open(os.path.join(self.outdir, self.name + '.h'), 'wb') as outfile:
385 outfile.write('\n'.join(lines))
386 outfile.write('\n#ifndef RC_INVOKED')
387 outfile.write(' \\\n'.join(do_languages_lines))
388 outfile.write(' \\\n'.join(installer_string_mapping_lines))
389 # .rc files must end in a new line
390 outfile.write('\n#endif // ndef RC_INVOKED\n')
393 def ParseCommandLine():
394 def GrdPathAndXtbDirPair(string):
395 """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
397 parts = string.split(':')
398 if len(parts) is not 2:
399 raise argparse.ArgumentTypeError('%r is not grd_path:xtb_dir')
400 return (parts[0], parts[1])
402 parser = argparse.ArgumentParser(
403 description='Generate .h and .rc files for installer strings.')
404 parser.add_argument('-i', action='append',
405 type=GrdPathAndXtbDirPair,
406 required=True,
407 help='path to .grd file:relative path to .xtb dir',
408 metavar='GRDFILE:XTBDIR',
409 dest='inputs')
410 parser.add_argument('-o',
411 required=True,
412 help='output directory for generated .rc and .h files',
413 dest='outdir')
414 parser.add_argument('-n',
415 required=True,
416 help='base name of generated .rc and .h files',
417 dest='name')
418 return parser.parse_args()
421 def main():
422 args = ParseCommandLine()
423 StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
424 return 0
427 if '__main__' == __name__:
428 sys.exit(main())