]> git.donarmstrong.com Git - lilypond.git/blob - buildscripts/translations-status.py
Merge branch 'lilypond/translation' into dev/jmandereau
[lilypond.git] / buildscripts / translations-status.py
1 #!@PYTHON@
2
3 """
4 USAGE: translations-status.py BUILDSCRIPT-DIR LOCALEDIR
5
6   This script must be run from Documentation/
7
8   Reads template files translations.template.html
9 and for each LANG in LANGUAGES LANG/translations.template.html
10
11   Writes translations.html.in and for each LANG in LANGUAGES
12 translations.LANG.html.in
13 """
14
15 import sys
16 import re
17 import string
18 import os
19 import gettext
20 import subprocess
21
22 def progress (str):
23     sys.stderr.write (str + '\n')
24
25 progress ("translations-status.py")
26
27 buildscript_dir = sys.argv[1]
28 localedir = sys.argv[2]
29
30 _doc = lambda s: s
31
32 sys.path.append (buildscript_dir)
33 import langdefs
34
35 # load gettext messages catalogs
36 translation = {}
37 for l in langdefs.LANGUAGES:
38     if l.enabled and l.code != 'en':
39         translation[l.code] = gettext.translation('lilypond-doc', localedir, [l.code]).gettext
40
41 def read_pipe (command):
42     child = subprocess.Popen (command,
43                               stdout = subprocess.PIPE,
44                               stderr = subprocess.PIPE,
45                               shell = True)
46     (output, error) = child.communicate ()
47     code = str (child.wait ())
48     if not child.stdout or child.stdout.close ():
49         print "pipe failed: %(command)s" % locals ()
50     if code != '0':
51         error = code + ' ' + error
52     return (output, error)
53
54 comments_re = re.compile (r'^@ignore\n(.|\n)*?\n@end ignore$|@c .*?$', re.M)
55 space_re = re.compile (r'\s+', re.M)
56 lilypond_re = re.compile (r'@lilypond({.*?}|(.|\n)*?\n@end lilypond$)', re.M)
57 node_re = re.compile ('^@node .*?$', re.M)
58 title_re = re.compile ('^@(top|chapter|(?:sub){0,2}section|(?:unnumbered|appendix)(?:(?:sub){0,2}sec)?) (.*?)$', re.M)
59 include_re = re.compile ('^@include (.*?)$', re.M)
60
61 committish_re = re.compile ('GIT [Cc]ommittish: ([a-f0-9]+)')
62 translators_re = re.compile (r'^@c\s+Translators\s*:\s*(.*?)$', re.M | re.I)
63 checkers_re = re.compile (r'^@c\s+Translation\s*checkers\s*:\s*(.*?)$', re.M | re.I)
64 status_re = re.compile (r'^@c\s+Translation\s*status\s*:\s*(.*?)$', re.M | re.I)
65 post_gdp_re = re.compile ('post.GDP', re.I)
66 untranslated_node_str = 'UNTRANSLATED NODE: IGNORE ME'
67 skeleton_str = '-- SKELETON FILE --'
68
69 diff_cmd = 'git diff --no-color %(committish)s HEAD -- %(original)s | cat'
70
71 format_table = {
72     'not translated': {'color':'d0f0f8', 'short':_doc ('no'), 'long':_doc ('not translated')},
73     'partially translated': {'color':'dfef77', 'short':_doc ('partially (%(p)d %%)'),
74                              'long':_doc ('partially translated (%(p)d %%)')},
75     'fully translated': {'color':'1fff1f', 'short':_doc ('yes'), 'long': _doc ('translated')},
76     'up to date': {'short':_doc ('yes'), 'long':_doc ('up to date')},
77     'outdated': {'short':_doc ('partially (%(p)d %%)'), 'long':_doc ('partially up-to-date (%(p)d %%)')},
78     'N/A': {'short':_doc ('N/A'), 'long':'', 'color':'d587ff' },
79     'pre-GDP':_doc ('pre-GDP'),
80     'post-GDP':_doc ('post-GDP')
81 }
82
83 texi_level = {
84 # (Unumbered/Numbered/Lettered, level)
85     'top': ('u', 0),
86     'unnumbered': ('u', 1),
87     'unnumberedsec': ('u', 2),
88     'unnumberedsubsec': ('u', 3),
89     'chapter': ('n', 1),
90     'section': ('n', 2),
91     'subsection': ('n', 3),
92     'appendix': ('l', 1)
93 }
94
95 appendix_number_trans = string.maketrans ('@ABCDEFGHIJKLMNOPQRSTUVWXY','ABCDEFGHIJKLMNOPQRSTUVWXYZ')
96
97 class SectionNumber (object):
98     def __init__ (self):
99         self.__data = [[0,'u']]
100
101     def __increase_last_index (self):
102         type = self.__data[-1][1]
103         if type == 'l':
104             self.__data[-1][0] = self.__data[-1][0].translate (appendix_number_trans)
105         elif type == 'n':
106             self.__data[-1][0] += 1
107
108     def format (self):
109         if self.__data[-1][1] == 'u':
110             return ''
111         return '.'.join ([str (i[0]) for i in self.__data if i[1] != 'u']) + ' '
112
113     def increase (self, (type, level)):
114         if level == 0:
115             self.__data = [[0,'u']]
116         while level + 1 < len (self.__data):
117             del self.__data[-1]
118         if level + 1 > len (self.__data):
119             self.__data.append ([0, type])
120             if type == 'l':
121                 self.__data[-1][0] = '@'
122         if type == self.__data[-1][1]:
123             self.__increase_last_index ()
124         else:
125             self.__data[-1] = ([0, type])
126             if type == 'l':
127                 self.__data[-1][0] = 'A'
128             elif type == 'n':
129                 self.__data[-1][0] = 1
130         return self.format ()
131
132
133 def percentage_color (percent):
134     p = percent / 100.0
135     if p < 0.33:
136         c = [hex (int (3 * p * b + (1 - 3 * p) * a))[2:] for (a, b) in [(0xff, 0xff), (0x5c, 0xa6), (0x5c, 0x4c)]]
137     elif p < 0.67:
138         c = [hex (int ((3 * p - 1) * b + (2 - 3 * p) * a))[2:] for (a, b) in [(0xff, 0xff), (0xa6, 0xff), (0x4c, 0x3d)]]
139     else:
140         c = [hex (int ((3 * p - 2) * b + 3 * (1 - p) * a))[2:] for (a, b) in [(0xff, 0x1f), (0xff, 0xff), (0x3d, 0x1f)]]
141     return ''.join (c)
142
143 def tely_word_count (tely_doc):
144     '''
145     Calculate word count of a Texinfo document node by node.
146
147     Take string tely_doc as an argument.
148     Return a list of integers.
149
150     Texinfo comments and @lilypond blocks are not included in word counts.
151     '''
152     tely_doc = comments_re.sub ('', tely_doc)
153     tely_doc = lilypond_re.sub ('', tely_doc)
154     nodes = node_re.split (tely_doc)
155     return [len (space_re.split (n)) for n in nodes]
156
157
158 class TelyDocument (object):
159     def __init__ (self, filename):
160         self.filename = filename
161         self.contents = open (filename).read ()
162
163         ## record title and sectionning level of first Texinfo section
164         m = title_re.search (self.contents)
165         if m:
166             self.title = m.group (2)
167             self.level = texi_level [m.group (1)]
168         else:
169             self.title = 'Untitled'
170             self.level = ('u', 1)
171
172         included_files = [os.path.join (os.path.dirname (filename), t) for t in include_re.findall (self.contents)]
173         self.included_files = [p for p in included_files if os.path.exists (p)]
174
175     def print_title (self, section_number):
176         return section_number.increase (self.level) + self.title
177
178
179 class TranslatedTelyDocument (TelyDocument):
180     def __init__ (self, filename, masterdocument, parent_translation=None):
181         TelyDocument.__init__ (self, filename)
182
183         self.masterdocument = masterdocument
184
185         ## record authoring information
186         m = translators_re.search (self.contents)
187         if m:
188             self.translators = [n.strip () for n in m.group (1).split (',')]
189         else:
190             self.translators = parent_translation.translators
191         m = checkers_re.search (self.contents)
192         if m:
193             self.checkers = [n.strip () for n in m.group (1).split (',')]
194         elif isinstance (parent_translation, TranslatedTelyDocument):
195             self.checkers = parent_translation.checkers
196         else:
197             self.checkers = []
198
199         ## check whether translation is pre- or post-GDP
200         m = status_re.search (self.contents)
201         if m:
202             self.post_gdp = bool (post_gdp_re.search (m.group (1)))
203         else:
204             self.post_gdp = False
205
206         ## record which parts (nodes) of the file are actually translated
207         self.partially_translated = not skeleton_str in self.contents
208         nodes = node_re.split (self.contents)
209         self.translated_nodes = [not untranslated_node_str in n for n in nodes]
210
211         ## calculate translation percentage
212         master_total_word_count = sum (masterdocument.word_count)
213         translation_word_count = sum ([masterdocument.word_count[k] * self.translated_nodes[k]
214                                        for k in range (min (len (masterdocument.word_count), len (self.translated_nodes)))])
215         self.translation_percentage = 100 * translation_word_count / master_total_word_count
216
217         ## calculate how much the file is outdated
218         m = committish_re.search (self.contents)
219         if not m:
220             sys.stderr.write ('error: ' + filename + \
221                                   ": no 'GIT committish: <hash>' found.\nPlease check " + \
222                                   'the whole file against the original in English, then ' + \
223                                   'fill in HEAD committish in the header.\n')
224             sys.exit (1)
225         (diff_string, error) = read_pipe (diff_cmd % {'committish':m.group (1), 'original':masterdocument.filename})
226         if error:
227             sys.stderr.write ('warning: %s: %s' % (self.filename, error))
228             self.uptodate_percentage = None
229         else:
230             diff = diff_string.splitlines ()
231             insertions = sum ([len (l) - 1 for l in diff if l.startswith ('+') and not l.startswith ('+++')])
232             deletions = sum ([len (l) - 1 for l in diff if l.startswith ('-') and not l.startswith ('---')])
233             outdateness_percentage = 50.0 * (deletions + insertions) / (masterdocument.size + 0.5 * (deletions - insertions))
234             self.uptodate_percentage = 100 - int (outdateness_percentage)
235             if self.uptodate_percentage > 100:
236                 alternative = 50
237                 progress ("%s: strange uptodateness percentage %d %%, setting to %d %%" \
238                               % (self.filename, self.uptodate_percentage, alternative))
239                 self.uptodate_percentage = alternative
240             elif self.uptodate_percentage < 1:
241                 alternative = 1
242                 progress ("%s: strange uptodateness percentage %d %%, setting to %d %%" \
243                               % (self.filename, self.uptodate_percentage, alternative))
244                 self.uptodate_percentage = alternative
245
246     def completeness (self, formats=['long']):
247         if isinstance (formats, str):
248             formats = [formats]
249         p = self.translation_percentage
250         if p == 0:
251             status = 'not translated'
252         elif p == 100:
253             status = 'fully translated'
254         else:
255             status = 'partially translated'
256         return dict ([(f, format_table[status][f] % locals()) for f in formats])
257
258     def uptodateness (self, formats=['long']):
259         if isinstance (formats, str):
260             formats = [formats]
261         p = self.uptodate_percentage
262         if p == None:
263             status = 'N/A'
264         elif p == 100:
265             status = 'up to date'
266         else:
267             status = 'outdated'
268         l = {}
269         for f in formats:
270             if f == 'color' and p != None:
271                 l['color'] = percentage_color (p)
272             else:
273                 l[f] = format_table[status][f] % locals ()
274         return l
275
276     def gdp_status (self, translation=lambda s: s):
277         if self.post_gdp:
278             return translation (format-table['post-GDP'])
279         else:
280             return translation (format-table['pre-GDP'])
281
282     def short_html_status (self):
283         s = '  <td>'
284         if self.partially_translated:
285             s += '<br>\n   '.join (self.translators) + '<br>\n'
286             if self.checkers:
287                 s += '   <small>' + '<br>\n   '.join (self.checkers) + '</small><br>\n'
288
289         c = self.completeness (['long', 'color'])
290         s += '   <span style="background-color: #%(color)s">%(long)s</span><br>\n' % c
291
292         if self.partially_translated:
293             u = self.uptodateness (['long', 'color'])
294             s += '   <span style="background-color: #%(color)s">%(long)s</span><br>\n' % u
295
296         s += '  </td>\n'
297         return s
298
299     def html_status (self):
300         # TODO
301         return ''
302
303 class MasterTelyDocument (TelyDocument):
304     def __init__ (self, filename, parent_translations=dict ([(lang, None) for lang in langdefs.LANGDICT.keys()])):
305         #print "init MasterTelyDocument %s" % filename
306         TelyDocument.__init__ (self, filename)
307         self.size = len (self.contents)
308         self.word_count = tely_word_count (self.contents)
309         translations = dict ([(lang, os.path.join (lang, filename)) for lang in langdefs.LANGDICT.keys()])
310         #print translations
311         self.translations = dict ([(lang, TranslatedTelyDocument (translations[lang], self, parent_translations.get (lang)))
312                                    for lang in langdefs.LANGDICT.keys() if os.path.exists (translations[lang])])
313         if self.translations:
314             self.includes = [MasterTelyDocument (f, self.translations) for f in self.included_files]
315         else:
316             self.includes = []
317
318     # TODO
319     def print_wc_priority (self):
320         return
321
322     def html_status (self, numbering=SectionNumber ()):
323         if self.title == 'Untitled' or not self.translations:
324             return ''
325         if self.level[1] == 0: # if self is a master document
326             s = '''<table align="center" border="2">
327  <tr align="center">
328   <th>%s</th>''' % self.print_title (numbering)
329             s += ''.join (['  <th>%s</th>\n' % l for l in self.translations.keys ()])
330             s += ' </tr>\n'
331             s += ' <tr align="left">\n  <td>Section titles<br>(%d)</td>\n' \
332                 % sum (self.word_count)
333
334         else:
335             s = ' <tr align="left">\n  <td>%s<br>(%d)</td>\n' \
336                 % (self.print_title (numbering), sum (self.word_count))
337
338         s += ''.join ([t.short_html_status () for t in self.translations.values ()])
339         s += ' </tr>\n'
340         s += ''.join ([i.html_status (numbering) for i in self.includes])
341
342         if self.level[1] == 0:
343             s += '</table>\n<p></p>\n'
344         return s
345
346 progress ("Reading documents...")
347
348 tely_files = read_pipe ("find -maxdepth 2 -name '*.tely'")[0].splitlines ()
349 master_docs = [MasterTelyDocument (os.path.normpath (filename)) for filename in tely_files]
350 master_docs = [doc for doc in master_docs if doc.translations]
351
352 main_status_page = open ('translations.template.html').read ()
353
354 ## TODO
355 #per_lang_status_pages = dict ([(l, open (os.path.join (l, 'translations.template.html')). read ())
356 #                               for l in langdefs.LANGDICT.keys ()
357 #                               if langdefs.LANGDICT[l].enabled])
358
359 progress ("Generating status pages...")
360
361 main_status_html = ' <p><i>Last updated %s</i></p>\n' % read_pipe ('LANG= date -u')[0]
362 main_status_html += '\n'.join ([doc.html_status () for doc in master_docs])
363
364 html_re = re.compile ('<html>', re.I)
365 end_body_re = re.compile ('</body>', re.I)
366
367 main_status_page = html_re.sub ('''<html>
368 <!-- This page is automatically generated by translation-status.py from
369 translations.template.html; DO NOT EDIT !-->''', main_status_page)
370
371 main_status_page = end_body_re.sub (main_status_html + '\n</body>', main_status_page)
372
373 open ('translations.html.in', 'w').write (main_status_page)