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   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 
     59 def 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 
     92 def 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 
    127 def 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 
    176 if __name__ == '__main__':
    177   sys.exit(main())
    178