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
17 #define IDS_L10N_OFFSET_AR 0
18 #define IDS_L10N_OFFSET_BG 1
19 #define IDS_L10N_OFFSET_CA 2
21 #define IDS_L10N_OFFSET_ZH_TW 41
23 #define IDS_MY_STRING_AR 1600
24 #define IDS_MY_STRING_BG 1601
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.
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.
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',
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_UNINSTALL_COMPLETE',
68 'IDS_INSTALL_DIR_IN_USE',
69 'IDS_INSTALL_MULTI_INSTALLATION_EXISTS',
70 'IDS_INSTALL_INCONSISTENT_UPDATE_POLICY',
71 'IDS_OEM_MAIN_SHORTCUT_NAME',
72 'IDS_SHORTCUT_TOOLTIP',
73 'IDS_SHORTCUT_NEW_WINDOW',
74 'IDS_APP_LAUNCHER_PRODUCT_DESCRIPTION',
75 'IDS_APP_LAUNCHER_SHORTCUT_TOOLTIP',
76 'IDS_UNINSTALL_APP_LAUNCHER',
77 'IDS_APP_LIST_SHORTCUT_NAME',
78 'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
79 'IDS_APP_SHORTCUTS_SUBDIR_NAME',
80 'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
83 # The ID of the first resource string.
84 FIRST_RESOURCE_ID = 1600
87 class GrdHandler(sax.handler.ContentHandler):
88 """Extracts selected strings from a .grd file.
91 messages: A dict mapping string identifiers to their corresponding messages.
93 def __init__(self, string_ids):
94 """Constructs a handler that reads selected strings from a .grd file.
96 The dict attribute |messages| is populated with the strings that are read.
99 string_ids: A list of message identifiers to extract.
101 sax.handler.ContentHandler.__init__(self)
103 self.__id_set = set(string_ids)
104 self.__message_name = None
105 self.__element_stack = []
106 self.__text_scraps = []
107 self.__characters_callback = None
109 def startElement(self, name, attrs):
110 self.__element_stack.append(name)
111 if name == 'message':
112 self.__OnOpenMessage(attrs.getValue('name'))
114 def endElement(self, name):
115 popped = self.__element_stack.pop()
116 assert popped == name
117 if name == 'message':
118 self.__OnCloseMessage()
120 def characters(self, content):
121 if self.__characters_callback:
122 self.__characters_callback(self.__element_stack[-1], content)
124 def __IsExtractingMessage(self):
125 """Returns True if a message is currently being extracted."""
126 return self.__message_name is not None
128 def __OnOpenMessage(self, message_name):
129 """Invoked at the start of a <message> with message's name."""
130 assert not self.__IsExtractingMessage()
131 self.__message_name = (message_name if message_name in self.__id_set
133 if self.__message_name:
134 self.__characters_callback = self.__OnMessageText
136 def __OnMessageText(self, containing_element, message_text):
137 """Invoked to handle a block of text for a message."""
138 if message_text and (containing_element == 'message' or
139 containing_element == 'ph'):
140 self.__text_scraps.append(message_text)
142 def __OnCloseMessage(self):
143 """Invoked at the end of a message."""
144 if self.__IsExtractingMessage():
145 self.messages[self.__message_name] = ''.join(self.__text_scraps).strip()
146 self.__message_name = None
147 self.__text_scraps = []
148 self.__characters_callback = None
151 class XtbHandler(sax.handler.ContentHandler):
152 """Extracts selected translations from an .xrd file.
154 Populates the |lang| and |translations| attributes with the language and
155 selected strings of an .xtb file. Instances may be re-used to read the same
156 set of translations from multiple .xtb files.
159 translations: A mapping of translation ids to strings.
160 lang: The language parsed from the .xtb file.
162 def __init__(self, translation_ids):
163 """Constructs an instance to parse the given strings from an .xtb file.
166 translation_ids: a mapping of translation ids to their string
167 identifiers for the translations to be extracted.
169 sax.handler.ContentHandler.__init__(self)
171 self.translations = None
172 self.__translation_ids = translation_ids
173 self.__element_stack = []
174 self.__string_id = None
175 self.__text_scraps = []
176 self.__characters_callback = None
178 def startDocument(self):
179 # Clear the lang and translations since a new document is being parsed.
181 self.translations = {}
183 def startElement(self, name, attrs):
184 self.__element_stack.append(name)
185 # translationbundle is the document element, and hosts the lang id.
186 if len(self.__element_stack) == 1:
187 assert name == 'translationbundle'
188 self.__OnLanguage(attrs.getValue('lang'))
189 if name == 'translation':
190 self.__OnOpenTranslation(attrs.getValue('id'))
192 def endElement(self, name):
193 popped = self.__element_stack.pop()
194 assert popped == name
195 if name == 'translation':
196 self.__OnCloseTranslation()
198 def characters(self, content):
199 if self.__characters_callback:
200 self.__characters_callback(self.__element_stack[-1], content)
202 def __OnLanguage(self, lang):
203 self.lang = lang.replace('-', '_').upper()
205 def __OnOpenTranslation(self, translation_id):
206 assert self.__string_id is None
207 self.__string_id = self.__translation_ids.get(translation_id)
208 if self.__string_id is not None:
209 self.__characters_callback = self.__OnTranslationText
211 def __OnTranslationText(self, containing_element, message_text):
212 if message_text and containing_element == 'translation':
213 self.__text_scraps.append(message_text)
215 def __OnCloseTranslation(self):
216 if self.__string_id is not None:
217 self.translations[self.__string_id] = ''.join(self.__text_scraps).strip()
218 self.__string_id = None
219 self.__text_scraps = []
220 self.__characters_callback = None
223 class StringRcMaker(object):
224 """Makes .h and .rc files containing strings and translations."""
225 def __init__(self, name, inputs, outdir):
226 """Constructs a maker.
229 name: The base name of the generated files (e.g.,
230 'installer_util_strings').
231 inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
232 outdir: The directory into which the files will be generated.
239 translated_strings = self.__ReadSourceAndTranslatedStrings()
240 self.__WriteRCFile(translated_strings)
241 self.__WriteHeaderFile(translated_strings)
243 class __TranslationData(object):
244 """A container of information about a single translation."""
245 def __init__(self, resource_id_str, language, translation):
246 self.resource_id_str = resource_id_str
247 self.language = language
248 self.translation = translation
250 def __cmp__(self, other):
251 """Allow __TranslationDatas to be sorted by id then by language."""
252 id_result = cmp(self.resource_id_str, other.resource_id_str)
253 return cmp(self.language, other.language) if id_result == 0 else id_result
255 def __ReadSourceAndTranslatedStrings(self):
256 """Reads the source strings and translations from all inputs."""
257 translated_strings = []
258 for grd_file, xtb_dir in self.inputs:
259 # Get the name of the grd file sans extension.
260 source_name = os.path.splitext(os.path.basename(grd_file))[0]
261 # Compute a glob for the translation files.
262 xtb_pattern = os.path.join(os.path.dirname(grd_file), xtb_dir,
263 '%s*.xtb' % source_name)
264 translated_strings.extend(
265 self.__ReadSourceAndTranslationsFrom(grd_file, glob.glob(xtb_pattern)))
266 translated_strings.sort()
267 return translated_strings
269 def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
270 """Reads source strings and translations for a .grd file.
272 Reads the source strings and all available translations for the messages
273 identified by STRING_IDS. The source string is used where translations are
277 grd_file: Path to a .grd file.
278 xtb_files: List of paths to .xtb files.
281 An unsorted list of __TranslationData instances.
283 sax_parser = sax.make_parser()
285 # Read the source (en-US) string from the .grd file.
286 grd_handler = GrdHandler(STRING_IDS)
287 sax_parser.setContentHandler(grd_handler)
288 sax_parser.parse(grd_file)
289 source_strings = grd_handler.messages
291 # Manually put the source strings as en-US in the list of translated
293 translated_strings = []
294 for string_id, message_text in source_strings.iteritems():
295 translated_strings.append(self.__TranslationData(string_id,
299 # Generate the message ID for each source string to correlate it with its
300 # translations in the .xtb files.
302 tclib.GenerateMessageId(message_text): string_id
303 for (string_id, message_text) in source_strings.iteritems()
306 # Gather the translated strings from the .xtb files. Use the en-US string
307 # for any message lacking a translation.
308 xtb_handler = XtbHandler(translation_ids)
309 sax_parser.setContentHandler(xtb_handler)
310 for xtb_filename in xtb_files:
311 sax_parser.parse(xtb_filename)
312 for string_id, message_text in source_strings.iteritems():
313 translated_string = xtb_handler.translations.get(string_id,
315 translated_strings.append(self.__TranslationData(string_id,
318 return translated_strings
320 def __WriteRCFile(self, translated_strings):
321 """Writes a resource file with the strings provided in |translated_strings|.
324 u'#include "%s.h"\n\n'
333 with io.open(os.path.join(self.outdir, self.name + '.rc'),
336 newline='\n') as outfile:
337 outfile.write(HEADER_TEXT)
338 for translation in translated_strings:
339 # Escape special characters for the rc file.
340 escaped_text = (translation.translation.replace('"', '""')
341 .replace('\t', '\\t')
342 .replace('\n', '\\n'))
343 outfile.write(u' %s "%s"\n' %
344 (translation.resource_id_str + '_' + translation.language,
346 outfile.write(FOOTER_TEXT)
348 def __WriteHeaderFile(self, translated_strings):
349 """Writes a .h file with resource ids."""
350 # TODO(grt): Stream the lines to the file rather than building this giant
351 # list of lines first.
353 do_languages_lines = ['\n#define DO_LANGUAGES']
354 installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
356 # Write the values for how the languages ids are offset.
357 seen_languages = set()
359 for translation_data in translated_strings:
360 lang = translation_data.language
361 if lang not in seen_languages:
362 seen_languages.add(lang)
363 lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
364 do_languages_lines.append(' HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
365 % (lang.replace('_', '-').lower(), lang))
370 # Write the resource ids themselves.
371 resource_id = FIRST_RESOURCE_ID
372 for translation_data in translated_strings:
373 lines.append('#define %s %s' % (translation_data.resource_id_str + '_' +
374 translation_data.language,
378 # Write out base ID values.
379 for string_id in STRING_IDS:
380 lines.append('#define %s_BASE %s_%s' % (string_id,
382 translated_strings[0].language))
383 installer_string_mapping_lines.append(' HANDLE_STRING(%s_BASE, %s)'
384 % (string_id, string_id))
386 with open(os.path.join(self.outdir, self.name + '.h'), 'wb') as outfile:
387 outfile.write('\n'.join(lines))
388 outfile.write('\n#ifndef RC_INVOKED')
389 outfile.write(' \\\n'.join(do_languages_lines))
390 outfile.write(' \\\n'.join(installer_string_mapping_lines))
391 # .rc files must end in a new line
392 outfile.write('\n#endif // ndef RC_INVOKED\n')
395 def ParseCommandLine():
396 def GrdPathAndXtbDirPair(string):
397 """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
399 parts = string.split(':')
400 if len(parts) is not 2:
401 raise argparse.ArgumentTypeError('%r is not grd_path:xtb_dir')
402 return (parts[0], parts[1])
404 parser = argparse.ArgumentParser(
405 description='Generate .h and .rc files for installer strings.')
406 parser.add_argument('-i', action='append',
407 type=GrdPathAndXtbDirPair,
409 help='path to .grd file:relative path to .xtb dir',
410 metavar='GRDFILE:XTBDIR',
412 parser.add_argument('-o',
414 help='output directory for generated .rc and .h files',
416 parser.add_argument('-n',
418 help='base name of generated .rc and .h files',
420 return parser.parse_args()
424 args = ParseCommandLine()
425 StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
429 if '__main__' == __name__: