Re-land: C++ readability review
[chromium-blink-merge.git] / remoting / tools / build / remoting_ios_localize.py
blob04beb18d0e7b1869a2b9ac0ca33cf4b88cd5793f
1 #!/usr/bin/env python
2 # Copyright 2014 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 """Tool to produce localized strings for the remoting iOS client.
8 This script uses a subset of grit-generated string data-packs to produce
9 localized string files appropriate for iOS.
11 For each locale, it generates the following:
13 <locale>.lproj/
14 Localizable.strings
15 InfoPlist.strings
17 The strings in Localizable.strings are specified in a file containing a list of
18 IDS. E.g.:
20 Given: Localizable_ids.txt:
21 IDS_PRODUCT_NAME
22 IDS_SIGN_IN_BUTTON
23 IDS_CANCEL
25 Produces: Localizable.strings:
26 "IDS_PRODUCT_NAME" = "Remote Desktop";
27 "IDS_SIGN_IN_BUTTON" = "Sign In";
28 "IDS_CANCEL" = "Cancel";
30 The InfoPlist.strings is formatted using a Jinja2 template where the "ids"
31 variable is a dictionary of id -> string. E.g.:
33 Given: InfoPlist.strings.jinja2:
34 "CFBundleName" = "{{ ids.IDS_PRODUCT_NAME }}"
35 "CFCopyrightNotice" = "{{ ids.IDS_COPYRIGHT }}"
37 Produces: InfoPlist.strings:
38 "CFBundleName" = "Remote Desktop";
39 "CFCopyrightNotice" = "Copyright 2014 The Chromium Authors.";
41 Parameters:
42 --print-inputs
43 Prints the expected input file list, then exit. This can be used in gyp
44 input rules.
46 --print-outputs
47 Prints the expected output file list, then exit. This can be used in gyp
48 output rules.
50 --from-dir FROM_DIR
51 Specify the directory containing the data pack files generated by grit.
52 Each data pack should be named <locale>.pak.
54 --to-dir TO_DIR
55 Specify the directory to write the <locale>.lproj directories containing
56 the string files.
58 --localizable-list LOCALIZABLE_ID_LIST
59 Specify the file containing the list of the IDs of the strings that each
60 Localizable.strings file should contain.
62 --infoplist-template INFOPLIST_TEMPLATE
63 Specify the Jinja2 template to be used to create each InfoPlist.strings
64 file.
66 --resources-header RESOURCES_HEADER
67 Specifies the grit-generated header file that maps ID names to ID values.
68 It's required to map the IDs in LOCALIZABLE_ID_LIST and INFOPLIST_TEMPLATE
69 to strings in the data packs.
70 """
73 import codecs
74 import optparse
75 import os
76 import re
77 import sys
79 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..',
80 'tools', 'grit'))
81 from grit.format import data_pack
83 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..',
84 'third_party'))
85 import jinja2
88 LOCALIZABLE_STRINGS = 'Localizable.strings'
89 INFOPLIST_STRINGS = 'InfoPlist.strings'
92 class LocalizeException(Exception):
93 pass
96 class LocalizedStringJinja2Adapter:
97 """Class that maps ID names to localized strings in Jinja2."""
98 def __init__(self, id_map, pack):
99 self.id_map = id_map
100 self.pack = pack
102 def __getattr__(self, name):
103 id_value = self.id_map.get(name)
104 if not id_value:
105 raise LocalizeException('Could not find id %s in resource header' % name)
106 data = self.pack.resources.get(id_value)
107 if not data:
108 raise LocalizeException(
109 'Could not find string with id %s (%d) in data pack' %
110 (name, id_value))
111 return decode_and_escape(data)
114 def get_inputs(from_dir, locales):
115 """Returns the list of files that would be required to run the tool."""
116 inputs = []
117 for locale in locales:
118 inputs.append(os.path.join(from_dir, '%s.pak' % locale))
119 return format_quoted_list(inputs)
122 def get_outputs(to_dir, locales):
123 """Returns the list of files that would be produced by the tool."""
124 outputs = []
125 for locale in locales:
126 lproj_dir = format_lproj_dir(to_dir, locale)
127 outputs.append(os.path.join(lproj_dir, LOCALIZABLE_STRINGS))
128 outputs.append(os.path.join(lproj_dir, INFOPLIST_STRINGS))
129 return format_quoted_list(outputs)
132 def format_quoted_list(items):
133 """Formats a list as a string, with items space-separated and quoted."""
134 return " ".join(['"%s"' % x for x in items])
137 def format_lproj_dir(to_dir, locale):
138 """Formats the name of the lproj directory for a given locale."""
139 locale = locale.replace('-', '_')
140 return os.path.join(to_dir, '%s.lproj' % locale)
143 def read_resources_header(resources_header_path):
144 """Reads and parses a grit-generated resource header file.
146 This function will parse lines like the following:
148 #define IDS_PRODUCT_NAME 28531
149 #define IDS_CANCEL 28542
151 And return a dictionary like the following:
153 { 'IDS_PRODUCT_NAME': 28531, 'IDS_CANCEL': 28542 }
155 regex = re.compile(r'^#define\s+(\w+)\s+(\d+)$')
156 id_map = {}
157 try:
158 with open(resources_header_path, 'r') as f:
159 for line in f:
160 match = regex.match(line)
161 if match:
162 id_str = match.group(1)
163 id_value = int(match.group(2))
164 id_map[id_str] = id_value
165 except:
166 sys.stderr.write('Error while reading header file %s\n'
167 % resources_header_path)
168 raise
170 return id_map
173 def read_id_list(id_list_path):
174 """Read a text file with ID names.
176 Names are stripped of leading and trailing spaces. Empty lines are ignored.
178 with open(id_list_path, 'r') as f:
179 stripped_lines = [x.strip() for x in f]
180 non_empty_lines = [x for x in stripped_lines if x]
181 return non_empty_lines
184 def read_jinja2_template(template_path):
185 """Reads a Jinja2 template."""
186 (template_dir, template_name) = os.path.split(template_path)
187 env = jinja2.Environment(loader = jinja2.FileSystemLoader(template_dir))
188 template = env.get_template(template_name)
189 return template
192 def decode_and_escape(data):
193 """Decodes utf-8 data, and escapes it appropriately to use in *.strings."""
194 u_string = codecs.decode(data, 'utf-8')
195 u_string = u_string.replace('\\', '\\\\')
196 u_string = u_string.replace('"', '\\"')
197 return u_string
200 def generate(from_dir, to_dir, localizable_list_path, infoplist_template_path,
201 resources_header_path, locales):
202 """Generates the <locale>.lproj directories and files."""
204 id_map = read_resources_header(resources_header_path)
205 localizable_ids = read_id_list(localizable_list_path)
206 infoplist_template = read_jinja2_template(infoplist_template_path)
208 # Generate string files for each locale
209 for locale in locales:
210 pack = data_pack.ReadDataPack(
211 os.path.join(os.path.join(from_dir, '%s.pak' % locale)))
213 lproj_dir = format_lproj_dir(to_dir, locale)
214 if not os.path.exists(lproj_dir):
215 os.makedirs(lproj_dir)
217 # Generate Localizable.strings
218 localizable_strings_path = os.path.join(lproj_dir, LOCALIZABLE_STRINGS)
219 try:
220 with codecs.open(localizable_strings_path, 'w', 'utf-16') as f:
221 for id_str in localizable_ids:
222 id_value = id_map.get(id_str)
223 if not id_value:
224 raise LocalizeException('Could not find "%s" in %s' %
225 (id_str, resources_header_path))
227 localized_data = pack.resources.get(id_value)
228 if not localized_data:
229 raise LocalizeException(
230 'Could not find localized string in %s for %s (%d)' %
231 (localizable_strings_path, id_str, id_value))
233 f.write(u'"%s" = "%s";\n' %
234 (id_str, decode_and_escape(localized_data)))
235 except:
236 sys.stderr.write('Error while creating %s\n' % localizable_strings_path)
237 raise
239 # Generate InfoPlist.strings
240 infoplist_strings_path = os.path.join(lproj_dir, INFOPLIST_STRINGS)
241 try:
242 with codecs.open(infoplist_strings_path, 'w', 'utf-16') as f:
243 infoplist = infoplist_template.render(
244 ids = LocalizedStringJinja2Adapter(id_map, pack))
245 f.write(infoplist)
246 except:
247 sys.stderr.write('Error while creating %s\n' % infoplist_strings_path)
248 raise
251 def DoMain(args):
252 """Entrypoint used by gyp's pymod_do_main."""
253 parser = optparse.OptionParser("usage: %prog [options] locales")
254 parser.add_option("--print-inputs", action="store_true", dest="print_input",
255 default=False,
256 help="Print the expected input file list, then exit.")
257 parser.add_option("--print-outputs", action="store_true", dest="print_output",
258 default=False,
259 help="Print the expected output file list, then exit.")
260 parser.add_option("--from-dir", action="store", dest="from_dir",
261 help="Source data pack directory.")
262 parser.add_option("--to-dir", action="store", dest="to_dir",
263 help="Destination data pack directory.")
264 parser.add_option("--localizable-list", action="store",
265 dest="localizable_list",
266 help="File with list of IDS to build Localizable.strings")
267 parser.add_option("--infoplist-template", action="store",
268 dest="infoplist_template",
269 help="File with list of IDS to build InfoPlist.strings")
270 parser.add_option("--resources-header", action="store",
271 dest="resources_header",
272 help="Auto-generated header with resource ids.")
273 options, locales = parser.parse_args(args)
275 if not locales:
276 parser.error('At least one locale is required.')
278 if options.print_input and options.print_output:
279 parser.error('Only one of --print-inputs or --print-outputs is allowed')
281 if options.print_input:
282 if not options.from_dir:
283 parser.error('--from-dir is required.')
284 return get_inputs(options.from_dir, locales)
286 if options.print_output:
287 if not options.to_dir:
288 parser.error('--to-dir is required.')
289 return get_outputs(options.to_dir, locales)
291 if not (options.from_dir and options.to_dir and options.localizable_list and
292 options.infoplist_template and options.resources_header):
293 parser.error('--from-dir, --to-dir, --localizable-list, ' +
294 '--infoplist-template and --resources-header are required.')
296 try:
297 generate(options.from_dir, options.to_dir, options.localizable_list,
298 options.infoplist_template, options.resources_header, locales)
299 except LocalizeException as e:
300 sys.stderr.write('Error: %s\n' % str(e))
301 sys.exit(1)
303 return ""
306 def main(args):
307 print DoMain(args[1:])
310 if __name__ == '__main__':
311 main(sys.argv)