verify_resources.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env python
  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.
  5. """Verifies that GRD resource files define all the strings used by a given
  6. set of source files. For file formats where it is not possible to infer which
  7. strings represent message identifiers, localized strings should be explicitly
  8. annotated with the string "i18n-content", for example:
  9. LocalizeString(/*i18n-content*/"PRODUCT_NAME");
  10. This script also recognises localized strings in HTML and manifest.json files:
  11. HTML: i18n-content="PRODUCT_NAME"
  12. or i18n-value-name-1="BUTTON_NAME"
  13. or i18n-title="TOOLTIP_NAME"
  14. manifest.json: __MSG_PRODUCT_NAME__
  15. Note that these forms must be exact; extra spaces are not permitted, though
  16. either single or double quotes are recognized.
  17. In addition, the script checks that all the messages are still in use; if
  18. this is not the case then a warning is issued, but the script still succeeds.
  19. """
  20. from __future__ import print_function
  21. import json
  22. import os
  23. import optparse
  24. import re
  25. import sys
  26. import xml.dom.minidom as minidom
  27. WARNING_MESSAGE = """
  28. To remove this warning, either remove the unused tags from
  29. resource files, add the files that use the tags listed above to
  30. remoting.gyp, or annotate existing uses of those tags with the
  31. prefix /*i18n-content*/
  32. """
  33. def LoadTagsFromGrd(filename):
  34. xml = minidom.parse(filename)
  35. android_tags = []
  36. other_tags = []
  37. msgs_and_structs = xml.getElementsByTagName("message")
  38. msgs_and_structs.extend(xml.getElementsByTagName("structure"))
  39. for res in msgs_and_structs:
  40. name = res.getAttribute("name")
  41. if not name or not name.startswith("IDS_"):
  42. raise Exception("Tag name doesn't start with IDS_: %s" % name)
  43. name = name[4:]
  44. if 'android_java' in res.getAttribute('formatter_data'):
  45. android_tags.append(name)
  46. else:
  47. other_tags.append(name)
  48. return android_tags, other_tags
  49. def ExtractTagFromLine(file_type, line):
  50. """Extract a tag from a line of HTML, C++, JS or JSON."""
  51. if file_type == "html":
  52. # HTML-style (tags)
  53. m = re.search('i18n-content=[\'"]([^\'"]*)[\'"]', line)
  54. if m: return m.group(1)
  55. # HTML-style (titles)
  56. m = re.search('i18n-title=[\'"]([^\'"]*)[\'"]', line)
  57. if m: return m.group(1)
  58. # HTML-style (substitutions)
  59. m = re.search('i18n-value-name-[1-9]=[\'"]([^\'"]*)[\'"]', line)
  60. if m: return m.group(1)
  61. elif file_type == 'js':
  62. # Javascript style
  63. m = re.search('/\*i18n-content\*/[\'"]([^\`"]*)[\'"]', line)
  64. if m: return m.group(1)
  65. elif file_type == 'cc' or file_type == 'mm':
  66. # C++ style
  67. m = re.search('IDS_([A-Z0-9_]*)', line)
  68. if m: return m.group(1)
  69. m = re.search('/\*i18n-content\*/["]([^\`"]*)["]', line)
  70. if m: return m.group(1)
  71. elif file_type == 'json.jinja2':
  72. # Manifest style
  73. m = re.search('__MSG_(.*)__', line)
  74. if m: return m.group(1)
  75. elif file_type == 'jinja2':
  76. # Jinja2 template file
  77. m = re.search('\{\%\s+trans\s+\%\}([A-Z0-9_]+)\{\%\s+endtrans\s+\%\}', line)
  78. if m: return m.group(1)
  79. return None
  80. def VerifyFile(filename, messages, used_tags):
  81. """
  82. Parse |filename|, looking for tags and report any that are not included in
  83. |messages|. Return True if all tags are present and correct, or False if
  84. any are missing.
  85. """
  86. base_name, file_type = os.path.splitext(filename)
  87. file_type = file_type[1:]
  88. if file_type == 'jinja2' and base_name.endswith('.json'):
  89. file_type = 'json.jinja2'
  90. if file_type not in ['js', 'cc', 'html', 'json.jinja2', 'jinja2', 'mm']:
  91. raise Exception("Unknown file type: %s" % file_type)
  92. result = True
  93. matches = False
  94. f = open(filename, 'r')
  95. lines = f.readlines()
  96. for i in range(0, len(lines)):
  97. tag = ExtractTagFromLine(file_type, lines[i])
  98. if tag:
  99. tag = tag.upper()
  100. used_tags.add(tag)
  101. matches = True
  102. if not tag in messages:
  103. result = False
  104. print('%s/%s:%d: error: Undefined tag: %s' %
  105. (os.getcwd(), filename, i + 1, tag))
  106. f.close()
  107. return result
  108. def main():
  109. parser = optparse.OptionParser(
  110. usage='Usage: %prog [options...] [source_file...]')
  111. parser.add_option('-t', '--touch', dest='touch',
  112. help='File to touch when finished.')
  113. parser.add_option('-r', '--grd', dest='grd', action='append',
  114. help='grd file')
  115. parser.add_option('--strict', dest='strict', action='store_true',
  116. help='Use strict verification checks.')
  117. options, args = parser.parse_args()
  118. if not options.touch:
  119. print('-t is not specified.')
  120. return 1
  121. if len(options.grd) == 0 or len(args) == 0:
  122. print('At least one GRD file needs to be specified.')
  123. return 1
  124. all_resources = []
  125. non_android_resources = []
  126. for f in options.grd:
  127. android_tags, other_tags = LoadTagsFromGrd(f)
  128. all_resources.extend(android_tags + other_tags)
  129. non_android_resources.extend(other_tags)
  130. used_tags = set([])
  131. exit_code = 0
  132. for f in args:
  133. if not VerifyFile(f, all_resources, used_tags):
  134. exit_code = 1
  135. if options.strict:
  136. warnings = False
  137. # Determining if a resource is being used in the Android app is tricky
  138. # because it requires annotating and parsing Android XML layout files.
  139. # For now, exclude Android strings from this check.
  140. for tag in non_android_resources:
  141. if tag not in used_tags:
  142. print('%s/%s:0: warning: %s is defined but not used' %
  143. (os.getcwd(), sys.argv[2], tag))
  144. warnings = True
  145. if warnings:
  146. print(WARNING_MESSAGE)
  147. if exit_code == 0:
  148. f = open(options.touch, 'a')
  149. f.close()
  150. os.utime(options.touch, None)
  151. return exit_code
  152. if __name__ == '__main__':
  153. sys.exit(main())