2
# -*- coding: iso-8859-1 -*-
3
"""check_i18n - compare texts in the source with the language files
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
9
Usage: check_i18n.py [lang ...]
11
Without arguments, checks all languages in i18n or the specified
12
languages. Look into MoinMoin.i18n.__init__ for availeable language
15
The script will run from the moin root directory, where the MoinMoin
16
package lives, or from MoinMoin/i18n where this script lives.
18
TextFinder class based on code by Seo Sanghyeon and the python compiler
21
@copyright: 2003 Florian Festi, Nir Soffer, Thomas Waldmann
22
@license: GNU GPL, see COPYING for details.
25
output_encoding = 'utf-8'
27
# These lead to crashes (MemoryError - due to missing codecs?)
28
#blacklist_files = ["ja.py", "zh.py", "zh_tw.py"]
29
#blacklist_langs = ["ja", "zh", "zh-tw"]
31
# If you have cjkcodecs installed, use this:
35
import sys, os, compiler
36
from compiler.ast import Name, Const, CallFunc, Getattr
39
""" Walk through AST tree and collect text from gettext calls
41
Find all calls to gettext function in the source tree and collect
42
the texts in a dict. Use compiler to create an abstract syntax tree
43
from each source file, then find the nodes for gettext function
44
call, and get the text from the call.
46
Localized texts are used usually translated during runtime by
47
gettext functions and apear in the source as
48
_('text...'). TextFinder class finds calls to the '_' function in
49
any namespace, or your prefered gettext function.
51
Note that TextFinder will only retrieve text from function calls
52
with a constant argument like _('text'). Calls like _('text' % locals()),
53
_('text 1' + 'text 2') are marked as bad call in the report, and the
54
text is not retrieved into the dictionary.
56
Note also that texts in source can appear several times in the same
57
file or different files, but they will only apear once in the
58
dictionary that this tool creates.
60
The dictionary value for each text is a dictionary of filenames each
61
containing a list of (best guess) lines numbers containning the text.
64
def __init__(self, name='_'):
65
""" Init with the gettext function name or '_'"""
66
self._name = name # getText function name
67
self._dictionary = {} # Unique texts in the found texts
68
self._found = 0 # All good calls including duplicates
69
self._bad = 0 # Bad calls: _('%s' % var) or _('a' + 'b')
71
def setFilename(self, filename):
72
"""Remember the filename we are parsing"""
73
self._filename = filename
75
def visitModule(self, node):
76
""" Start the search from the top node of a module
78
This is the entry point into the search. When compiler.walk is
79
called it calls this method with the module node.
81
This is the place to initialize module specific data.
83
self._visited = {} # init node cache - we will visit each node once
84
self._lineno = 'NA' # init line number
86
# Start walking in the module node
90
""" Walk through all nodes """
91
if self._visited.has_key(node):
92
# We visited this node already
95
self._visited[node] = 1
96
if not self.parseNode(node):
97
for child in node.getChildNodes():
100
def parseNode(self, node):
101
""" Parse function call nodes and collect text """
103
# Get the current line number. Since not all nodes have a line number
104
# we save the last line number - it should be close to the gettext call
105
if node.lineno != None:
106
self._lineno = node.lineno
108
if node.__class__ == CallFunc and node.args:
110
klass = child.__class__
111
if (# Standard call _('text')
112
(klass == Name and child.name == self._name) or
113
# A call to an object attribute: object._('text')
114
(klass == Getattr and child.attrname == self._name)):
115
if node.args[0].__class__ == Const:
116
# Good call with a constant _('text')
117
self.addText(node.args[0].value)
119
self.addBadCall(node)
123
def addText(self, text):
124
""" Add text to dictionary and count found texts.
126
Note that number of texts in dictionary could be different from
127
the number of texts found, because some texts appear several
130
Each text value is a dictionary of filenames that contain the
131
text and each filename value is the list of line numbers with
132
the text. Missing line numbers are recorded as 'NA'.
134
self._lineno is the last line number we checked. It may be the line
135
number of the text, or near it.
138
self._found = self._found + 1
140
# Create key for this text if needed
141
if not self._dictionary.has_key(text):
142
self._dictionary[text] = {}
144
# Create key for this filename if needed
145
textInfo = self._dictionary[text]
146
if not textInfo.has_key(self._filename):
147
textInfo[self._filename] = [self._lineno]
149
textInfo[self._filename].append(self._lineno)
151
def addBadCall(self, node):
152
"""Called when a bad call like _('a' + 'b') is found"""
153
self._bad = self._bad + 1
155
print "<!> Warning: non-constant _ call:"
156
print " `%s`" % str(node)
157
print " `%s`:%s" % (self._filename, self._lineno)
161
def dictionary(self):
162
return self._dictionary
171
def visit(path, visitor):
172
visitor.setFilename(path)
173
tree = compiler.parseFile(path)
174
compiler.walk(tree, visitor)
177
# MoinMoin specific stuff follows
181
"""Language status report"""
182
def __init__(self, lang, sourceDict):
184
self.__sourceDict = sourceDict
185
self.__langDict = None
192
def loadLanguage(self):
193
filename = i18n.filename(self.__lang)
194
self.__langDict = pysupport.importName("MoinMoin.i18n." + filename, "text")
197
"""Compare language text dict against source dict"""
199
if not self.__langDict:
200
self.__error = "Language %s not found!" % self.__lang
204
# Collect missing texts
205
for text in self.__sourceDict:
206
if not self.__langDict.has_key(text):
207
self.__missing[text] = self.__sourceDict[text]
209
# Collect unused texts
210
for text in self.__langDict:
211
if not self.__sourceDict.has_key(text):
212
self.__unused[text] = self.__langDict[text]
216
"""Return summary dict"""
218
'name': i18n.languages[self.__lang][i18n.ENAME].encode(output_encoding),
219
'maintainer': i18n.languages[self.__lang][i18n.MAINTAINER],
220
'total' : len(self.__langDict),
221
'missing': len(self.__missing),
222
'unused': len(self.__unused),
223
'error': self.__error
228
return self.__missing
235
if __name__ == '__main__':
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')):
246
MoinMoin_dir = os.path.join(os.pardir, os.pardir)
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
256
textFinder = TextFinder()
261
# Find gettext calls in the source
262
for root, dirs, files in os.walk(os.path.join(MoinMoin_dir, 'MoinMoin')):
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)
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()
275
# Warn about bad calls - these should be fixed!
276
new_bad = textFinder.bad() - bad
278
# print '### Warning: %(new_bad)d bad call(s)' % locals()
280
unique = unique + new_unique
282
found = found + new_found
284
# Print report using wiki markup, so we can publish this on MoinDev
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??
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"
296
print '[[TableOfContents(2)]]'
299
print "= Translation Report ="
301
print "== Summary =="
303
print 'Created on %s' % time.asctime()
306
print ('\n%(unique)d unique texts in dictionary of %(found)d texts '
307
'in source.') % locals()
309
print '\n%(bad)d bad calls.' % locals()
312
# Check languages from the command line or from moin.i18n against
315
languages = sys.argv[1:]
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
326
# Create report for all languages
328
for lang in languages:
329
report[lang] = Report(lang, textFinder.dictionary())
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()
339
for lang in languages:
340
dict = report[lang].summary()
342
print "== %(name)s ==" % dict
344
print "Maintainer: [[MailTo(%(maintainer)s)]]" % dict
346
# Print missing texts, if any
347
if report[lang].missing():
349
=== Missing texts ===
351
These items should ''definitely'' get fixed.
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.
357
for text in report[lang].missing():
358
print " 1. `%r`" % text
360
# Print unused texts, if any
361
if report[lang].unused():
363
=== Possibly unused texts ===
365
Be ''very careful'' and double-check before removing any of these
366
potentially unused items.
368
This program can't detect references done from wiki pages, from
369
UserPreferences options, from Icon titles etc.!
371
for text in report[lang].unused():
372
print " 1. `%r`" % text