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 7set of source files. For file formats where it is not possible to infer which 8strings represent message identifiers, localized strings should be explicitly 9annotated with the string "i18n-content", for example: 10 11 LocalizeString(/*i18n-content*/"PRODUCT_NAME"); 12 13This script also recognises localized strings in HTML and manifest.json files: 14 15 HTML: <span i18n-content="PRODUCT_NAME"></span> 16 or ...i18n-value-name-1="BUTTON_NAME"... 17 manifest.json: __MSG_PRODUCT_NAME__ 18 19Note that these forms must be exact; extra spaces are not permitted, though 20either single or double quotes are recognized. 21 22In addition, the script checks that all the messages are still in use; if 23this is not the case then a warning is issued, but the script still succeeds. 24""" 25 26import json 27import os 28import optparse 29import re 30import sys 31import xml.dom.minidom as minidom 32 33WARNING_MESSAGE = """ 34To remove this warning, either remove the unused tags from 35resource files, add the files that use the tags listed above to 36remoting.gyp, or annotate existing uses of those tags with the 37prefix /*i18n-content*/ 38""" 39 40def LoadTagsFromGrd(filename): 41 xml = minidom.parse(filename) 42 tags = [] 43 msgs_and_structs = xml.getElementsByTagName("message") 44 msgs_and_structs.extend(xml.getElementsByTagName("structure")) 45 for res in msgs_and_structs: 46 name = res.getAttribute("name") 47 if not name or not name.startswith("IDR_"): 48 raise Exception("Tag name doesn't start with IDR_: %s" % name) 49 tags.append(name[4:]) 50 return tags 51 52def ExtractTagFromLine(file_type, line): 53 """Extract a tag from a line of HTML, C++, JS or JSON.""" 54 if file_type == "html": 55 # HTML-style (tags) 56 m = re.search('i18n-content=[\'"]([^\'"]*)[\'"]', 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('IDR_([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': 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 81 82def VerifyFile(filename, messages, used_tags): 83 """ 84 Parse |filename|, looking for tags and report any that are not included in 85 |messages|. Return True if all tags are present and correct, or False if 86 any are missing. If no tags are found, print a warning message and return 87 True. 88 """ 89 90 base_name, extension = os.path.splitext(filename) 91 extension = extension[1:] 92 if extension not in ['js', 'cc', 'html', 'json', 'jinja2', 'mm']: 93 raise Exception("Unknown file type: %s" % extension) 94 95 result = True 96 matches = False 97 f = open(filename, 'r') 98 lines = f.readlines() 99 for i in xrange(0, len(lines)): 100 tag = ExtractTagFromLine(extension, lines[i]) 101 if tag: 102 tag = tag.upper() 103 used_tags.add(tag) 104 matches = True 105 if not tag in messages: 106 result = False 107 print '%s/%s:%d: error: Undefined tag: %s' % \ 108 (os.getcwd(), filename, i + 1, tag) 109 if not matches: 110 print '%s/%s:0: warning: No tags found' % (os.getcwd(), filename) 111 f.close() 112 return result 113 114 115def main(): 116 parser = optparse.OptionParser( 117 usage='Usage: %prog [options...] [source_file...]') 118 parser.add_option('-t', '--touch', dest='touch', 119 help='File to touch when finished.') 120 parser.add_option('-r', '--grd', dest='grd', action='append', 121 help='grd file') 122 123 options, args = parser.parse_args() 124 if not options.touch: 125 print '-t is not specified.' 126 return 1 127 if len(options.grd) == 0 or len(args) == 0: 128 print 'At least one GRD file needs to be specified.' 129 return 1 130 131 resources = [] 132 for f in options.grd: 133 resources.extend(LoadTagsFromGrd(f)) 134 135 used_tags = set([]) 136 exit_code = 0 137 for f in args: 138 if not VerifyFile(f, resources, used_tags): 139 exit_code = 1 140 141 warnings = False 142 for tag in resources: 143 if tag not in used_tags: 144 print ('%s/%s:0: warning: %s is defined but not used') % \ 145 (os.getcwd(), sys.argv[2], tag) 146 warnings = True 147 if warnings: 148 print WARNING_MESSAGE 149 150 if exit_code == 0: 151 f = open(options.touch, 'a') 152 f.close() 153 os.utime(options.touch, None) 154 155 return exit_code 156 157 158if __name__ == '__main__': 159 sys.exit(main()) 160