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_UNINSTALL_CHROME',
54 'IDS_ABOUT_VERSION_COMPANY_NAME',
55 'IDS_INSTALL_HIGHER_VERSION',
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.
89 messages: A dict mapping string identifiers to their corresponding messages.
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.
97 string_ids: A list of message identifiers to extract.
99 sax
.handler
.ContentHandler
.__init
__(self
)
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
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.
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.
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
)
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.
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.
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.
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
275 grd_file: Path to a .grd file.
276 xtb_files: List of paths to .xtb files.
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
291 translated_strings
= []
292 for string_id
, message_text
in source_strings
.iteritems():
293 translated_strings
.append(self
.__TranslationData
(string_id
,
297 # Generate the message ID for each source string to correlate it with its
298 # translations in the .xtb files.
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
,
313 translated_strings
.append(self
.__TranslationData
(string_id
,
316 return translated_strings
318 def __WriteRCFile(self
, translated_strings
):
319 """Writes a resource file with the strings provided in |translated_strings|.
322 u
'#include "%s.h"\n\n'
331 with io
.open(os
.path
.join(self
.outdir
, self
.name
+ '.rc'),
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
,
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.
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()
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
))
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
,
376 # Write out base ID values.
377 for string_id
in STRING_IDS
:
378 lines
.append('#define %s_BASE %s_%s' % (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
,
407 help='path to .grd file:relative path to .xtb dir',
408 metavar
='GRDFILE:XTBDIR',
410 parser
.add_argument('-o',
412 help='output directory for generated .rc and .h files',
414 parser
.add_argument('-n',
416 help='base name of generated .rc and .h files',
418 return parser
.parse_args()
422 args
= ParseCommandLine()
423 StringRcMaker(args
.name
, args
.inputs
, args
.outdir
).MakeFiles()
427 if '__main__' == __name__
: