blob: 871c0faabc1e04bc8c2f37ff8013d4014e1d2eb3 [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
[email protected]5c071ed2013-09-10 17:31:1815 HTML: i18n-content="PRODUCT_NAME"
16 or i18n-value-name-1="BUTTON_NAME"
17 or i18n-title="TOOLTIP_NAME"
[email protected]dc91bc562012-11-12 19:28:1818 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)
[email protected]e908b232014-02-21 23:26:2043 android_tags = []
44 other_tags = []
[email protected]dc91bc562012-11-12 19:28:1845 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")
[email protected]8bf71782014-01-27 21:44:1149 if not name or not name.startswith("IDS_"):
50 raise Exception("Tag name doesn't start with IDS_: %s" % name)
[email protected]e908b232014-02-21 23:26:2051 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
[email protected]dc91bc562012-11-12 19:28:1858
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)
[email protected]5c071ed2013-09-10 17:31:1865 # HTML-style (titles)
66 m = re.search('i18n-title=[\'"]([^\'"]*)[\'"]', line)
67 if m: return m.group(1)
[email protected]dc91bc562012-11-12 19:28:1868 # 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)
[email protected]fc877cf2013-07-31 23:08:3975 elif file_type == 'cc' or file_type == 'mm':
[email protected]dc91bc562012-11-12 19:28:1876 # C++ style
[email protected]8bf71782014-01-27 21:44:1177 m = re.search('IDS_([A-Z0-9_]*)', line)
[email protected]dc91bc562012-11-12 19:28:1878 if m: return m.group(1)
79 m = re.search('/\*i18n-content\*/["]([^\`"]*)["]', line)
80 if m: return m.group(1)
[email protected]098f45c2014-03-04 18:30:2681 elif file_type == 'json.jinja2':
[email protected]dc91bc562012-11-12 19:28:1882 # Manifest style
83 m = re.search('__MSG_(.*)__', line)
84 if m: return m.group(1)
[email protected]afebae12013-07-09 17:27:3385 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)
[email protected]dc91bc562012-11-12 19:28:1889 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
garykacbdd4f782014-11-03 23:26:2196 any are missing.
[email protected]dc91bc562012-11-12 19:28:1897 """
98
[email protected]098f45c2014-03-04 18:30:2699 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)
[email protected]dc91bc562012-11-12 19:28:18105
106 result = True
107 matches = False
108 f = open(filename, 'r')
109 lines = f.readlines()
110 for i in xrange(0, len(lines)):
[email protected]098f45c2014-03-04 18:30:26111 tag = ExtractTagFromLine(file_type, lines[i])
[email protected]dc91bc562012-11-12 19:28:18112 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)
[email protected]dc91bc562012-11-12 19:28:18120 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')
garykacb7e9e03a2015-01-14 23:52:01131 parser.add_option('--strict', dest='strict', action='store_true',
132 help='Use strict verification checks.')
[email protected]dc91bc562012-11-12 19:28:18133
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
[email protected]e908b232014-02-21 23:26:20142 all_resources = []
143 non_android_resources = []
[email protected]dc91bc562012-11-12 19:28:18144 for f in options.grd:
[email protected]e908b232014-02-21 23:26:20145 android_tags, other_tags = LoadTagsFromGrd(f)
146 all_resources.extend(android_tags + other_tags)
147 non_android_resources.extend(other_tags)
[email protected]dc91bc562012-11-12 19:28:18148
149 used_tags = set([])
150 exit_code = 0
151 for f in args:
[email protected]e908b232014-02-21 23:26:20152 if not VerifyFile(f, all_resources, used_tags):
[email protected]dc91bc562012-11-12 19:28:18153 exit_code = 1
154
garykacb7e9e03a2015-01-14 23:52:01155 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
[email protected]dc91bc562012-11-12 19:28:18167
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())