# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-"""This script generates an rc file and header (setup_strings.{rc,h}) to be
-included in setup.exe. The rc file includes translations for strings pulled
-from generated_resource.grd and the localized .xtb files.
+"""Generates .h and .rc files for installer strings. Run "python
+create_string_rc.py" for usage details.
+
+This script generates an rc file and header (NAME.{rc,h}) to be included in
+setup.exe. The rc file includes translations for strings pulled from the given
+.grd file(s) and their corresponding localized .xtb files.
The header file includes IDs for each string, but also has values to allow
getting a string based on a language offset. For example, the header file
IDS_L10N_OFFSET_* for the language we are interested in.
"""
+import argparse
import glob
+import io
import os
import sys
-from xml.dom import minidom
+from xml import sax
-# We are expected to use ../../../../third_party/python_24/python.exe
-from google import path_utils
+BASEDIR = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(os.path.join(BASEDIR, '../../../../tools/grit'))
+sys.path.append(os.path.join(BASEDIR, '../../../../tools/python'))
-# Quick hack to fix the path.
-sys.path.append(os.path.abspath('../../tools/grit'))
-sys.path.append(os.path.abspath('../tools/grit'))
from grit.extern import tclib
-# The IDs of strings we want to import from generated_resources.grd and include
-# in setup.exe's resources.
-kStringIds = [
+# The IDs of strings we want to import from the .grd files and include in
+# setup.exe's resources.
+STRING_IDS = [
'IDS_PRODUCT_NAME',
'IDS_SXS_SHORTCUT_NAME',
'IDS_PRODUCT_APP_LAUNCHER_NAME',
'IDS_PRODUCT_BINARIES_NAME',
'IDS_PRODUCT_DESCRIPTION',
- 'IDS_PRODUCT_FRAME_NAME',
'IDS_UNINSTALL_CHROME',
'IDS_ABOUT_VERSION_COMPANY_NAME',
'IDS_INSTALL_HIGHER_VERSION',
'IDS_INSTALL_HIGHER_VERSION_APP_LAUNCHER',
- 'IDS_INSTALL_HIGHER_VERSION_CF',
- 'IDS_INSTALL_HIGHER_VERSION_CB_CF',
- 'IDS_INSTALL_SYSTEM_LEVEL_EXISTS',
'IDS_INSTALL_FAILED',
'IDS_SAME_VERSION_REPAIR_FAILED',
- 'IDS_SAME_VERSION_REPAIR_FAILED_CF',
'IDS_SETUP_PATCH_FAILED',
'IDS_INSTALL_OS_NOT_SUPPORTED',
'IDS_INSTALL_OS_ERROR',
'IDS_INSTALL_NO_PRODUCTS_TO_UPDATE',
'IDS_UNINSTALL_COMPLETE',
'IDS_INSTALL_DIR_IN_USE',
- 'IDS_INSTALL_NON_MULTI_INSTALLATION_EXISTS',
'IDS_INSTALL_MULTI_INSTALLATION_EXISTS',
- 'IDS_INSTALL_READY_MODE_REQUIRES_CHROME',
'IDS_INSTALL_INCONSISTENT_UPDATE_POLICY',
'IDS_OEM_MAIN_SHORTCUT_NAME',
'IDS_SHORTCUT_TOOLTIP',
'IDS_UNINSTALL_APP_LAUNCHER',
'IDS_APP_LIST_SHORTCUT_NAME',
'IDS_APP_LIST_SHORTCUT_NAME_CANARY',
+ 'IDS_APP_SHORTCUTS_SUBDIR_NAME',
+ 'IDS_APP_SHORTCUTS_SUBDIR_NAME_CANARY',
]
# The ID of the first resource string.
-kFirstResourceID = 1600
-
-
-class TranslationStruct:
- """A helper struct that holds information about a single translation."""
- def __init__(self, resource_id_str, language, translation):
- self.resource_id_str = resource_id_str
- self.language = language
- self.translation = translation
-
- def __cmp__(self, other):
- """Allow TranslationStructs to be sorted by id."""
- id_result = cmp(self.resource_id_str, other.resource_id_str)
- return cmp(self.language, other.language) if id_result == 0 else id_result
-
-
-def CollectTranslatedStrings(branding):
- """Collects all the translations for all the strings specified by kStringIds.
- Returns a list of tuples of (string_id, language, translated string). The
- list is sorted by language codes."""
- strings_file = 'app/chromium_strings.grd'
- translation_files = 'chromium_strings*.xtb'
- if branding == 'Chrome':
- strings_file = 'app/google_chrome_strings.grd'
- translation_files = 'google_chrome_strings*.xtb'
- kGeneratedResourcesPath = os.path.join(path_utils.ScriptDir(), '..', '..',
- '..', strings_file)
- kTranslationDirectory = os.path.join(path_utils.ScriptDir(), '..', '..',
- '..', 'app', 'resources')
- kTranslationFiles = glob.glob(os.path.join(kTranslationDirectory,
- translation_files))
-
- # Get the strings out of generated_resources.grd.
- dom = minidom.parse(kGeneratedResourcesPath)
- # message_nodes is a list of message dom nodes corresponding to the string
- # ids we care about. We want to make sure that this list is in the same
- # order as kStringIds so we can associate them together.
- message_nodes = []
- all_message_nodes = dom.getElementsByTagName('message')
- for string_id in kStringIds:
- message_nodes.append([x for x in all_message_nodes if
- x.getAttribute('name') == string_id][0])
- message_texts = [node.firstChild.nodeValue.strip() for node in message_nodes]
-
- # Generate the message ID of the string to correlate it with its translations
- # in the xtb files.
- translation_ids = [tclib.GenerateMessageId(text) for text in message_texts]
-
- # Manually put _EN_US in the list of translated strings because it doesn't
- # have a .xtb file.
- translated_strings = []
- for string_id, message_text in zip(kStringIds, message_texts):
- translated_strings.append(TranslationStruct(string_id,
- 'EN_US',
- message_text))
-
- # Gather the translated strings from the .xtb files. If an .xtb file doesn't
- # have the string we want, use the en-US string.
- for xtb_filename in kTranslationFiles:
- dom = minidom.parse(xtb_filename)
- language = dom.documentElement.getAttribute('lang')
- language = language.replace('-', '_').upper()
- translation_nodes = {}
- for translation_node in dom.getElementsByTagName('translation'):
- translation_id = translation_node.getAttribute('id')
- if translation_id in translation_ids:
- translation_nodes[translation_id] = (translation_node.firstChild
- .nodeValue
- .strip())
- for i, string_id in enumerate(kStringIds):
- translated_string = translation_nodes.get(translation_ids[i],
- message_texts[i])
- translated_strings.append(TranslationStruct(string_id,
- language,
- translated_string))
-
- translated_strings.sort()
- return translated_strings
-
-
-def WriteRCFile(translated_strings, out_filename):
- """Writes a resource (rc) file with all the language strings provided in
- |translated_strings|."""
- kHeaderText = (
- u'#include "%s.h"\n\n'
- u'STRINGTABLE\n'
- u'BEGIN\n'
- ) % os.path.basename(out_filename)
- kFooterText = (
- u'END\n'
- )
- lines = [kHeaderText]
- for translation_struct in translated_strings:
- # Escape special characters for the rc file.
- translation = (translation_struct.translation.replace('"', '""')
- .replace('\t', '\\t')
- .replace('\n', '\\n'))
- lines.append(u' %s "%s"\n' % (translation_struct.resource_id_str + '_'
- + translation_struct.language,
- translation))
- lines.append(kFooterText)
- outfile = open(out_filename + '.rc', 'wb')
- outfile.write(''.join(lines).encode('utf-16'))
- outfile.close()
-
-
-def WriteHeaderFile(translated_strings, out_filename):
- """Writes a .h file with resource ids. This file can be included by the
- executable to refer to identifiers."""
- lines = []
- do_languages_lines = ['\n#define DO_LANGUAGES']
- installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
-
- # Write the values for how the languages ids are offset.
- seen_languages = set()
- offset_id = 0
- for translation_struct in translated_strings:
- lang = translation_struct.language
- if lang not in seen_languages:
- seen_languages.add(lang)
- lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
- do_languages_lines.append(' HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
- % (lang.replace('_', '-').lower(), lang))
- offset_id += 1
- else:
- break
-
- # Write the resource ids themselves.
- resource_id = kFirstResourceID
- for translation_struct in translated_strings:
- lines.append('#define %s %s' % (translation_struct.resource_id_str + '_'
- + translation_struct.language,
- resource_id))
- resource_id += 1
-
- # Write out base ID values.
- for string_id in kStringIds:
- lines.append('#define %s_BASE %s_%s' % (string_id,
- string_id,
- translated_strings[0].language))
- installer_string_mapping_lines.append(' HANDLE_STRING(%s_BASE, %s)'
- % (string_id, string_id))
-
- outfile = open(out_filename, 'wb')
- outfile.write('\n'.join(lines))
- outfile.write('\n#ifndef RC_INVOKED')
- outfile.write(' \\\n'.join(do_languages_lines))
- outfile.write(' \\\n'.join(installer_string_mapping_lines))
- # .rc files must end in a new line
- outfile.write('\n#endif // ndef RC_INVOKED\n')
- outfile.close()
-
-
-def main(argv):
- # TODO: Use optparse to parse command line flags.
- if len(argv) < 2:
- print 'Usage:\n %s <output_directory> [branding]' % argv[0]
- return 1
- branding = ''
- if (len(sys.argv) > 2):
- branding = argv[2]
- translated_strings = CollectTranslatedStrings(branding)
- kFilebase = os.path.join(argv[1], 'installer_util_strings')
- WriteRCFile(translated_strings, kFilebase)
- WriteHeaderFile(translated_strings, kFilebase + '.h')
+FIRST_RESOURCE_ID = 1600
+
+
+class GrdHandler(sax.handler.ContentHandler):
+ """Extracts selected strings from a .grd file.
+
+ Attributes:
+ messages: A dict mapping string identifiers to their corresponding messages.
+ """
+ def __init__(self, string_ids):
+ """Constructs a handler that reads selected strings from a .grd file.
+
+ The dict attribute |messages| is populated with the strings that are read.
+
+ Args:
+ string_ids: A list of message identifiers to extract.
+ """
+ sax.handler.ContentHandler.__init__(self)
+ self.messages = {}
+ self.__id_set = set(string_ids)
+ self.__message_name = None
+ self.__element_stack = []
+ self.__text_scraps = []
+ self.__characters_callback = None
+
+ def startElement(self, name, attrs):
+ self.__element_stack.append(name)
+ if name == 'message':
+ self.__OnOpenMessage(attrs.getValue('name'))
+
+ def endElement(self, name):
+ popped = self.__element_stack.pop()
+ assert popped == name
+ if name == 'message':
+ self.__OnCloseMessage()
+
+ def characters(self, content):
+ if self.__characters_callback:
+ self.__characters_callback(self.__element_stack[-1], content)
+
+ def __IsExtractingMessage(self):
+ """Returns True if a message is currently being extracted."""
+ return self.__message_name is not None
+
+ def __OnOpenMessage(self, message_name):
+ """Invoked at the start of a <message> with message's name."""
+ assert not self.__IsExtractingMessage()
+ self.__message_name = (message_name if message_name in self.__id_set
+ else None)
+ if self.__message_name:
+ self.__characters_callback = self.__OnMessageText
+
+ def __OnMessageText(self, containing_element, message_text):
+ """Invoked to handle a block of text for a message."""
+ if message_text and (containing_element == 'message' or
+ containing_element == 'ph'):
+ self.__text_scraps.append(message_text)
+
+ def __OnCloseMessage(self):
+ """Invoked at the end of a message."""
+ if self.__IsExtractingMessage():
+ self.messages[self.__message_name] = ''.join(self.__text_scraps).strip()
+ self.__message_name = None
+ self.__text_scraps = []
+ self.__characters_callback = None
+
+
+class XtbHandler(sax.handler.ContentHandler):
+ """Extracts selected translations from an .xrd file.
+
+ Populates the |lang| and |translations| attributes with the language and
+ selected strings of an .xtb file. Instances may be re-used to read the same
+ set of translations from multiple .xtb files.
+
+ Attributes:
+ translations: A mapping of translation ids to strings.
+ lang: The language parsed from the .xtb file.
+ """
+ def __init__(self, translation_ids):
+ """Constructs an instance to parse the given strings from an .xtb file.
+
+ Args:
+ translation_ids: a mapping of translation ids to their string
+ identifiers for the translations to be extracted.
+ """
+ sax.handler.ContentHandler.__init__(self)
+ self.lang = None
+ self.translations = None
+ self.__translation_ids = translation_ids
+ self.__element_stack = []
+ self.__string_id = None
+ self.__text_scraps = []
+ self.__characters_callback = None
+
+ def startDocument(self):
+ # Clear the lang and translations since a new document is being parsed.
+ self.lang = ''
+ self.translations = {}
+
+ def startElement(self, name, attrs):
+ self.__element_stack.append(name)
+ # translationbundle is the document element, and hosts the lang id.
+ if len(self.__element_stack) == 1:
+ assert name == 'translationbundle'
+ self.__OnLanguage(attrs.getValue('lang'))
+ if name == 'translation':
+ self.__OnOpenTranslation(attrs.getValue('id'))
+
+ def endElement(self, name):
+ popped = self.__element_stack.pop()
+ assert popped == name
+ if name == 'translation':
+ self.__OnCloseTranslation()
+
+ def characters(self, content):
+ if self.__characters_callback:
+ self.__characters_callback(self.__element_stack[-1], content)
+
+ def __OnLanguage(self, lang):
+ self.lang = lang.replace('-', '_').upper()
+
+ def __OnOpenTranslation(self, translation_id):
+ assert self.__string_id is None
+ self.__string_id = self.__translation_ids.get(translation_id)
+ if self.__string_id is not None:
+ self.__characters_callback = self.__OnTranslationText
+
+ def __OnTranslationText(self, containing_element, message_text):
+ if message_text and containing_element == 'translation':
+ self.__text_scraps.append(message_text)
+
+ def __OnCloseTranslation(self):
+ if self.__string_id is not None:
+ self.translations[self.__string_id] = ''.join(self.__text_scraps).strip()
+ self.__string_id = None
+ self.__text_scraps = []
+ self.__characters_callback = None
+
+
+class StringRcMaker(object):
+ """Makes .h and .rc files containing strings and translations."""
+ def __init__(self, name, inputs, outdir):
+ """Constructs a maker.
+
+ Args:
+ name: The base name of the generated files (e.g.,
+ 'installer_util_strings').
+ inputs: A list of (grd_file, xtb_dir) pairs containing the source data.
+ outdir: The directory into which the files will be generated.
+ """
+ self.name = name
+ self.inputs = inputs
+ self.outdir = outdir
+
+ def MakeFiles(self):
+ translated_strings = self.__ReadSourceAndTranslatedStrings()
+ self.__WriteRCFile(translated_strings)
+ self.__WriteHeaderFile(translated_strings)
+
+ class __TranslationData(object):
+ """A container of information about a single translation."""
+ def __init__(self, resource_id_str, language, translation):
+ self.resource_id_str = resource_id_str
+ self.language = language
+ self.translation = translation
+
+ def __cmp__(self, other):
+ """Allow __TranslationDatas to be sorted by id then by language."""
+ id_result = cmp(self.resource_id_str, other.resource_id_str)
+ return cmp(self.language, other.language) if id_result == 0 else id_result
+
+ def __ReadSourceAndTranslatedStrings(self):
+ """Reads the source strings and translations from all inputs."""
+ translated_strings = []
+ for grd_file, xtb_dir in self.inputs:
+ # Get the name of the grd file sans extension.
+ source_name = os.path.splitext(os.path.basename(grd_file))[0]
+ # Compute a glob for the translation files.
+ xtb_pattern = os.path.join(os.path.dirname(grd_file), xtb_dir,
+ '%s*.xtb' % source_name)
+ translated_strings.extend(
+ self.__ReadSourceAndTranslationsFrom(grd_file, glob.glob(xtb_pattern)))
+ translated_strings.sort()
+ return translated_strings
+
+ def __ReadSourceAndTranslationsFrom(self, grd_file, xtb_files):
+ """Reads source strings and translations for a .grd file.
+
+ Reads the source strings and all available translations for the messages
+ identified by STRING_IDS. The source string is used where translations are
+ missing.
+
+ Args:
+ grd_file: Path to a .grd file.
+ xtb_files: List of paths to .xtb files.
+
+ Returns:
+ An unsorted list of __TranslationData instances.
+ """
+ sax_parser = sax.make_parser()
+
+ # Read the source (en-US) string from the .grd file.
+ grd_handler = GrdHandler(STRING_IDS)
+ sax_parser.setContentHandler(grd_handler)
+ sax_parser.parse(grd_file)
+ source_strings = grd_handler.messages
+
+ # Manually put the source strings as en-US in the list of translated
+ # strings.
+ translated_strings = []
+ for string_id, message_text in source_strings.iteritems():
+ translated_strings.append(self.__TranslationData(string_id,
+ 'EN_US',
+ message_text))
+
+ # Generate the message ID for each source string to correlate it with its
+ # translations in the .xtb files.
+ translation_ids = {
+ tclib.GenerateMessageId(message_text): string_id
+ for (string_id, message_text) in source_strings.iteritems()
+ }
+
+ # Gather the translated strings from the .xtb files. Use the en-US string
+ # for any message lacking a translation.
+ xtb_handler = XtbHandler(translation_ids)
+ sax_parser.setContentHandler(xtb_handler)
+ for xtb_filename in xtb_files:
+ sax_parser.parse(xtb_filename)
+ for string_id, message_text in source_strings.iteritems():
+ translated_string = xtb_handler.translations.get(string_id,
+ message_text)
+ translated_strings.append(self.__TranslationData(string_id,
+ xtb_handler.lang,
+ translated_string))
+ return translated_strings
+
+ def __WriteRCFile(self, translated_strings):
+ """Writes a resource file with the strings provided in |translated_strings|.
+ """
+ HEADER_TEXT = (
+ u'#include "%s.h"\n\n'
+ u'STRINGTABLE\n'
+ u'BEGIN\n'
+ ) % self.name
+
+ FOOTER_TEXT = (
+ u'END\n'
+ )
+
+ with io.open(os.path.join(self.outdir, self.name + '.rc'),
+ mode='w',
+ encoding='utf-16',
+ newline='\n') as outfile:
+ outfile.write(HEADER_TEXT)
+ for translation in translated_strings:
+ # Escape special characters for the rc file.
+ escaped_text = (translation.translation.replace('"', '""')
+ .replace('\t', '\\t')
+ .replace('\n', '\\n'))
+ outfile.write(u' %s "%s"\n' %
+ (translation.resource_id_str + '_' + translation.language,
+ escaped_text))
+ outfile.write(FOOTER_TEXT)
+
+ def __WriteHeaderFile(self, translated_strings):
+ """Writes a .h file with resource ids."""
+ # TODO(grt): Stream the lines to the file rather than building this giant
+ # list of lines first.
+ lines = []
+ do_languages_lines = ['\n#define DO_LANGUAGES']
+ installer_string_mapping_lines = ['\n#define DO_INSTALLER_STRING_MAPPING']
+
+ # Write the values for how the languages ids are offset.
+ seen_languages = set()
+ offset_id = 0
+ for translation_data in translated_strings:
+ lang = translation_data.language
+ if lang not in seen_languages:
+ seen_languages.add(lang)
+ lines.append('#define IDS_L10N_OFFSET_%s %s' % (lang, offset_id))
+ do_languages_lines.append(' HANDLE_LANGUAGE(%s, IDS_L10N_OFFSET_%s)'
+ % (lang.replace('_', '-').lower(), lang))
+ offset_id += 1
+ else:
+ break
+
+ # Write the resource ids themselves.
+ resource_id = FIRST_RESOURCE_ID
+ for translation_data in translated_strings:
+ lines.append('#define %s %s' % (translation_data.resource_id_str + '_' +
+ translation_data.language,
+ resource_id))
+ resource_id += 1
+
+ # Write out base ID values.
+ for string_id in STRING_IDS:
+ lines.append('#define %s_BASE %s_%s' % (string_id,
+ string_id,
+ translated_strings[0].language))
+ installer_string_mapping_lines.append(' HANDLE_STRING(%s_BASE, %s)'
+ % (string_id, string_id))
+
+ with open(os.path.join(self.outdir, self.name + '.h'), 'wb') as outfile:
+ outfile.write('\n'.join(lines))
+ outfile.write('\n#ifndef RC_INVOKED')
+ outfile.write(' \\\n'.join(do_languages_lines))
+ outfile.write(' \\\n'.join(installer_string_mapping_lines))
+ # .rc files must end in a new line
+ outfile.write('\n#endif // ndef RC_INVOKED\n')
+
+
+def ParseCommandLine():
+ def GrdPathAndXtbDirPair(string):
+ """Returns (grd_path, xtb_dir) given a colon-separated string of the same.
+ """
+ parts = string.split(':')
+ if len(parts) is not 2:
+ raise argparse.ArgumentTypeError('%r is not grd_path:xtb_dir')
+ return (parts[0], parts[1])
+
+ parser = argparse.ArgumentParser(
+ description='Generate .h and .rc files for installer strings.')
+ parser.add_argument('-i', action='append',
+ type=GrdPathAndXtbDirPair,
+ required=True,
+ help='path to .grd file:relative path to .xtb dir',
+ metavar='GRDFILE:XTBDIR',
+ dest='inputs')
+ parser.add_argument('-o',
+ required=True,
+ help='output directory for generated .rc and .h files',
+ dest='outdir')
+ parser.add_argument('-n',
+ required=True,
+ help='base name of generated .rc and .h files',
+ dest='name')
+ return parser.parse_args()
+
+
+def main():
+ args = ParseCommandLine()
+ StringRcMaker(args.name, args.inputs, args.outdir).MakeFiles()
return 0
if '__main__' == __name__:
- sys.exit(main(sys.argv))
+ sys.exit(main())