blob: 045f95de5b77b90ffb63f7593b8062c2597ec761 [file] [log] [blame]
[email protected]dc91bc562012-11-12 19:28:181#!/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':
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 return None
76
77
78def VerifyFile(filename, messages, used_tags):
79 """
80 Parse |filename|, looking for tags and report any that are not included in
81 |messages|. Return True if all tags are present and correct, or False if
82 any are missing. If no tags are found, print a warning message and return
83 True.
84 """
85
86 base_name, extension = os.path.splitext(filename)
87 extension = extension[1:]
88 if extension not in ['js', 'cc', 'html', 'json']:
89 raise Exception("Unknown file type: %s" % extension)
90
91 result = True
92 matches = False
93 f = open(filename, 'r')
94 lines = f.readlines()
95 for i in xrange(0, len(lines)):
96 tag = ExtractTagFromLine(extension, lines[i])
97 if tag:
98 tag = tag.upper()
99 used_tags.add(tag)
100 matches = True
101 if not tag in messages:
102 result = False
103 print '%s/%s:%d: error: Undefined tag: %s' % \
104 (os.getcwd(), filename, i + 1, tag)
105 if not matches:
106 print '%s/%s:0: warning: No tags found' % (os.getcwd(), filename)
107 f.close()
108 return result
109
110
111def main():
112 parser = optparse.OptionParser(
113 usage='Usage: %prog [options...] [source_file...]')
114 parser.add_option('-t', '--touch', dest='touch',
115 help='File to touch when finished.')
116 parser.add_option('-r', '--grd', dest='grd', action='append',
117 help='grd file')
118
119 options, args = parser.parse_args()
120 if not options.touch:
121 print '-t is not specified.'
122 return 1
123 if len(options.grd) == 0 or len(args) == 0:
124 print 'At least one GRD file needs to be specified.'
125 return 1
126
127 resources = []
128 for f in options.grd:
129 resources.extend(LoadTagsFromGrd(f))
130
131 used_tags = set([])
132 exit_code = 0
133 for f in args:
134 if not VerifyFile(f, resources, used_tags):
135 exit_code = 1
136
137 warnings = False
138 for tag in resources:
139 if tag not in used_tags:
140 print ('%s/%s:0: warning: %s is defined but not used') % \
141 (os.getcwd(), sys.argv[2], tag)
142 warnings = True
143 if warnings:
144 print WARNING_MESSAGE
145
146 if exit_code == 0:
147 f = open(options.touch, 'a')
148 f.close()
149 os.utime(options.touch, None)
150
151 return exit_code
152
153
154if __name__ == '__main__':
155 sys.exit(main())