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