comparison i18n/check-translation.py @ 20152:84939b728749 stable

i18n: add the tool to check Mercurial specific translation problems in *.po Existing tool like "msgfmt --check" can check typical translation problems (missing "%s" in msgstr, for example), but can't check Mercurial specific ones. For example, "msgfmt --check" can't check whether the translated string given to "ui.promptchoice()" is correct or not, even though problems like below cause run-time error or unexpected behavior: - less or more choices than msgid, - choices without '&', or - choices with '&' followed by none This patch adds the tool to check Mercurial specific translation problems in *.po files.
author FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
date Wed, 27 Nov 2013 22:47:32 +0900
parents
children 209e04a06467
comparison
equal deleted inserted replaced
20151:734ff413eb7e 20152:84939b728749
1 #!/usr/bin/env python
2 #
3 # check-translation.py - check Mercurial specific translation problems
4
5 import polib
6 import re
7
8 checkers = []
9
10 def checker(level, msgidpat):
11 def decorator(func):
12 if msgidpat:
13 match = re.compile(msgidpat).search
14 else:
15 match = lambda msgid: True
16 checkers.append((func, level))
17 func.match = match
18 return func
19 return decorator
20
21 def match(checker, pe):
22 """Examine whether POEntry "pe" is target of specified checker or not
23 """
24 if not checker.match(pe.msgid):
25 return
26 # examine suppression by translator comment
27 nochecker = 'no-%s-check' % checker.__name__
28 for tc in pe.tcomment.split():
29 if nochecker == tc:
30 return
31 return True
32
33 ####################
34
35 def fatalchecker(msgidpat=None):
36 return checker('fatal', msgidpat)
37
38 @fatalchecker(r'\$\$')
39 def promptchoice(pe):
40 """Check translation of the string given to "ui.promptchoice()"
41
42 >>> pe = polib.POEntry(
43 ... msgid ='prompt$$missing &sep$$missing &amp$$followed by &none',
44 ... msgstr='prompt missing &sep$$missing amp$$followed by none&')
45 >>> match(promptchoice, pe)
46 True
47 >>> for e in promptchoice(pe): print e
48 number of choices differs between msgid and msgstr
49 msgstr has invalid choice missing '&'
50 msgstr has invalid '&' followed by none
51 """
52 idchoices = [c.rstrip(' ') for c in pe.msgid.split('$$')[1:]]
53 strchoices = [c.rstrip(' ') for c in pe.msgstr.split('$$')[1:]]
54
55 if len(idchoices) != len(strchoices):
56 yield "number of choices differs between msgid and msgstr"
57
58 indices = [(c, c.find('&')) for c in strchoices]
59 if [c for c, i in indices if i == -1]:
60 yield "msgstr has invalid choice missing '&'"
61 if [c for c, i in indices if len(c) == i + 1]:
62 yield "msgstr has invalid '&' followed by none"
63
64 ####################
65
66 def warningchecker(msgidpat=None):
67 return checker('warning', msgidpat)
68
69 ####################
70
71 def check(pofile, fatal=True, warning=False):
72 targetlevel = { 'fatal': fatal, 'warning': warning }
73 targetcheckers = [(checker, level)
74 for checker, level in checkers
75 if targetlevel[level]]
76 if not targetcheckers:
77 return []
78
79 detected = []
80 for pe in pofile.translated_entries():
81 errors = []
82 for checker, level in targetcheckers:
83 if match(checker, pe):
84 errors.extend((level, checker.__name__, error)
85 for error in checker(pe))
86 if errors:
87 detected.append((pe, errors))
88 return detected
89
90 ########################################
91
92 if __name__ == "__main__":
93 import sys
94 import optparse
95
96 optparser = optparse.OptionParser("""%prog [options] pofile ...
97
98 This checks Mercurial specific translation problems in specified
99 '*.po' files.
100
101 Each detected problems are shown in the format below::
102
103 filename:linenum:type(checker): problem detail .....
104
105 "type" is "fatal" or "warning". "checker" is the name of the function
106 detecting corresponded error.
107
108 Checking by checker "foo" on the specific msgstr can be suppressed by
109 the "translator comment" like below. Multiple "no-xxxx-check" should
110 be separated by whitespaces::
111
112 # no-foo-check
113 msgid = "....."
114 msgstr = "....."
115 """)
116 optparser.add_option("", "--warning",
117 help="show also warning level problems",
118 action="store_true")
119 optparser.add_option("", "--doctest",
120 help="run doctest of this tool, instead of check",
121 action="store_true")
122 (options, args) = optparser.parse_args()
123
124 if options.doctest:
125 import doctest
126 failures, tests = doctest.testmod()
127 sys.exit(failures and 1 or 0)
128
129 # replace polib._POFileParser to show linenum of problematic msgstr
130 class ExtPOFileParser(polib._POFileParser):
131 def process(self, symbol, linenum):
132 super(ExtPOFileParser, self).process(symbol, linenum)
133 if symbol == 'MS': # msgstr
134 self.current_entry.linenum = linenum
135 polib._POFileParser = ExtPOFileParser
136
137 detected = []
138 warning = options.warning
139 for f in args:
140 detected.extend((f, pe, errors)
141 for pe, errors in check(polib.pofile(f),
142 warning=warning))
143 if detected:
144 for f, pe, errors in detected:
145 for level, checker, error in errors:
146 sys.stderr.write('%s:%d:%s(%s): %s\n'
147 % (f, pe.linenum, level, checker, error))
148 sys.exit(1)