Home | History | Annotate | Download | only in tools
      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
      7 set of source files. For file formats where it is not possible to infer which
      8 strings represent message identifiers, localized strings should be explicitly
      9 annotated with the string "i18n-content", for example:
     10 
     11   LocalizeString(/*i18n-content*/"PRODUCT_NAME");
     12 
     13 This 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 
     20 Note that these forms must be exact; extra spaces are not permitted, though
     21 either single or double quotes are recognized.
     22 
     23 In addition, the script checks that all the messages are still in use; if
     24 this is not the case then a warning is issued, but the script still succeeds.
     25 """
     26 
     27 import json
     28 import os
     29 import optparse
     30 import re
     31 import sys
     32 import xml.dom.minidom as minidom
     33 
     34 WARNING_MESSAGE = """
     35 To remove this warning, either remove the unused tags from
     36 resource files, add the files that use the tags listed above to
     37 remoting.gyp, or annotate existing uses of those tags with the
     38 prefix /*i18n-content*/
     39 """
     40 
     41 def 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 
     53 def 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 
     86 def 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 
    119 def 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 
    162 if __name__ == '__main__':
    163   sys.exit(main())
    164