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 tags = [] 44 msgs_and_structs = xml.getElementsByTagName("message") 45 msgs_and_structs.extend(xml.getElementsByTagName("structure")) 46 for res in msgs_and_structs: 47 name = res.getAttribute("name") 48 if not name or not name.startswith("IDR_"): 49 raise Exception("Tag name doesn't start with IDR_: %s" % name) 50 tags.append(name[4:]) 51 return tags 52 53 def ExtractTagFromLine(file_type, line): 54 """Extract a tag from a line of HTML, C++, JS or JSON.""" 55 if file_type == "html": 56 # HTML-style (tags) 57 m = re.search('i18n-content=[\'"]([^\'"]*)[\'"]', line) 58 if m: return m.group(1) 59 # HTML-style (titles) 60 m = re.search('i18n-title=[\'"]([^\'"]*)[\'"]', line) 61 if m: return m.group(1) 62 # HTML-style (substitutions) 63 m = re.search('i18n-value-name-[1-9]=[\'"]([^\'"]*)[\'"]', line) 64 if m: return m.group(1) 65 elif file_type == 'js': 66 # Javascript style 67 m = re.search('/\*i18n-content\*/[\'"]([^\`"]*)[\'"]', line) 68 if m: return m.group(1) 69 elif file_type == 'cc' or file_type == 'mm': 70 # C++ style 71 m = re.search('IDR_([A-Z0-9_]*)', line) 72 if m: return m.group(1) 73 m = re.search('/\*i18n-content\*/["]([^\`"]*)["]', line) 74 if m: return m.group(1) 75 elif file_type == 'json': 76 # Manifest style 77 m = re.search('__MSG_(.*)__', line) 78 if m: return m.group(1) 79 elif file_type == 'jinja2': 80 # Jinja2 template file 81 m = re.search('\{\%\s+trans\s+\%\}([A-Z0-9_]+)\{\%\s+endtrans\s+\%\}', line) 82 if m: return m.group(1) 83 return None 84 85 86 def VerifyFile(filename, messages, used_tags): 87 """ 88 Parse |filename|, looking for tags and report any that are not included in 89 |messages|. Return True if all tags are present and correct, or False if 90 any are missing. If no tags are found, print a warning message and return 91 True. 92 """ 93 94 base_name, extension = os.path.splitext(filename) 95 extension = extension[1:] 96 if extension not in ['js', 'cc', 'html', 'json', 'jinja2', 'mm']: 97 raise Exception("Unknown file type: %s" % extension) 98 99 result = True 100 matches = False 101 f = open(filename, 'r') 102 lines = f.readlines() 103 for i in xrange(0, len(lines)): 104 tag = ExtractTagFromLine(extension, lines[i]) 105 if tag: 106 tag = tag.upper() 107 used_tags.add(tag) 108 matches = True 109 if not tag in messages: 110 result = False 111 print '%s/%s:%d: error: Undefined tag: %s' % \ 112 (os.getcwd(), filename, i + 1, tag) 113 if not matches: 114 print '%s/%s:0: warning: No tags found' % (os.getcwd(), filename) 115 f.close() 116 return result 117 118 119 def main(): 120 parser = optparse.OptionParser( 121 usage='Usage: %prog [options...] [source_file...]') 122 parser.add_option('-t', '--touch', dest='touch', 123 help='File to touch when finished.') 124 parser.add_option('-r', '--grd', dest='grd', action='append', 125 help='grd file') 126 127 options, args = parser.parse_args() 128 if not options.touch: 129 print '-t is not specified.' 130 return 1 131 if len(options.grd) == 0 or len(args) == 0: 132 print 'At least one GRD file needs to be specified.' 133 return 1 134 135 resources = [] 136 for f in options.grd: 137 resources.extend(LoadTagsFromGrd(f)) 138 139 used_tags = set([]) 140 exit_code = 0 141 for f in args: 142 if not VerifyFile(f, resources, used_tags): 143 exit_code = 1 144 145 warnings = False 146 for tag in resources: 147 if tag not in used_tags: 148 print ('%s/%s:0: warning: %s is defined but not used') % \ 149 (os.getcwd(), sys.argv[2], tag) 150 warnings = True 151 if warnings: 152 print WARNING_MESSAGE 153 154 if exit_code == 0: 155 f = open(options.touch, 'a') 156 f.close() 157 os.utime(options.touch, None) 158 159 return exit_code 160 161 162 if __name__ == '__main__': 163 sys.exit(main()) 164