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