~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/i18n/tools/check_i18n.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python
 
2
# -*- coding: iso-8859-1 -*-
 
3
"""check_i18n - compare texts in the source with the language files
 
4
 
 
5
Searches in the MoinMoin sources for calls of _() and tries to extract
 
6
the parameter.  Then it checks the language modules if those parameters
 
7
are in the dictionary.
 
8
 
 
9
Usage: check_i18n.py [lang ...]
 
10
 
 
11
Without arguments, checks all languages in i18n or the specified
 
12
languages. Look into MoinMoin.i18n.__init__ for availeable language
 
13
names.
 
14
 
 
15
The script will run from the moin root directory, where the MoinMoin
 
16
package lives, or from MoinMoin/i18n where this script lives.
 
17
 
 
18
TextFinder class based on code by Seo Sanghyeon and the python compiler
 
19
package.
 
20
 
 
21
TODO: fix it for the changed i18n stuff of moin 1.6
 
22
 
 
23
@copyright: 2003 Florian Festi, Nir Soffer, Thomas Waldmann
 
24
@license: GNU GPL, see COPYING for details.
 
25
"""
 
26
 
 
27
output_encoding = 'utf-8'
 
28
 
 
29
# These lead to crashes (MemoryError - due to missing codecs?)
 
30
#blacklist_files = ["ja.py", "zh.py", "zh_tw.py"]
 
31
#blacklist_langs = ["ja", "zh", "zh-tw"]
 
32
 
 
33
# If you have cjkcodecs installed, use this:
 
34
blacklist_files = []
 
35
blacklist_langs = []
 
36
 
 
37
import sys, os, compiler
 
38
from compiler.ast import Name, Const, CallFunc, Getattr
 
39
 
 
40
class TextFinder:
 
41
    """ Walk through AST tree and collect text from gettext calls
 
42
 
 
43
    Find all calls to gettext function in the source tree and collect
 
44
    the texts in a dict. Use compiler to create an abstract syntax tree
 
45
    from each source file, then find the nodes for gettext function
 
46
    call, and get the text from the call.
 
47
 
 
48
    Localized texts are used usually translated during runtime by
 
49
    gettext functions and apear in the source as
 
50
    _('text...'). TextFinder class finds calls to the '_' function in
 
51
    any namespace, or your prefered gettext function.
 
52
 
 
53
    Note that TextFinder will only retrieve text from function calls
 
54
    with a constant argument like _('text'). Calls like _('text' % locals()),
 
55
    _('text 1' + 'text 2') are marked as bad call in the report, and the
 
56
    text is not retrieved into the dictionary.
 
57
 
 
58
    Note also that texts in source can appear several times in the same
 
59
    file or different files, but they will only apear once in the
 
60
    dictionary that this tool creates.
 
61
 
 
62
    The dictionary value for each text is a dictionary of filenames each
 
63
    containing a list of (best guess) lines numbers containning the text.
 
64
    """
 
65
 
 
66
    def __init__(self, name='_'):
 
67
        """ Init with the gettext function name or '_'"""
 
68
        self._name = name       # getText function name
 
69
        self._dictionary = {}   # Unique texts in the found texts
 
70
        self._found = 0         # All good calls including duplicates
 
71
        self._bad = 0           # Bad calls: _('%s' % var) or _('a' + 'b')
 
72
 
 
73
    def setFilename(self, filename):
 
74
        """Remember the filename we are parsing"""
 
75
        self._filename = filename
 
76
 
 
77
    def visitModule(self, node):
 
78
        """ Start the search from the top node of a module
 
79
 
 
80
        This is the entry point into the search. When compiler.walk is
 
81
        called it calls this method with the module node.
 
82
 
 
83
        This is the place to initialize module specific data.
 
84
        """
 
85
        self._visited = {}  # init node cache - we will visit each node once
 
86
        self._lineno = 'NA' # init line number
 
87
 
 
88
        # Start walking in the module node
 
89
        self.walk(node)
 
90
 
 
91
    def walk(self, node):
 
92
        """ Walk through all nodes """
 
93
        if node in self._visited:
 
94
            # We visited this node already
 
95
            return
 
96
 
 
97
        self._visited[node] = 1
 
98
        if not self.parseNode(node):
 
99
            for child in node.getChildNodes():
 
100
                self.walk(child)
 
101
 
 
102
    def parseNode(self, node):
 
103
        """ Parse function call nodes and collect text """
 
104
 
 
105
        # Get the current line number. Since not all nodes have a line number
 
106
        # we save the last line number - it should be close to the gettext call
 
107
        if node.lineno is not None:
 
108
            self._lineno = node.lineno
 
109
 
 
110
        if node.__class__ == CallFunc and node.args:
 
111
            child = node.node
 
112
            klass = child.__class__
 
113
            if (# Standard call _('text')
 
114
                (klass == Name and child.name == self._name) or
 
115
                # A call to an object attribute: object._('text')
 
116
                (klass == Getattr and child.attrname == self._name)):
 
117
                if node.args[0].__class__ == Const:
 
118
                    # Good call with a constant _('text')
 
119
                    self.addText(node.args[0].value)
 
120
                else:
 
121
                    self.addBadCall(node)
 
122
                return 1
 
123
        return 0
 
124
 
 
125
    def addText(self, text):
 
126
        """ Add text to dictionary and count found texts.
 
127
 
 
128
        Note that number of texts in dictionary could be different from
 
129
        the number of texts found, because some texts appear several
 
130
        times in the code.
 
131
 
 
132
        Each text value is a dictionary of filenames that contain the
 
133
        text and each filename value is the list of line numbers with
 
134
        the text. Missing line numbers are recorded as 'NA'.
 
135
 
 
136
        self._lineno is the last line number we checked. It may be the line
 
137
        number of the text, or near it.
 
138
        """
 
139
 
 
140
        self._found = self._found + 1
 
141
 
 
142
        # Create key for this text if needed
 
143
        if text not in self._dictionary:
 
144
            self._dictionary[text] = {}
 
145
 
 
146
        # Create key for this filename if needed
 
147
        textInfo = self._dictionary[text]
 
148
        if self._filename not in textInfo:
 
149
            textInfo[self._filename] = [self._lineno]
 
150
        else:
 
151
            textInfo[self._filename].append(self._lineno)
 
152
 
 
153
    def addBadCall(self, node):
 
154
        """Called when a bad call like _('a' + 'b') is found"""
 
155
        self._bad = self._bad + 1
 
156
        print
 
157
        print "<!> Warning: non-constant _ call:"
 
158
        print " `%s`" % str(node)
 
159
        print " `%s`:%s" % (self._filename, self._lineno)
 
160
 
 
161
    # Accessors
 
162
 
 
163
    def dictionary(self):
 
164
        return self._dictionary
 
165
 
 
166
    def bad(self):
 
167
        return self._bad
 
168
 
 
169
    def found(self):
 
170
        return self._found
 
171
 
 
172
 
 
173
def visit(path, visitor):
 
174
    visitor.setFilename(path)
 
175
    tree = compiler.parseFile(path)
 
176
    compiler.walk(tree, visitor)
 
177
 
 
178
 
 
179
# MoinMoin specific stuff follows
 
180
 
 
181
 
 
182
class Report:
 
183
    """Language status report"""
 
184
    def __init__(self, lang, sourceDict):
 
185
        self.__lang = lang
 
186
        self.__sourceDict = sourceDict
 
187
        self.__langDict = None
 
188
        self.__missing = {}
 
189
        self.__unused = {}
 
190
        self.__error = None
 
191
        self.__ready = 0
 
192
        self.create()
 
193
 
 
194
    def loadLanguage(self):
 
195
        filename = i18n.filename(self.__lang)
 
196
        self.__langDict = pysupport.importName("MoinMoin.i18n." + filename, "text")
 
197
 
 
198
    def create(self):
 
199
        """Compare language text dict against source dict"""
 
200
        self.loadLanguage()
 
201
        if not self.__langDict:
 
202
            self.__error = "Language %s not found!" % self.__lang
 
203
            self.__ready = 1
 
204
            return
 
205
 
 
206
        # Collect missing texts
 
207
        for text in self.__sourceDict:
 
208
            if text not in self.__langDict:
 
209
                self.__missing[text] = self.__sourceDict[text]
 
210
 
 
211
        # Collect unused texts
 
212
        for text in self.__langDict:
 
213
            if text not in self.__sourceDict:
 
214
                self.__unused[text] = self.__langDict[text]
 
215
        self.__ready = 1
 
216
 
 
217
    def summary(self):
 
218
        """Return summary dict"""
 
219
        summary = {
 
220
            'name': i18n.languages[self.__lang][i18n.ENAME].encode(output_encoding),
 
221
            'maintainer': i18n.languages[self.__lang][i18n.MAINTAINER],
 
222
            'total': len(self.__langDict),
 
223
            'missing': len(self.__missing),
 
224
            'unused': len(self.__unused),
 
225
            'error': self.__error
 
226
            }
 
227
        return summary
 
228
 
 
229
    def missing(self):
 
230
        return self.__missing
 
231
 
 
232
    def unused(self):
 
233
        return self.__unused
 
234
 
 
235
 
 
236
if __name__ == '__main__':
 
237
    import time
 
238
 
 
239
    # Check that we run from the root directory where MoinMoin package lives
 
240
    # or from the i18n directory when this script lives
 
241
    if os.path.exists('MoinMoin/__init__.py'):
 
242
        # Running from the root directory
 
243
        MoinMoin_dir = os.curdir
 
244
    elif os.path.exists(os.path.join(os.pardir, 'i18n')):
 
245
        # Runing from i18n
 
246
        MoinMoin_dir = os.path.join(os.pardir, os.pardir)
 
247
    else:
 
248
        print __doc__
 
249
        sys.exit(1)
 
250
 
 
251
    # Insert MoinMoin_dir into sys.path
 
252
    sys.path.insert(0, MoinMoin_dir)
 
253
    from MoinMoin import i18n
 
254
    from MoinMoin.util import pysupport
 
255
 
 
256
    textFinder = TextFinder()
 
257
    found = 0
 
258
    unique = 0
 
259
    bad = 0
 
260
 
 
261
    # Find gettext calls in the source
 
262
    for root, dirs, files in os.walk(os.path.join(MoinMoin_dir, 'MoinMoin')):
 
263
        for name in files:
 
264
            if name.endswith('.py'):
 
265
                if name in blacklist_files: continue
 
266
                path = os.path.join(root, name)
 
267
                #print '%(path)s:' % locals(),
 
268
                visit(path, textFinder)
 
269
 
 
270
                # Report each file's results
 
271
                new_unique = len(textFinder.dictionary()) - unique
 
272
                new_found = textFinder.found() - found
 
273
                #print '%(new_unique)d (of %(new_found)d)' % locals()
 
274
 
 
275
                # Warn about bad calls - these should be fixed!
 
276
                new_bad = textFinder.bad() - bad
 
277
                #if new_bad:
 
278
                #    print '### Warning: %(new_bad)d bad call(s)' % locals()
 
279
 
 
280
                unique = unique + new_unique
 
281
                bad = bad + new_bad
 
282
                found = found + new_found
 
283
 
 
284
    # Print report using wiki markup, so we can publish this on MoinDev
 
285
    # !!! Todo:
 
286
    #     save executive summary for the wiki
 
287
    #     save separate report for each language to be sent to the
 
288
    #     language translator.
 
289
    #     Update the wiki using XML-RPC??
 
290
 
 
291
    print "This page is generated by `MoinMoin/i18n/check_i18n.py`."
 
292
    print "To recreate this report run `make check-i18n` and paste here"
 
293
    print
 
294
    print '----'
 
295
    print
 
296
    print '<<TableOfContents(2)>>'
 
297
    print
 
298
    print
 
299
    print "= Translation Report ="
 
300
    print
 
301
    print "== Summary =="
 
302
    print
 
303
    print 'Created on %s' % time.asctime()
 
304
    print
 
305
 
 
306
    print ('\n%(unique)d unique texts in dictionary of %(found)d texts '
 
307
           'in source.') % locals()
 
308
    if bad:
 
309
        print '\n%(bad)d bad calls.' % locals()
 
310
    print
 
311
 
 
312
    # Check languages from the command line or from moin.i18n against
 
313
    # the source
 
314
    if sys.argv[1:]:
 
315
        languages = sys.argv[1:]
 
316
    else:
 
317
        languages = i18n.languages.keys()
 
318
        for lang in blacklist_langs:
 
319
            # problems, maybe due to encoding?
 
320
            if lang in languages:
 
321
                languages.remove(lang)
 
322
    if 'en' in languages:
 
323
        languages.remove('en') # there is no en lang file
 
324
    languages.sort()
 
325
 
 
326
    # Create report for all languages
 
327
    report = {}
 
328
    for lang in languages:
 
329
        report[lang] = Report(lang, textFinder.dictionary())
 
330
 
 
331
    # Print summary for all languages
 
332
    print ("||<:>'''Language'''||<:>'''Texts'''||<:>'''Missing'''"
 
333
           "||<:>'''Unused'''||")
 
334
    for lang in languages:
 
335
        print ("||%(name)s||<)>%(total)s||<)>%(missing)s||<)>%(unused)s||"
 
336
               ) % report[lang].summary()
 
337
 
 
338
    # Print details
 
339
    for lang in languages:
 
340
        dict = report[lang].summary()
 
341
        print
 
342
        print "== %(name)s ==" % dict
 
343
        print
 
344
        print "Maintainer: <<MailTo(%(maintainer)s)>>" % dict
 
345
 
 
346
        # Print missing texts, if any
 
347
        if report[lang].missing():
 
348
            print """
 
349
=== Missing texts ===
 
350
 
 
351
These items should ''definitely'' get fixed.
 
352
 
 
353
Maybe the corresponding english text in the source code was only changed
 
354
slightly, then you want to look for a similar text in the ''unused''
 
355
section below and modify i18n, so that it will match again.
 
356
"""
 
357
            for text in report[lang].missing():
 
358
                print " 1. `%r`" % text
 
359
 
 
360
        # Print unused texts, if any
 
361
        if report[lang].unused():
 
362
            print """
 
363
=== Possibly unused texts ===
 
364
 
 
365
Be ''very careful'' and double-check before removing any of these
 
366
potentially unused items.
 
367
 
 
368
This program can't detect references done from wiki pages, from
 
369
userprefs options, from Icon titles etc.!
 
370
"""
 
371
            for text in report[lang].unused():
 
372
                print " 1. `%r`" % text
 
373
 
 
374