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_INSTALL_MULTI_INSTALLATION_EXISTS',
68 'IDS_INSTALL_INCONSISTENT_UPDATE_POLICY',
69 'IDS_OEM_MAIN_SHORTCUT_NAME',
70 'IDS_SHORTCUT_TOOLTIP',
71 'IDS_SHORTCUT_NEW_WINDOW',
72 'IDS_APP_LAUNCHER_PRODUCT_DESCRIPTION',
73 'IDS_APP_LAUNCHER_SHORTCUT_TOOLTIP',
74 'IDS_UNINSTALL_APP_LAUNCHER',
75 'IDS_APP_LIST_SHORTCUT_NAME',
76 'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
77 'IDS_APP_SHORTCUTS_SUBDIR_NAME',
78 'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
79 'IDS_INBOUND_MDNS_RULE_NAME',
80 'IDS_INBOUND_MDNS_RULE_NAME_CANARY',
81 'IDS_INBOUND_MDNS_RULE_DESCRIPTION',
82 'IDS_INBOUND_MDNS_RULE_DESCRIPTION_CANARY',
85 # The ID of the first resource string.
86 FIRST_RESOURCE_ID
= 1600
89 class GrdHandler(sax
.handler
.ContentHandler
):
90 """Extracts selected strings from a .grd file.
93 messages: A dict mapping string identifiers to their corresponding messages.
95 def __init__(self
, string_ids
):
96 """Constructs a handler that reads selected strings from a .grd file.
98 The dict attribute |messages| is populated with the strings that are read.
101 string_ids: A list of message identifiers to extract.
103 sax
.handler
.ContentHandler
.__init
__(self
)
105 self
.__id
_set
= set(string_ids
)
106 self
.__message
_name
= None
107 self
.__element
_stack
= []
108 self
.__text
_scraps
= []
109 self
.__characters
_callback
= None
111 def startElement(self
, name
, attrs
):
112 self
.__element
_stack
.append(name
)
113 if name
== 'message':
114 self
.__OnOpenMessage
(attrs
.getValue('name'))
116 def endElement(self
, name
):
117 popped
= self
.__element
_stack
.pop()
118 assert popped
== name
119 if name
== 'message':
120 self
.__OnCloseMessage
()
122 def characters(self
, content
):
123 if self
.__characters
_callback
:
124 self
.__characters
_callback
(self
.__element
_stack
[-1], content
)
126 def __IsExtractingMessage(self
):
127 """Returns True if a message is currently being extracted."""
128 return self
.__message
_name
is not None
130 def __OnOpenMessage(self
, message_name
):
131 """Invoked at the start of a <message> with message's name."""
132 assert not self
.__IsExtractingMessage
()
133 self
.__message
_name
= (message_name
if message_name
in self
.__id
_set
135 if self
.__message
_name
:
136 self
.__characters
_callback
= self
.__OnMessageText
138 def __OnMessageText(self
, containing_element
, message_text
):
139 """Invoked to handle a block of text for a message."""
140 if message_text
and (containing_element
== 'message' or
141 containing_element
== 'ph'):
142 self
.__text
_scraps
.append(message_text
)
144 def __OnCloseMessage(self
):
145 """Invoked at the end of a message."""
146 if self
.__IsExtractingMessage
():
147 self
.messages
[self
.__message
_name
] = ''.join(self
.__text
_scraps
).strip()
148 self
.__message
_name
= None
149 self
.__text
_scraps
= []
150 self
.__characters
_callback
= None
153 class XtbHandler(sax
.handler
.ContentHandler
):
154 """Extracts selected translations from an .xrd file.
156 Populates the |lang| and |translations| attributes with the language and
157 selected strings of an .xtb file. Instances may be re-used to read the same
158 set of translations from multiple .xtb files.
161 translations: A mapping of translation ids to strings.
162 lang: The language parsed from the .xtb file.
164 def __init__(self
, translation_ids
):
165 """Constructs an instance to parse the given strings from an .xtb file.
168 translation_ids: a mapping of translation ids to their string
169 identifiers for the translations to be extracted.
171 sax
.handler
.ContentHandler
.__init
__(self
)
173 self
.translations
= None
174 self
.__translation
_ids
= translation_ids
175 self
.__element
_stack
= []
176 self
.__string
_id
= None
177 self
.__text
_scraps
= []
178 self
.__characters
_callback
= None
180 def startDocument(self
):
181 # Clear the lang and translations since a new document is being parsed.
183 self
.translations
= {}
185 def startElement(self
, name
, attrs
):
186 self
.__element
_stack
.append(name
)
187 # translationbundle is the document element, and hosts the lang id.
188 if len(self
.__element
_stack
) == 1:
189 assert name
== 'translationbundle'
190 self
.__OnLanguage
(attrs
.getValue('lang'))
191 if name
== 'translation':
192 self
.__OnOpenTranslation
(attrs
.getValue('id'))
194 def endElement(self
, name
):
195 popped
= self
.__element
_stack
.pop()
196 assert popped
== name
197 if name
== 'translation':
198 self
.__OnCloseTranslation
()
200 def characters(self
, content
):
201 if self
.__characters
_callback
:
202 self
.__characters
_callback
(self
.__element
_stack
[-1], content
)
204 def __OnLanguage(self
, lang
):
205 self
.lang
= lang
.replace('-', '_').upper()
207 def __OnOpenTranslation(self
, translation_id
):
208 assert self
.__string
_id
is None
209 self
.__string
_id
= self
.__translation
_ids
.get(translation_id
)
210 if self
.__string
_id
is not None:
211 self
.__characters
_callback
= self
.__OnTranslationText
213 def __OnTranslationText(self
, containing_element
, message_text
):
214 if message_text
and containing_element
== 'translation':
215 self
.__text
_scraps
.append(message_text
)
217 def __OnCloseTranslation(self
):
218 if self
.__string
_id
is not None:
219 self
.translations
[self
.__string
_id
] = ''.join(self
.__text
_scraps
).strip()
220 self
.__string
_id
= None
221 self
.__text
_scraps
= []
222 self
.__characters
_callback
= None
225 class StringRcMaker(object):
226 """Makes .h and .rc files containing strings and translations."""
227 def __init__(self
, name
, inputs
, outdir
):
228 """Constructs a maker.
231 name: The base name of the generated files (e.g.,
232 'installer_util_strings').
233 inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
234 outdir: The directory into which the files will be generated.
241 translated_strings
= self
.__ReadSourceAndTranslatedStrings
()
242 self
.__WriteRCFile
(translated_strings
)
243 self
.__WriteHeaderFile
(translated_strings
)
245 class __TranslationData(object):
246 """A container of information about a single translation."""
247 def __init__(self
, resource_id_str
, language
, translation
):
248 self
.resource_id_str
= resource_id_str
249 self
.language
= language
250 self
.translation
= translation
252 def __cmp__(self
, other
):
253 """Allow __TranslationDatas to be sorted by id then by language."""
254 id_result
= cmp(self
.resource_id_str
, other
.resource_id_str
)
255 return cmp(self
.language
, other
.language
) if id_result
== 0 else id_result
257 def __ReadSourceAndTranslatedStrings(self
):
258 """Reads the source strings and translations from all inputs."""
259 translated_strings
= []
260 for grd_file
, xtb_dir
in self
.inputs
:
261 # Get the name of the grd file sans extension.
262 source_name
= os
.path
.splitext(os
.path
.basename(grd_file
))[0]
263 # Compute a glob for the translation files.
264 xtb_pattern
= os
.path
.join(os
.path
.dirname(grd_file
), xtb_dir
,
265 '%s*.xtb' % source_name
)
266 translated_strings
.extend(
267 self
.__ReadSourceAndTranslationsFrom
(grd_file
, glob
.glob(xtb_pattern
)))
268 translated_strings
.sort()
269 return translated_strings
271 def __ReadSourceAndTranslationsFrom(self
, grd_file
, xtb_files
):
272 """Reads source strings and translations for a .grd file.
274 Reads the source strings and all available translations for the messages
275 identified by STRING_IDS. The source string is used where translations are
279 grd_file: Path to a .grd file.
280 xtb_files: List of paths to .xtb files.
283 An unsorted list of __TranslationData instances.
285 sax_parser
= sax
.make_parser()
287 # Read the source (en-US) string from the .grd file.
288 grd_handler
= GrdHandler(STRING_IDS
)
289 sax_parser
.setContentHandler(grd_handler
)
290 sax_parser
.parse(grd_file
)
291 source_strings
= grd_handler
.messages
293 # Manually put the source strings as en-US in the list of translated
295 translated_strings
= []
296 for string_id
, message_text
in source_strings
.iteritems():
297 translated_strings
.append(self
.__TranslationData
(string_id
,
301 # Generate the message ID for each source string to correlate it with its
302 # translations in the .xtb files.
304 tclib
.GenerateMessageId(message_text
): string_id
305 for (string_id
, message_text
) in source_strings
.iteritems()
308 # Gather the translated strings from the .xtb files. Use the en-US string
309 # for any message lacking a translation.
310 xtb_handler
= XtbHandler(translation_ids
)
311 sax_parser
.setContentHandler(xtb_handler
)
312 for xtb_filename
in xtb_files
:
313 sax_parser
.parse(xtb_filename
)
314 for string_id
, message_text
in source_strings
.iteritems():
315 translated_string
= xtb_handler
.translations
.get(string_id
,
317 translated_strings
.append(self
.__TranslationData
(string_id
,
320 return translated_strings
322 def __WriteRCFile(self
, translated_strings
):
323 """Writes a resource file with the strings provided in |translated_strings|.
326 u
'#include "%s.h"\n\n'
335 with io
.open(os
.path
.join(self
.outdir
, self
.name
+ '.rc'),
338 newline
='\n') as outfile
:
339 outfile
.write(HEADER_TEXT
)
340 for translation
in translated_strings
:
341 # Escape special characters for the rc file.
342 escaped_text
= (translation
.translation
.replace('"', '""')
343 .replace('\t', '\\t')
344 .replace('\n', '\\n'))
345 outfile
.write(u
' %s "%s"\n' %
346 (translation
.resource_id_str
+ '_' + translation
.language
,
348 outfile
.write(FOOTER_TEXT
)
350 def __WriteHeaderFile(self
, translated_strings
):
351 """Writes a .h file with resource ids."""
352 # TODO(grt): Stream the lines to the file rather than building this giant
353 # list of lines first.
355 do_languages_lines
= ['\n#define DO_LANGUAGES']
356 installer_string_mapping_lines
= ['\n#define DO_INSTALLER_STRING_MAPPING']
358 # Write the values for how the languages ids are offset.
359 seen_languages
= set()
361 for translation_data
in translated_strings
:
362 lang
= translation_data
.language
363 if lang
not in seen_languages
:
364 seen_languages
.add(lang
)
365 lines
.append('#define IDS_L10N_OFFSET_%s %s' % (lang
, offset_id
))
366 do_languages_lines
.append(' HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
367 % (lang
.replace('_', '-').lower(), lang
))
372 # Write the resource ids themselves.
373 resource_id
= FIRST_RESOURCE_ID
374 for translation_data
in translated_strings
:
375 lines
.append('#define %s %s' % (translation_data
.resource_id_str
+ '_' +
376 translation_data
.language
,
380 # Write out base ID values.
381 for string_id
in STRING_IDS
:
382 lines
.append('#define %s_BASE %s_%s' % (string_id
,
384 translated_strings
[0].language
))
385 installer_string_mapping_lines
.append(' HANDLE_STRING(%s_BASE, %s)'
386 % (string_id
, string_id
))
388 with
open(os
.path
.join(self
.outdir
, self
.name
+ '.h'), 'wb') as outfile
:
389 outfile
.write('\n'.join(lines
))
390 outfile
.write('\n#ifndef RC_INVOKED')
391 outfile
.write(' \\\n'.join(do_languages_lines
))
392 outfile
.write(' \\\n'.join(installer_string_mapping_lines
))
393 # .rc files must end in a new line
394 outfile
.write('\n#endif // ndef RC_INVOKED\n')
397 def ParseCommandLine():
398 def GrdPathAndXtbDirPair(string
):
399 """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
401 parts
= string
.split(':')
402 if len(parts
) is not 2:
403 raise argparse
.ArgumentTypeError('%r is not grd_path:xtb_dir')
404 return (parts
[0], parts
[1])
406 parser
= argparse
.ArgumentParser(
407 description
='Generate .h and .rc files for installer strings.')
408 parser
.add_argument('-i', action
='append',
409 type=GrdPathAndXtbDirPair
,
411 help='path to .grd file:relative path to .xtb dir',
412 metavar
='GRDFILE:XTBDIR',
414 parser
.add_argument('-o',
416 help='output directory for generated .rc and .h files',
418 parser
.add_argument('-n',
420 help='base name of generated .rc and .h files',
422 return parser
.parse_args()
426 args
= ParseCommandLine()
427 StringRcMaker(args
.name
, args
.inputs
, args
.outdir
).MakeFiles()
431 if '__main__' == __name__
: