| #!/usr/bin/env python |
| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Tool to produce localized strings for the remoting iOS client. |
| |
| This script uses a subset of grit-generated string data-packs to produce |
| localized string files appropriate for iOS. |
| |
| For each locale, it generates the following: |
| |
| <locale>.lproj/ |
| Localizable.strings |
| InfoPlist.strings |
| |
| The strings in Localizable.strings are specified in a file containing a list of |
| IDS. E.g.: |
| |
| Given: Localizable_ids.txt: |
| IDS_PRODUCT_NAME |
| IDS_SIGN_IN_BUTTON |
| IDS_CANCEL |
| |
| Produces: Localizable.strings: |
| "IDS_PRODUCT_NAME" = "Remote Desktop"; |
| "IDS_SIGN_IN_BUTTON" = "Sign In"; |
| "IDS_CANCEL" = "Cancel"; |
| |
| The InfoPlist.strings is formatted using a Jinja2 template where the "ids" |
| variable is a dictionary of id -> string. E.g.: |
| |
| Given: InfoPlist.strings.jinja2: |
| "CFBundleName" = "{{ ids.IDS_PRODUCT_NAME }}" |
| "CFCopyrightNotice" = "{{ ids.IDS_COPYRIGHT }}" |
| |
| Produces: InfoPlist.strings: |
| "CFBundleName" = "Remote Desktop"; |
| "CFCopyrightNotice" = "Copyright 2014 The Chromium Authors."; |
| |
| Parameters: |
| --print-inputs |
| Prints the expected input file list, then exit. This can be used in gyp |
| input rules. |
| |
| --print-outputs |
| Prints the expected output file list, then exit. This can be used in gyp |
| output rules. |
| |
| --from-dir FROM_DIR |
| Specify the directory containing the data pack files generated by grit. |
| Each data pack should be named <locale>.pak. |
| |
| --to-dir TO_DIR |
| Specify the directory to write the <locale>.lproj directories containing |
| the string files. |
| |
| --localizable-list LOCALIZABLE_ID_LIST |
| Specify the file containing the list of the IDs of the strings that each |
| Localizable.strings file should contain. |
| |
| --infoplist-template INFOPLIST_TEMPLATE |
| Specify the Jinja2 template to be used to create each InfoPlist.strings |
| file. |
| |
| --resources-header RESOURCES_HEADER |
| Specifies the grit-generated header file that maps ID names to ID values. |
| It's required to map the IDs in LOCALIZABLE_ID_LIST and INFOPLIST_TEMPLATE |
| to strings in the data packs. |
| """ |
| |
| |
| import codecs |
| import optparse |
| import os |
| import re |
| import sys |
| |
| sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', |
| 'tools', 'grit')) |
| from grit.format import data_pack |
| |
| sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', |
| 'third_party')) |
| import jinja2 |
| |
| |
| LOCALIZABLE_STRINGS = 'Localizable.strings' |
| INFOPLIST_STRINGS = 'InfoPlist.strings' |
| |
| |
| class LocalizeException(Exception): |
| pass |
| |
| |
| class LocalizedStringJinja2Adapter: |
| """Class that maps ID names to localized strings in Jinja2.""" |
| def __init__(self, id_map, pack): |
| self.id_map = id_map |
| self.pack = pack |
| |
| def __getattr__(self, name): |
| id_value = self.id_map.get(name) |
| if not id_value: |
| raise LocalizeException('Could not find id %s in resource header' % name) |
| data = self.pack.resources.get(id_value) |
| if not data: |
| raise LocalizeException( |
| 'Could not find string with id %s (%d) in data pack' % |
| (name, id_value)) |
| return decode_and_escape(data) |
| |
| |
| def get_inputs(from_dir, locales): |
| """Returns the list of files that would be required to run the tool.""" |
| inputs = [] |
| for locale in locales: |
| inputs.append(os.path.join(from_dir, '%s.pak' % locale)) |
| return format_quoted_list(inputs) |
| |
| |
| def get_outputs(to_dir, locales): |
| """Returns the list of files that would be produced by the tool.""" |
| outputs = [] |
| for locale in locales: |
| lproj_dir = format_lproj_dir(to_dir, locale) |
| outputs.append(os.path.join(lproj_dir, LOCALIZABLE_STRINGS)) |
| outputs.append(os.path.join(lproj_dir, INFOPLIST_STRINGS)) |
| return format_quoted_list(outputs) |
| |
| |
| def format_quoted_list(items): |
| """Formats a list as a string, with items space-separated and quoted.""" |
| return " ".join(['"%s"' % x for x in items]) |
| |
| |
| def format_lproj_dir(to_dir, locale): |
| """Formats the name of the lproj directory for a given locale.""" |
| locale = locale.replace('-', '_') |
| return os.path.join(to_dir, '%s.lproj' % locale) |
| |
| |
| def read_resources_header(resources_header_path): |
| """Reads and parses a grit-generated resource header file. |
| |
| This function will parse lines like the following: |
| |
| #define IDS_PRODUCT_NAME 28531 |
| #define IDS_CANCEL 28542 |
| |
| And return a dictionary like the following: |
| |
| { 'IDS_PRODUCT_NAME': 28531, 'IDS_CANCEL': 28542 } |
| """ |
| regex = re.compile(r'^#define\s+(\w+)\s+(\d+)$') |
| id_map = {} |
| try: |
| with open(resources_header_path, 'r') as f: |
| for line in f: |
| match = regex.match(line) |
| if match: |
| id_str = match.group(1) |
| id_value = int(match.group(2)) |
| id_map[id_str] = id_value |
| except: |
| sys.stderr.write('Error while reading header file %s\n' |
| % resources_header_path) |
| raise |
| |
| return id_map |
| |
| |
| def read_id_list(id_list_path): |
| """Read a text file with ID names. |
| |
| Names are stripped of leading and trailing spaces. Empty lines are ignored. |
| """ |
| with open(id_list_path, 'r') as f: |
| stripped_lines = [x.strip() for x in f] |
| non_empty_lines = [x for x in stripped_lines if x] |
| return non_empty_lines |
| |
| |
| def read_jinja2_template(template_path): |
| """Reads a Jinja2 template.""" |
| (template_dir, template_name) = os.path.split(template_path) |
| env = jinja2.Environment(loader = jinja2.FileSystemLoader(template_dir)) |
| template = env.get_template(template_name) |
| return template |
| |
| |
| def decode_and_escape(data): |
| """Decodes utf-8 data, and escapes it appropriately to use in *.strings.""" |
| u_string = codecs.decode(data, 'utf-8') |
| u_string = u_string.replace('\\', '\\\\') |
| u_string = u_string.replace('"', '\\"') |
| return u_string |
| |
| |
| def generate(from_dir, to_dir, localizable_list_path, infoplist_template_path, |
| resources_header_path, locales): |
| """Generates the <locale>.lproj directories and files.""" |
| |
| id_map = read_resources_header(resources_header_path) |
| localizable_ids = read_id_list(localizable_list_path) |
| infoplist_template = read_jinja2_template(infoplist_template_path) |
| |
| # Generate string files for each locale |
| for locale in locales: |
| pack = data_pack.ReadDataPack( |
| os.path.join(os.path.join(from_dir, '%s.pak' % locale))) |
| |
| lproj_dir = format_lproj_dir(to_dir, locale) |
| if not os.path.exists(lproj_dir): |
| os.makedirs(lproj_dir) |
| |
| # Generate Localizable.strings |
| localizable_strings_path = os.path.join(lproj_dir, LOCALIZABLE_STRINGS) |
| try: |
| with codecs.open(localizable_strings_path, 'w', 'utf-16') as f: |
| for id_str in localizable_ids: |
| id_value = id_map.get(id_str) |
| if not id_value: |
| raise LocalizeException('Could not find "%s" in %s' % |
| (id_str, resources_header_path)) |
| |
| localized_data = pack.resources.get(id_value) |
| if not localized_data: |
| raise LocalizeException( |
| 'Could not find localized string in %s for %s (%d)' % |
| (localizable_strings_path, id_str, id_value)) |
| |
| f.write(u'"%s" = "%s";\n' % |
| (id_str, decode_and_escape(localized_data))) |
| except: |
| sys.stderr.write('Error while creating %s\n' % localizable_strings_path) |
| raise |
| |
| # Generate InfoPlist.strings |
| infoplist_strings_path = os.path.join(lproj_dir, INFOPLIST_STRINGS) |
| try: |
| with codecs.open(infoplist_strings_path, 'w', 'utf-16') as f: |
| infoplist = infoplist_template.render( |
| ids = LocalizedStringJinja2Adapter(id_map, pack)) |
| f.write(infoplist) |
| except: |
| sys.stderr.write('Error while creating %s\n' % infoplist_strings_path) |
| raise |
| |
| |
| def DoMain(args): |
| """Entrypoint used by gyp's pymod_do_main.""" |
| parser = optparse.OptionParser("usage: %prog [options] locales") |
| parser.add_option("--print-inputs", action="store_true", dest="print_input", |
| default=False, |
| help="Print the expected input file list, then exit.") |
| parser.add_option("--print-outputs", action="store_true", dest="print_output", |
| default=False, |
| help="Print the expected output file list, then exit.") |
| parser.add_option("--from-dir", action="store", dest="from_dir", |
| help="Source data pack directory.") |
| parser.add_option("--to-dir", action="store", dest="to_dir", |
| help="Destination data pack directory.") |
| parser.add_option("--localizable-list", action="store", |
| dest="localizable_list", |
| help="File with list of IDS to build Localizable.strings") |
| parser.add_option("--infoplist-template", action="store", |
| dest="infoplist_template", |
| help="File with list of IDS to build InfoPlist.strings") |
| parser.add_option("--resources-header", action="store", |
| dest="resources_header", |
| help="Auto-generated header with resource ids.") |
| options, locales = parser.parse_args(args) |
| |
| if not locales: |
| parser.error('At least one locale is required.') |
| |
| if options.print_input and options.print_output: |
| parser.error('Only one of --print-inputs or --print-outputs is allowed') |
| |
| if options.print_input: |
| if not options.from_dir: |
| parser.error('--from-dir is required.') |
| return get_inputs(options.from_dir, locales) |
| |
| if options.print_output: |
| if not options.to_dir: |
| parser.error('--to-dir is required.') |
| return get_outputs(options.to_dir, locales) |
| |
| if not (options.from_dir and options.to_dir and options.localizable_list and |
| options.infoplist_template and options.resources_header): |
| parser.error('--from-dir, --to-dir, --localizable-list, ' + |
| '--infoplist-template and --resources-header are required.') |
| |
| try: |
| generate(options.from_dir, options.to_dir, options.localizable_list, |
| options.infoplist_template, options.resources_header, locales) |
| except LocalizeException as e: |
| sys.stderr.write('Error: %s\n' % str(e)) |
| sys.exit(1) |
| |
| return "" |
| |
| |
| def main(args): |
| print DoMain(args[1:]) |
| |
| |
| if __name__ == '__main__': |
| main(sys.argv) |