check-translation.py
271 lines
| 7.4 KiB
| text/x-python
|
PythonLexer
/ i18n / check-translation.py
|
r46434 | #!/usr/bin/env python3 | ||
|
r20152 | # | ||
# check-translation.py - check Mercurial specific translation problems | ||||
|
r33900 | from __future__ import absolute_import | ||
import re | ||||
|
r20152 | |||
import polib | ||||
|
r26261 | scanners = [] | ||
|
r20152 | checkers = [] | ||
|
r43346 | |||
|
r26261 | def scanner(): | ||
def decorator(func): | ||||
scanners.append(func) | ||||
return func | ||||
|
r43346 | |||
|
r26261 | return decorator | ||
|
r43346 | |||
|
r22203 | def levelchecker(level, msgidpat): | ||
|
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 | ||||
|
r43346 | |||
|
r20152 | return decorator | ||
|
r43346 | |||
|
r20152 | def match(checker, pe): | ||
|
r46554 | """Examine whether POEntry "pe" is target of specified checker or not""" | ||
|
r20152 | 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 | ||||
|
r43346 | |||
|
r20152 | #################### | ||
|
r43346 | |||
|
r20152 | def fatalchecker(msgidpat=None): | ||
|
r22203 | return levelchecker('fatal', msgidpat) | ||
|
r20152 | |||
|
r43346 | |||
|
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 | ||||
|
r33681 | >>> for e in promptchoice(pe): print(e) | ||
|
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" | ||||
|
r43346 | |||
|
r26261 | deprecatedpe = None | ||
|
r43346 | |||
|
r26261 | @scanner() | ||
def deprecatedsetup(pofile): | ||||
|
r26852 | pes = [p for p in pofile if p.msgid == '(DEPRECATED)' and p.msgstr] | ||
|
r26261 | if len(pes): | ||
global deprecatedpe | ||||
deprecatedpe = pes[0] | ||||
|
r43346 | |||
|
r26837 | @fatalchecker(r'\(DEPRECATED\)') | ||
|
r26261 | def deprecated(pe): | ||
"""Check for DEPRECATED | ||||
>>> ped = polib.POEntry( | ||||
|
r26852 | ... msgid = '(DEPRECATED)', | ||
... msgstr= '(DETACERPED)') | ||||
|
r26261 | >>> deprecatedsetup([ped]) | ||
>>> pe = polib.POEntry( | ||||
... msgid = 'Something (DEPRECATED)', | ||||
|
r26277 | ... msgstr= 'something (DEPRECATED)') | ||
>>> match(deprecated, pe) | ||||
True | ||||
|
r33681 | >>> for e in deprecated(pe): print(e) | ||
|
r26277 | >>> pe = polib.POEntry( | ||
... msgid = 'Something (DEPRECATED)', | ||||
|
r26261 | ... msgstr= 'something (DETACERPED)') | ||
>>> match(deprecated, pe) | ||||
True | ||||
|
r33681 | >>> for e in deprecated(pe): print(e) | ||
|
r26261 | >>> pe = polib.POEntry( | ||
... msgid = 'Something (DEPRECATED)', | ||||
... msgstr= 'something') | ||||
>>> match(deprecated, pe) | ||||
True | ||||
|
r33681 | >>> for e in deprecated(pe): print(e) | ||
|
r26261 | msgstr inconsistently translated (DEPRECATED) | ||
|
r26837 | >>> pe = polib.POEntry( | ||
... msgid = 'Something (DEPRECATED, foo bar)', | ||||
... msgstr= 'something (DETACERPED, foo bar)') | ||||
>>> match(deprecated, pe) | ||||
|
r26261 | """ | ||
|
r43346 | if not ( | ||
'(DEPRECATED)' in pe.msgstr | ||||
or (deprecatedpe and deprecatedpe.msgstr in pe.msgstr) | ||||
): | ||||
|
r26276 | yield "msgstr inconsistently translated (DEPRECATED)" | ||
|
r26261 | |||
|
r43346 | |||
|
r20152 | #################### | ||
|
r43346 | |||
|
r20152 | def warningchecker(msgidpat=None): | ||
|
r22203 | return levelchecker('warning', msgidpat) | ||
|
r20152 | |||
|
r43346 | |||
|
r20514 | @warningchecker() | ||
def taildoublecolons(pe): | ||||
"""Check equality of tail '::'-ness between msgid and msgstr | ||||
>>> pe = polib.POEntry( | ||||
... msgid ='ends with ::', | ||||
... msgstr='ends with ::') | ||||
|
r33681 | >>> for e in taildoublecolons(pe): print(e) | ||
|
r20514 | >>> pe = polib.POEntry( | ||
... msgid ='ends with ::', | ||||
... msgstr='ends without double-colons') | ||||
|
r33681 | >>> for e in taildoublecolons(pe): print(e) | ||
|
r20514 | tail '::'-ness differs between msgid and msgstr | ||
>>> pe = polib.POEntry( | ||||
... msgid ='ends without double-colons', | ||||
... msgstr='ends with ::') | ||||
|
r33681 | >>> for e in taildoublecolons(pe): print(e) | ||
|
r20514 | tail '::'-ness differs between msgid and msgstr | ||
""" | ||||
if pe.msgid.endswith('::') != pe.msgstr.endswith('::'): | ||||
yield "tail '::'-ness differs between msgid and msgstr" | ||||
|
r43346 | |||
|
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') | ||||
|
r33681 | >>> for e in indentation(pe): print(e) | ||
|
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" | ||||
|
r43346 | |||
|
r20152 | #################### | ||
|
r43346 | |||
|
r20152 | def check(pofile, fatal=True, warning=False): | ||
|
r43346 | targetlevel = {'fatal': fatal, 'warning': warning} | ||
targetcheckers = [ | ||||
(checker, level) for checker, level in checkers if targetlevel[level] | ||||
] | ||||
|
r20152 | if not targetcheckers: | ||
return [] | ||||
detected = [] | ||||
|
r26261 | for checker in scanners: | ||
checker(pofile) | ||||
|
r20152 | for pe in pofile.translated_entries(): | ||
errors = [] | ||||
for checker, level in targetcheckers: | ||||
if match(checker, pe): | ||||
|
r43346 | errors.extend( | ||
(level, checker.__name__, error) for error in checker(pe) | ||||
) | ||||
|
r20152 | if errors: | ||
detected.append((pe, errors)) | ||||
return detected | ||||
|
r43346 | |||
|
r20152 | ######################################## | ||
if __name__ == "__main__": | ||||
import sys | ||||
import optparse | ||||
|
r43346 | optparser = optparse.OptionParser( | ||
"""%prog [options] pofile ... | ||||
|
r20152 | |||
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 = "....." | ||||
|
r43346 | """ | ||
) | ||||
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", | ||||
) | ||||
|
r20152 | (options, args) = optparser.parse_args() | ||
if options.doctest: | ||||
|
r20164 | import os | ||
|
r43346 | |||
|
r20158 | if 'TERM' in os.environ: | ||
del os.environ['TERM'] | ||||
|
r20152 | import doctest | ||
|
r43346 | |||
|
r20152 | failures, tests = doctest.testmod() | ||
sys.exit(failures and 1 or 0) | ||||
detected = [] | ||||
warning = options.warning | ||||
for f in args: | ||||
|
r43346 | detected.extend( | ||
(f, pe, errors) | ||||
for pe, errors in check(polib.pofile(f), warning=warning) | ||||
) | ||||
|
r20152 | if detected: | ||
for f, pe, errors in detected: | ||||
for level, checker, error in errors: | ||||
|
r43346 | sys.stderr.write( | ||
'%s:%d:%s(%s): %s\n' | ||||
% (f, pe.linenum, level, checker, error) | ||||
) | ||||
|
r20152 | sys.exit(1) | ||