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  tags = []
44  msgs_and_structs = xml.getElementsByTagName("message")
45  msgs_and_structs.extend(xml.getElementsByTagName("structure"))
46  for res in msgs_and_structs:
47    name = res.getAttribute("name")
48    if not name or not name.startswith("IDR_"):
49      raise Exception("Tag name doesn't start with IDR_: %s" % name)
50    tags.append(name[4:])
51  return tags
52
53def ExtractTagFromLine(file_type, line):
54  """Extract a tag from a line of HTML, C++, JS or JSON."""
55  if file_type == "html":
56    # HTML-style (tags)
57    m = re.search('i18n-content=[\'"]([^\'"]*)[\'"]', line)
58    if m: return m.group(1)
59    # HTML-style (titles)
60    m = re.search('i18n-title=[\'"]([^\'"]*)[\'"]', line)
61    if m: return m.group(1)
62    # HTML-style (substitutions)
63    m = re.search('i18n-value-name-[1-9]=[\'"]([^\'"]*)[\'"]', line)
64    if m: return m.group(1)
65  elif file_type == 'js':
66    # Javascript style
67    m = re.search('/\*i18n-content\*/[\'"]([^\`"]*)[\'"]', line)
68    if m: return m.group(1)
69  elif file_type == 'cc' or file_type == 'mm':
70    # C++ style
71    m = re.search('IDR_([A-Z0-9_]*)', line)
72    if m: return m.group(1)
73    m = re.search('/\*i18n-content\*/["]([^\`"]*)["]', line)
74    if m: return m.group(1)
75  elif file_type == 'json':
76    # Manifest style
77    m = re.search('__MSG_(.*)__', line)
78    if m: return m.group(1)
79  elif file_type == 'jinja2':
80    # Jinja2 template file
81    m = re.search('\{\%\s+trans\s+\%\}([A-Z0-9_]+)\{\%\s+endtrans\s+\%\}', line)
82    if m: return m.group(1)
83  return None
84
85
86def VerifyFile(filename, messages, used_tags):
87  """
88  Parse |filename|, looking for tags and report any that are not included in
89  |messages|. Return True if all tags are present and correct, or False if
90  any are missing. If no tags are found, print a warning message and return
91  True.
92  """
93
94  base_name, extension = os.path.splitext(filename)
95  extension = extension[1:]
96  if extension not in ['js', 'cc', 'html', 'json', 'jinja2', 'mm']:
97    raise Exception("Unknown file type: %s" % extension)
98
99  result = True
100  matches = False
101  f = open(filename, 'r')
102  lines = f.readlines()
103  for i in xrange(0, len(lines)):
104    tag = ExtractTagFromLine(extension, lines[i])
105    if tag:
106      tag = tag.upper()
107      used_tags.add(tag)
108      matches = True
109      if not tag in messages:
110        result = False
111        print '%s/%s:%d: error: Undefined tag: %s' % \
112            (os.getcwd(), filename, i + 1, tag)
113  if not matches:
114    print '%s/%s:0: warning: No tags found' % (os.getcwd(), filename)
115  f.close()
116  return result
117
118
119def main():
120  parser = optparse.OptionParser(
121      usage='Usage: %prog [options...] [source_file...]')
122  parser.add_option('-t', '--touch', dest='touch',
123                    help='File to touch when finished.')
124  parser.add_option('-r', '--grd', dest='grd', action='append',
125                    help='grd file')
126
127  options, args = parser.parse_args()
128  if not options.touch:
129    print '-t is not specified.'
130    return 1
131  if len(options.grd) == 0 or len(args) == 0:
132    print 'At least one GRD file needs to be specified.'
133    return 1
134
135  resources = []
136  for f in options.grd:
137    resources.extend(LoadTagsFromGrd(f))
138
139  used_tags = set([])
140  exit_code = 0
141  for f in args:
142    if not VerifyFile(f, resources, used_tags):
143      exit_code = 1
144
145  warnings = False
146  for tag in resources:
147    if tag not in used_tags:
148      print ('%s/%s:0: warning: %s is defined but not used') % \
149          (os.getcwd(), sys.argv[2], tag)
150      warnings = True
151  if warnings:
152    print WARNING_MESSAGE
153
154  if exit_code == 0:
155    f = open(options.touch, 'a')
156    f.close()
157    os.utime(options.touch, None)
158
159  return exit_code
160
161
162if __name__ == '__main__':
163  sys.exit(main())
164