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. If no tags are found, print a warning message and return
97  True.
98  """
99
100  base_name, file_type = os.path.splitext(filename)
101  file_type = file_type[1:]
102  if file_type == 'jinja2' and base_name.endswith('.json'):
103    file_type = 'json.jinja2'
104  if file_type not in ['js', 'cc', 'html', 'json.jinja2', 'jinja2', 'mm']:
105    raise Exception("Unknown file type: %s" % file_type)
106
107  result = True
108  matches = False
109  f = open(filename, 'r')
110  lines = f.readlines()
111  for i in xrange(0, len(lines)):
112    tag = ExtractTagFromLine(file_type, lines[i])
113    if tag:
114      tag = tag.upper()
115      used_tags.add(tag)
116      matches = True
117      if not tag in messages:
118        result = False
119        print '%s/%s:%d: error: Undefined tag: %s' % \
120            (os.getcwd(), filename, i + 1, tag)
121  if not matches:
122    print '%s/%s:0: warning: No tags found' % (os.getcwd(), filename)
123  f.close()
124  return result
125
126
127def main():
128  parser = optparse.OptionParser(
129      usage='Usage: %prog [options...] [source_file...]')
130  parser.add_option('-t', '--touch', dest='touch',
131                    help='File to touch when finished.')
132  parser.add_option('-r', '--grd', dest='grd', action='append',
133                    help='grd file')
134
135  options, args = parser.parse_args()
136  if not options.touch:
137    print '-t is not specified.'
138    return 1
139  if len(options.grd) == 0 or len(args) == 0:
140    print 'At least one GRD file needs to be specified.'
141    return 1
142
143  all_resources = []
144  non_android_resources = []
145  for f in options.grd:
146    android_tags, other_tags = LoadTagsFromGrd(f)
147    all_resources.extend(android_tags + other_tags)
148    non_android_resources.extend(other_tags)
149
150  used_tags = set([])
151  exit_code = 0
152  for f in args:
153    if not VerifyFile(f, all_resources, used_tags):
154      exit_code = 1
155
156  # Determining if a resource is being used in the Android app is tricky
157  # because it requires annotating and parsing Android XML layout files.
158  # For now, exclude Android strings from this check.
159  warnings = False
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