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: 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 20Note that these forms must be exact; extra spaces are not permitted, though 21either single or double quotes are recognized. 22 23In addition, the script checks that all the messages are still in use; if 24this is not the case then a warning is issued, but the script still succeeds. 25""" 26 27import json 28import os 29import optparse 30import re 31import sys 32import xml.dom.minidom as minidom 33 34WARNING_MESSAGE = """ 35To remove this warning, either remove the unused tags from 36resource files, add the files that use the tags listed above to 37remoting.gyp, or annotate existing uses of those tags with the 38prefix /*i18n-content*/ 39""" 40 41def 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 59def 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 92def 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. 97 """ 98 99 base_name, file_type = os.path.splitext(filename) 100 file_type = file_type[1:] 101 if file_type == 'jinja2' and base_name.endswith('.json'): 102 file_type = 'json.jinja2' 103 if file_type not in ['js', 'cc', 'html', 'json.jinja2', 'jinja2', 'mm']: 104 raise Exception("Unknown file type: %s" % file_type) 105 106 result = True 107 matches = False 108 f = open(filename, 'r') 109 lines = f.readlines() 110 for i in xrange(0, len(lines)): 111 tag = ExtractTagFromLine(file_type, lines[i]) 112 if tag: 113 tag = tag.upper() 114 used_tags.add(tag) 115 matches = True 116 if not tag in messages: 117 result = False 118 print '%s/%s:%d: error: Undefined tag: %s' % \ 119 (os.getcwd(), filename, i + 1, tag) 120 f.close() 121 return result 122 123 124def main(): 125 parser = optparse.OptionParser( 126 usage='Usage: %prog [options...] [source_file...]') 127 parser.add_option('-t', '--touch', dest='touch', 128 help='File to touch when finished.') 129 parser.add_option('-r', '--grd', dest='grd', action='append', 130 help='grd file') 131 parser.add_option('--strict', dest='strict', action='store_true', 132 help='Use strict verification checks.') 133 134 options, args = parser.parse_args() 135 if not options.touch: 136 print '-t is not specified.' 137 return 1 138 if len(options.grd) == 0 or len(args) == 0: 139 print 'At least one GRD file needs to be specified.' 140 return 1 141 142 all_resources = [] 143 non_android_resources = [] 144 for f in options.grd: 145 android_tags, other_tags = LoadTagsFromGrd(f) 146 all_resources.extend(android_tags + other_tags) 147 non_android_resources.extend(other_tags) 148 149 used_tags = set([]) 150 exit_code = 0 151 for f in args: 152 if not VerifyFile(f, all_resources, used_tags): 153 exit_code = 1 154 155 if options.strict: 156 warnings = False 157 # Determining if a resource is being used in the Android app is tricky 158 # because it requires annotating and parsing Android XML layout files. 159 # For now, exclude Android strings from this check. 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 176if __name__ == '__main__': 177 sys.exit(main()) 178