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', # 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',
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.
87 messages: A dict mapping string identifiers to their corresponding messages.
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.
95 string_ids: A list of message identifiers to extract.
97 sax
.handler
.ContentHandler
.__init
__(self
)
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
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.
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.
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
)
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.
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.
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.
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
273 grd_file: Path to a .grd file.
274 xtb_files: List of paths to .xtb files.
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
289 translated_strings
= []
290 for string_id
, message_text
in source_strings
.iteritems():
291 translated_strings
.append(self
.__TranslationData
(string_id
,
295 # Generate the message ID for each source string to correlate it with its
296 # translations in the .xtb files.
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
,
311 translated_strings
.append(self
.__TranslationData
(string_id
,
314 return translated_strings
316 def __WriteRCFile(self
, translated_strings
):
317 """Writes a resource file with the strings provided in |translated_strings|.
320 u
'#include "%s.h"\n\n'
329 with io
.open(os
.path
.join(self
.outdir
, self
.name
+ '.rc'),
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
,
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.
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()
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
))
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
,
374 # Write out base ID values.
375 for string_id
in STRING_IDS
:
376 lines
.append('#define %s_BASE %s_%s' % (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
,
405 help='path to .grd file:relative path to .xtb dir',
406 metavar
='GRDFILE:XTBDIR',
408 parser
.add_argument('-o',
410 help='output directory for generated .rc and .h files',
412 parser
.add_argument('-n',
414 help='base name of generated .rc and .h files',
416 return parser
.parse_args()
420 args
= ParseCommandLine()
421 StringRcMaker(args
.name
, args
.inputs
, args
.outdir
).MakeFiles()
425 if '__main__' == __name__
: