check-translation.py
246 lines
| 7.8 KiB
| text/x-python
|
PythonLexer
/ i18n / check-translation.py
FUJIWARA Katsunori
|
r20152 | #!/usr/bin/env python | ||
# | ||||
# check-translation.py - check Mercurial specific translation problems | ||||
Augie Fackler
|
r33900 | from __future__ import absolute_import | ||
import re | ||||
FUJIWARA Katsunori
|
r20152 | |||
import polib | ||||
timeless@mozdev.org
|
r26261 | scanners = [] | ||
FUJIWARA Katsunori
|
r20152 | checkers = [] | ||
timeless@mozdev.org
|
r26261 | def scanner(): | ||
def decorator(func): | ||||
scanners.append(func) | ||||
return func | ||||
return decorator | ||||
Mads Kiilerich
|
r22203 | def levelchecker(level, msgidpat): | ||
FUJIWARA Katsunori
|
r20152 | def decorator(func): | ||
if msgidpat: | ||||
match = re.compile(msgidpat).search | ||||
else: | ||||
match = lambda msgid: True | ||||
checkers.append((func, level)) | ||||
func.match = match | ||||
return func | ||||
return decorator | ||||
def match(checker, pe): | ||||
"""Examine whether POEntry "pe" is target of specified checker or not | ||||
""" | ||||
if not checker.match(pe.msgid): | ||||
return | ||||
# examine suppression by translator comment | ||||
nochecker = 'no-%s-check' % checker.__name__ | ||||
for tc in pe.tcomment.split(): | ||||
if nochecker == tc: | ||||
return | ||||
return True | ||||
#################### | ||||
def fatalchecker(msgidpat=None): | ||||
Mads Kiilerich
|
r22203 | return levelchecker('fatal', msgidpat) | ||
FUJIWARA Katsunori
|
r20152 | |||
@fatalchecker(r'\$\$') | ||||
def promptchoice(pe): | ||||
"""Check translation of the string given to "ui.promptchoice()" | ||||
>>> pe = polib.POEntry( | ||||
... msgid ='prompt$$missing &sep$$missing &$$followed by &none', | ||||
... msgstr='prompt missing &sep$$missing amp$$followed by none&') | ||||
>>> match(promptchoice, pe) | ||||
True | ||||
Augie Fackler
|
r33681 | >>> for e in promptchoice(pe): print(e) | ||
FUJIWARA Katsunori
|
r20152 | number of choices differs between msgid and msgstr | ||
msgstr has invalid choice missing '&' | ||||
msgstr has invalid '&' followed by none | ||||
""" | ||||
idchoices = [c.rstrip(' ') for c in pe.msgid.split('$$')[1:]] | ||||
strchoices = [c.rstrip(' ') for c in pe.msgstr.split('$$')[1:]] | ||||
if len(idchoices) != len(strchoices): | ||||
yield "number of choices differs between msgid and msgstr" | ||||
indices = [(c, c.find('&')) for c in strchoices] | ||||
if [c for c, i in indices if i == -1]: | ||||
yield "msgstr has invalid choice missing '&'" | ||||
if [c for c, i in indices if len(c) == i + 1]: | ||||
yield "msgstr has invalid '&' followed by none" | ||||
timeless@mozdev.org
|
r26261 | deprecatedpe = None | ||
@scanner() | ||||
def deprecatedsetup(pofile): | ||||
Yuya Nishihara
|
r26852 | pes = [p for p in pofile if p.msgid == '(DEPRECATED)' and p.msgstr] | ||
timeless@mozdev.org
|
r26261 | if len(pes): | ||
global deprecatedpe | ||||
deprecatedpe = pes[0] | ||||
FUJIWARA Katsunori
|
r26837 | @fatalchecker(r'\(DEPRECATED\)') | ||
timeless@mozdev.org
|
r26261 | def deprecated(pe): | ||
"""Check for DEPRECATED | ||||
>>> ped = polib.POEntry( | ||||
Yuya Nishihara
|
r26852 | ... msgid = '(DEPRECATED)', | ||
... msgstr= '(DETACERPED)') | ||||
timeless@mozdev.org
|
r26261 | >>> deprecatedsetup([ped]) | ||
>>> pe = polib.POEntry( | ||||
... msgid = 'Something (DEPRECATED)', | ||||
timeless@mozdev.org
|
r26277 | ... msgstr= 'something (DEPRECATED)') | ||
>>> match(deprecated, pe) | ||||
True | ||||
Augie Fackler
|
r33681 | >>> for e in deprecated(pe): print(e) | ||
timeless@mozdev.org
|
r26277 | >>> pe = polib.POEntry( | ||
... msgid = 'Something (DEPRECATED)', | ||||
timeless@mozdev.org
|
r26261 | ... msgstr= 'something (DETACERPED)') | ||
>>> match(deprecated, pe) | ||||
True | ||||
Augie Fackler
|
r33681 | >>> for e in deprecated(pe): print(e) | ||
timeless@mozdev.org
|
r26261 | >>> pe = polib.POEntry( | ||
... msgid = 'Something (DEPRECATED)', | ||||
... msgstr= 'something') | ||||
>>> match(deprecated, pe) | ||||
True | ||||
Augie Fackler
|
r33681 | >>> for e in deprecated(pe): print(e) | ||
timeless@mozdev.org
|
r26261 | msgstr inconsistently translated (DEPRECATED) | ||
FUJIWARA Katsunori
|
r26837 | >>> pe = polib.POEntry( | ||
... msgid = 'Something (DEPRECATED, foo bar)', | ||||
... msgstr= 'something (DETACERPED, foo bar)') | ||||
>>> match(deprecated, pe) | ||||
timeless@mozdev.org
|
r26261 | """ | ||
timeless@mozdev.org
|
r26276 | if not ('(DEPRECATED)' in pe.msgstr or | ||
FUJIWARA Katsunori
|
r26838 | (deprecatedpe and | ||
timeless@mozdev.org
|
r26276 | deprecatedpe.msgstr in pe.msgstr)): | ||
yield "msgstr inconsistently translated (DEPRECATED)" | ||||
timeless@mozdev.org
|
r26261 | |||
FUJIWARA Katsunori
|
r20152 | #################### | ||
def warningchecker(msgidpat=None): | ||||
Mads Kiilerich
|
r22203 | return levelchecker('warning', msgidpat) | ||
FUJIWARA Katsunori
|
r20152 | |||
FUJIWARA Katsunori
|
r20514 | @warningchecker() | ||
def taildoublecolons(pe): | ||||
"""Check equality of tail '::'-ness between msgid and msgstr | ||||
>>> pe = polib.POEntry( | ||||
... msgid ='ends with ::', | ||||
... msgstr='ends with ::') | ||||
Augie Fackler
|
r33681 | >>> for e in taildoublecolons(pe): print(e) | ||
FUJIWARA Katsunori
|
r20514 | >>> pe = polib.POEntry( | ||
... msgid ='ends with ::', | ||||
... msgstr='ends without double-colons') | ||||
Augie Fackler
|
r33681 | >>> for e in taildoublecolons(pe): print(e) | ||
FUJIWARA Katsunori
|
r20514 | tail '::'-ness differs between msgid and msgstr | ||
>>> pe = polib.POEntry( | ||||
... msgid ='ends without double-colons', | ||||
... msgstr='ends with ::') | ||||
Augie Fackler
|
r33681 | >>> for e in taildoublecolons(pe): print(e) | ||
FUJIWARA Katsunori
|
r20514 | tail '::'-ness differs between msgid and msgstr | ||
""" | ||||
if pe.msgid.endswith('::') != pe.msgstr.endswith('::'): | ||||
yield "tail '::'-ness differs between msgid and msgstr" | ||||
FUJIWARA Katsunori
|
r20515 | @warningchecker() | ||
def indentation(pe): | ||||
"""Check equality of initial indentation between msgid and msgstr | ||||
This may report unexpected warning, because this doesn't aware | ||||
the syntax of rst document and the context of msgstr. | ||||
>>> pe = polib.POEntry( | ||||
... msgid =' indented text', | ||||
... msgstr=' narrowed indentation') | ||||
Augie Fackler
|
r33681 | >>> for e in indentation(pe): print(e) | ||
FUJIWARA Katsunori
|
r20515 | initial indentation width differs betweeen msgid and msgstr | ||
""" | ||||
idindent = len(pe.msgid) - len(pe.msgid.lstrip()) | ||||
strindent = len(pe.msgstr) - len(pe.msgstr.lstrip()) | ||||
if idindent != strindent: | ||||
yield "initial indentation width differs betweeen msgid and msgstr" | ||||
FUJIWARA Katsunori
|
r20152 | #################### | ||
def check(pofile, fatal=True, warning=False): | ||||
targetlevel = { 'fatal': fatal, 'warning': warning } | ||||
targetcheckers = [(checker, level) | ||||
for checker, level in checkers | ||||
if targetlevel[level]] | ||||
if not targetcheckers: | ||||
return [] | ||||
detected = [] | ||||
timeless@mozdev.org
|
r26261 | for checker in scanners: | ||
checker(pofile) | ||||
FUJIWARA Katsunori
|
r20152 | for pe in pofile.translated_entries(): | ||
errors = [] | ||||
for checker, level in targetcheckers: | ||||
if match(checker, pe): | ||||
errors.extend((level, checker.__name__, error) | ||||
for error in checker(pe)) | ||||
if errors: | ||||
detected.append((pe, errors)) | ||||
return detected | ||||
######################################## | ||||
if __name__ == "__main__": | ||||
import sys | ||||
import optparse | ||||
optparser = optparse.OptionParser("""%prog [options] pofile ... | ||||
This checks Mercurial specific translation problems in specified | ||||
'*.po' files. | ||||
Each detected problems are shown in the format below:: | ||||
filename:linenum:type(checker): problem detail ..... | ||||
"type" is "fatal" or "warning". "checker" is the name of the function | ||||
detecting corresponded error. | ||||
Checking by checker "foo" on the specific msgstr can be suppressed by | ||||
the "translator comment" like below. Multiple "no-xxxx-check" should | ||||
be separated by whitespaces:: | ||||
# no-foo-check | ||||
msgid = "....." | ||||
msgstr = "....." | ||||
""") | ||||
optparser.add_option("", "--warning", | ||||
help="show also warning level problems", | ||||
action="store_true") | ||||
optparser.add_option("", "--doctest", | ||||
help="run doctest of this tool, instead of check", | ||||
action="store_true") | ||||
(options, args) = optparser.parse_args() | ||||
if options.doctest: | ||||
Matt Mackall
|
r20164 | import os | ||
Matt Mackall
|
r20158 | if 'TERM' in os.environ: | ||
del os.environ['TERM'] | ||||
FUJIWARA Katsunori
|
r20152 | import doctest | ||
failures, tests = doctest.testmod() | ||||
sys.exit(failures and 1 or 0) | ||||
# replace polib._POFileParser to show linenum of problematic msgstr | ||||
class ExtPOFileParser(polib._POFileParser): | ||||
def process(self, symbol, linenum): | ||||
super(ExtPOFileParser, self).process(symbol, linenum) | ||||
if symbol == 'MS': # msgstr | ||||
self.current_entry.linenum = linenum | ||||
polib._POFileParser = ExtPOFileParser | ||||
detected = [] | ||||
warning = options.warning | ||||
for f in args: | ||||
detected.extend((f, pe, errors) | ||||
for pe, errors in check(polib.pofile(f), | ||||
warning=warning)) | ||||
if detected: | ||||
for f, pe, errors in detected: | ||||
for level, checker, error in errors: | ||||
sys.stderr.write('%s:%d:%s(%s): %s\n' | ||||
% (f, pe.linenum, level, checker, error)) | ||||
sys.exit(1) | ||||