Mercurial > hg-stable
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 &$$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) |