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
TODO: fix it for the changed i18n stuff of moin 1.6
23
@copyright: 2003 Florian Festi, Nir Soffer, Thomas Waldmann
24
@license: GNU GPL, see COPYING for details.
27
output_encoding = 'utf-8'
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"]
33
# If you have cjkcodecs installed, use this:
37
import sys, os, compiler
38
from compiler.ast import Name, Const, CallFunc, Getattr
41
""" Walk through AST tree and collect text from gettext calls
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.
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.
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.
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.
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.
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')
73
def setFilename(self, filename):
74
"""Remember the filename we are parsing"""
75
self._filename = filename
77
def visitModule(self, node):
78
""" Start the search from the top node of a module
80
This is the entry point into the search. When compiler.walk is
81
called it calls this method with the module node.
83
This is the place to initialize module specific data.
85
self._visited = {} # init node cache - we will visit each node once
86
self._lineno = 'NA' # init line number
88
# Start walking in the module node
92
""" Walk through all nodes """
93
if node in self._visited:
94
# We visited this node already
97
self._visited[node] = 1
98
if not self.parseNode(node):
99
for child in node.getChildNodes():
102
def parseNode(self, node):
103
""" Parse function call nodes and collect text """
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
110
if node.__class__ == CallFunc and node.args:
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)
121
self.addBadCall(node)
125
def addText(self, text):
126
""" Add text to dictionary and count found texts.
128
Note that number of texts in dictionary could be different from
129
the number of texts found, because some texts appear several
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'.
136
self._lineno is the last line number we checked. It may be the line
137
number of the text, or near it.
140
self._found = self._found + 1
142
# Create key for this text if needed
143
if text not in self._dictionary:
144
self._dictionary[text] = {}
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]
151
textInfo[self._filename].append(self._lineno)
153
def addBadCall(self, node):
154
"""Called when a bad call like _('a' + 'b') is found"""
155
self._bad = self._bad + 1
157
print "<!> Warning: non-constant _ call:"
158
print " `%s`" % str(node)
159
print " `%s`:%s" % (self._filename, self._lineno)
163
def dictionary(self):
164
return self._dictionary
173
def visit(path, visitor):
174
visitor.setFilename(path)
175
tree = compiler.parseFile(path)
176
compiler.walk(tree, visitor)
179
# MoinMoin specific stuff follows
183
"""Language status report"""
184
def __init__(self, lang, sourceDict):
186
self.__sourceDict = sourceDict
187
self.__langDict = None
194
def loadLanguage(self):
195
filename = i18n.filename(self.__lang)
196
self.__langDict = pysupport.importName("MoinMoin.i18n." + filename, "text")
199
"""Compare language text dict against source dict"""
201
if not self.__langDict:
202
self.__error = "Language %s not found!" % self.__lang
206
# Collect missing texts
207
for text in self.__sourceDict:
208
if text not in self.__langDict:
209
self.__missing[text] = self.__sourceDict[text]
211
# Collect unused texts
212
for text in self.__langDict:
213
if text not in self.__sourceDict:
214
self.__unused[text] = self.__langDict[text]
218
"""Return summary dict"""
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
230
return self.__missing
236
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
userprefs options, from Icon titles etc.!
371
for text in report[lang].unused():
372
print " 1. `%r`" % text