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