remoting_ios_localize.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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.
  5. """Tool to produce localized strings for the remoting iOS client.
  6. This script uses a subset of grit-generated string data-packs to produce
  7. localized string files appropriate for iOS.
  8. For each locale, it generates the following:
  9. <locale>.lproj/
  10. Localizable.strings
  11. InfoPlist.strings
  12. The strings in Localizable.strings are specified in a file containing a list of
  13. IDS. E.g.:
  14. Given: Localizable_ids.txt:
  15. IDS_PRODUCT_NAME
  16. IDS_SIGN_IN_BUTTON
  17. IDS_CANCEL
  18. Produces: Localizable.strings:
  19. "IDS_PRODUCT_NAME" = "Remote Desktop";
  20. "IDS_SIGN_IN_BUTTON" = "Sign In";
  21. "IDS_CANCEL" = "Cancel";
  22. The InfoPlist.strings is formatted using a Jinja2 template where the "ids"
  23. variable is a dictionary of id -> string. E.g.:
  24. Given: InfoPlist.strings.jinja2:
  25. "CFBundleName" = "{{ ids.IDS_PRODUCT_NAME }}"
  26. "CFCopyrightNotice" = "{{ ids.IDS_COPYRIGHT }}"
  27. Produces: InfoPlist.strings:
  28. "CFBundleName" = "Remote Desktop";
  29. "CFCopyrightNotice" = "Copyright 2014 The Chromium Authors.";
  30. Parameters:
  31. --print-inputs
  32. Prints the expected input file list, then exit. This can be used in gyp
  33. input rules.
  34. --print-outputs
  35. Prints the expected output file list, then exit. This can be used in gyp
  36. output rules.
  37. --from-dir FROM_DIR
  38. Specify the directory containing the data pack files generated by grit.
  39. Each data pack should be named <locale>.pak.
  40. --to-dir TO_DIR
  41. Specify the directory to write the <locale>.lproj directories containing
  42. the string files.
  43. --localizable-list LOCALIZABLE_ID_LIST
  44. Specify the file containing the list of the IDs of the strings that each
  45. Localizable.strings file should contain.
  46. --infoplist-template INFOPLIST_TEMPLATE
  47. Specify the Jinja2 template to be used to create each InfoPlist.strings
  48. file.
  49. --resources-header RESOURCES_HEADER
  50. Specifies the grit-generated header file that maps ID names to ID values.
  51. It's required to map the IDs in LOCALIZABLE_ID_LIST and INFOPLIST_TEMPLATE
  52. to strings in the data packs.
  53. """
  54. import codecs
  55. import optparse
  56. import os
  57. import re
  58. import sys
  59. # Prepend the grit module from the source tree so it takes precedence over other
  60. # grit versions that might present in the search path.
  61. sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..', '..', '..',
  62. 'tools', 'grit'))
  63. from grit.format import data_pack
  64. sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..',
  65. 'third_party'))
  66. import jinja2
  67. LOCALIZABLE_STRINGS = 'Localizable.strings'
  68. INFOPLIST_STRINGS = 'InfoPlist.strings'
  69. class LocalizeException(Exception):
  70. pass
  71. class LocalizedStringJinja2Adapter:
  72. """Class that maps ID names to localized strings in Jinja2."""
  73. def __init__(self, id_map, pack):
  74. self.id_map = id_map
  75. self.pack = pack
  76. def __getattr__(self, name):
  77. id_value = self.id_map.get(name)
  78. if not id_value:
  79. raise LocalizeException('Could not find id %s in resource header' % name)
  80. data = self.pack.resources.get(id_value)
  81. if not data:
  82. raise LocalizeException(
  83. 'Could not find string with id %s (%d) in data pack' %
  84. (name, id_value))
  85. return decode_and_escape(data)
  86. def get_inputs(from_dir, locales):
  87. """Returns the list of files that would be required to run the tool."""
  88. inputs = []
  89. for locale in locales:
  90. inputs.append(os.path.join(from_dir, '%s.pak' % locale))
  91. return format_quoted_list(inputs)
  92. def get_outputs(to_dir, locales):
  93. """Returns the list of files that would be produced by the tool."""
  94. outputs = []
  95. for locale in locales:
  96. lproj_dir = format_lproj_dir(to_dir, locale)
  97. outputs.append(os.path.join(lproj_dir, LOCALIZABLE_STRINGS))
  98. outputs.append(os.path.join(lproj_dir, INFOPLIST_STRINGS))
  99. return format_quoted_list(outputs)
  100. def format_quoted_list(items):
  101. """Formats a list as a string, with items space-separated and quoted."""
  102. return " ".join(['"%s"' % x for x in items])
  103. def format_lproj_dir(to_dir, locale):
  104. """Formats the name of the lproj directory for a given locale."""
  105. locale = locale.replace('-', '_')
  106. return os.path.join(to_dir, '%s.lproj' % locale)
  107. def read_resources_header(resources_header_path):
  108. """Reads and parses a grit-generated resource header file.
  109. This function will parse lines like the following:
  110. #define IDS_PRODUCT_NAME 28531
  111. #define IDS_CANCEL 28542
  112. And return a dictionary like the following:
  113. { 'IDS_PRODUCT_NAME': 28531, 'IDS_CANCEL': 28542 }
  114. """
  115. regex = re.compile(r'^#define\s+(\w+)\s+(\d+)$')
  116. id_map = {}
  117. try:
  118. with open(resources_header_path, 'r') as f:
  119. for line in f:
  120. match = regex.match(line)
  121. if match:
  122. id_str = match.group(1)
  123. id_value = int(match.group(2))
  124. id_map[id_str] = id_value
  125. except:
  126. sys.stderr.write('Error while reading header file %s\n'
  127. % resources_header_path)
  128. raise
  129. return id_map
  130. def read_id_list(id_list_path):
  131. """Read a text file with ID names.
  132. Names are stripped of leading and trailing spaces. Empty lines are ignored.
  133. """
  134. with open(id_list_path, 'r') as f:
  135. stripped_lines = [x.strip() for x in f]
  136. non_empty_lines = [x for x in stripped_lines if x]
  137. return non_empty_lines
  138. def read_jinja2_template(template_path):
  139. """Reads a Jinja2 template."""
  140. (template_dir, template_name) = os.path.split(template_path)
  141. env = jinja2.Environment(loader = jinja2.FileSystemLoader(template_dir))
  142. template = env.get_template(template_name)
  143. return template
  144. def decode_and_escape(data):
  145. """Decodes utf-8 data, and escapes it appropriately to use in *.strings."""
  146. u_string = codecs.decode(data, 'utf-8')
  147. u_string = u_string.replace('\\', '\\\\')
  148. u_string = u_string.replace('"', '\\"')
  149. return u_string
  150. def generate(from_dir, to_dir, localizable_list_path, infoplist_template_path,
  151. resources_header_path, locales):
  152. """Generates the <locale>.lproj directories and files."""
  153. id_map = read_resources_header(resources_header_path)
  154. localizable_ids = read_id_list(localizable_list_path)
  155. infoplist_template = read_jinja2_template(infoplist_template_path)
  156. # Generate string files for each locale
  157. for locale in locales:
  158. pack = data_pack.ReadDataPack(
  159. os.path.join(os.path.join(from_dir, '%s.pak' % locale)))
  160. lproj_dir = format_lproj_dir(to_dir, locale)
  161. if not os.path.exists(lproj_dir):
  162. os.makedirs(lproj_dir)
  163. # Generate Localizable.strings
  164. localizable_strings_path = os.path.join(lproj_dir, LOCALIZABLE_STRINGS)
  165. try:
  166. with codecs.open(localizable_strings_path, 'w', 'utf-16') as f:
  167. for id_str in localizable_ids:
  168. id_value = id_map.get(id_str)
  169. if not id_value:
  170. raise LocalizeException('Could not find "%s" in %s' %
  171. (id_str, resources_header_path))
  172. localized_data = pack.resources.get(id_value)
  173. if not localized_data:
  174. raise LocalizeException(
  175. 'Could not find localized string in %s for %s (%d)' %
  176. (localizable_strings_path, id_str, id_value))
  177. f.write(u'"%s" = "%s";\n' %
  178. (id_str, decode_and_escape(localized_data)))
  179. except:
  180. sys.stderr.write('Error while creating %s\n' % localizable_strings_path)
  181. raise
  182. # Generate InfoPlist.strings
  183. infoplist_strings_path = os.path.join(lproj_dir, INFOPLIST_STRINGS)
  184. try:
  185. with codecs.open(infoplist_strings_path, 'w', 'utf-16') as f:
  186. infoplist = infoplist_template.render(
  187. ids = LocalizedStringJinja2Adapter(id_map, pack))
  188. f.write(infoplist)
  189. except:
  190. sys.stderr.write('Error while creating %s\n' % infoplist_strings_path)
  191. raise
  192. def DoMain(args):
  193. """Entrypoint used by gyp's pymod_do_main."""
  194. parser = optparse.OptionParser("usage: %prog [options] locales")
  195. parser.add_option("--print-inputs", action="store_true", dest="print_input",
  196. default=False,
  197. help="Print the expected input file list, then exit.")
  198. parser.add_option("--print-outputs", action="store_true", dest="print_output",
  199. default=False,
  200. help="Print the expected output file list, then exit.")
  201. parser.add_option("--from-dir", action="store", dest="from_dir",
  202. help="Source data pack directory.")
  203. parser.add_option("--to-dir", action="store", dest="to_dir",
  204. help="Destination data pack directory.")
  205. parser.add_option("--localizable-list", action="store",
  206. dest="localizable_list",
  207. help="File with list of IDS to build Localizable.strings")
  208. parser.add_option("--infoplist-template", action="store",
  209. dest="infoplist_template",
  210. help="File with list of IDS to build InfoPlist.strings")
  211. parser.add_option("--resources-header", action="store",
  212. dest="resources_header",
  213. help="Auto-generated header with resource ids.")
  214. options, locales = parser.parse_args(args)
  215. if not locales:
  216. parser.error('At least one locale is required.')
  217. if options.print_input and options.print_output:
  218. parser.error('Only one of --print-inputs or --print-outputs is allowed')
  219. if options.print_input:
  220. if not options.from_dir:
  221. parser.error('--from-dir is required.')
  222. return get_inputs(options.from_dir, locales)
  223. if options.print_output:
  224. if not options.to_dir:
  225. parser.error('--to-dir is required.')
  226. return get_outputs(options.to_dir, locales)
  227. if not (options.from_dir and options.to_dir and options.localizable_list and
  228. options.infoplist_template and options.resources_header):
  229. parser.error('--from-dir, --to-dir, --localizable-list, ' +
  230. '--infoplist-template and --resources-header are required.')
  231. try:
  232. generate(options.from_dir, options.to_dir, options.localizable_list,
  233. options.infoplist_template, options.resources_header, locales)
  234. except LocalizeException as e:
  235. sys.stderr.write('Error: %s\n' % str(e))
  236. sys.exit(1)
  237. return ""
  238. def main(args):
  239. print DoMain(args[1:])
  240. if __name__ == '__main__':
  241. main(sys.argv)