2 # -*- coding: utf-8 -*-;
6 # Copyright (c) 2009-2022 Tim Gerundt <tim@gerundt.de>
8 # Permission is hereby granted, free of charge, to any person obtaining a copy
9 # of this software and associated documentation files (the "Software"), to deal
10 # in the Software without restriction, including without limitation the rights
11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the Software is
13 # furnished to do so, subject to the following conditions:
15 # The above copyright notice and this permission notice shall be included in
16 # all copies or substantial portions of the Software.
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 # Python script to get the status of the translations
37 class TranslationsStatus(object):
47 ''' Return a list with all languages '''
49 for project in self._projects: #For all projects...
50 for language in project.languages: #For all languages...
51 if language not in temp: #If language NOT in list...
57 def noneTemplateLanguages(self):
58 ''' Return a list with all NONE template languages '''
60 for project in self._projects: #For all projects...
61 for language in project.noneTemplateLanguages: #For all NONE template languages...
62 if language not in temp: #If language NOT in list...
70 def addProject(self, project):
71 self._projects.append(project)
73 def writeToXmlFile(self, xmlpath):
74 xmlfile = codecs.open(xmlpath, 'w', 'utf-8')
75 xmlfile.write('<?xml version="1.0" encoding="UTF-8"?>\n')
76 xmlfile.write('<status>\n')
77 xmlfile.write(' <update>%s</update>\n' % (time.strftime('%Y-%m-%d')))
78 for project in self._projects: #For all projects...
79 xmlfile.write(' <translations project="%s">\n' % (project.name))
80 for language in project.languages: #For all (sorted) languages...
81 status1 = project[language]
82 if status1.template: #If a template file...
83 xmlfile.write(' <translation template="1">\n')
84 xmlfile.write(' <language>%s</language>\n' % (status1.language))
85 xmlfile.write(' <file>%s</file>\n' % (status1.filename))
86 xmlfile.write(' <update>%s</update>\n' % (status1.updatedate[0:10]))
87 xmlfile.write(' <strings>\n')
88 xmlfile.write(' <count>%u</count>\n' % (status1.count))
89 xmlfile.write(' <translated>%u</translated>\n' % (status1.count))
90 xmlfile.write(' <fuzzy>0</fuzzy>\n')
91 xmlfile.write(' <untranslated>0</untranslated>\n')
92 xmlfile.write(' </strings>\n')
93 xmlfile.write(' </translation>\n')
94 else: #If NOT a template file...
95 xmlfile.write(' <translation>\n')
96 xmlfile.write(' <language>%s</language>\n' % (status1.language))
97 xmlfile.write(' <file>%s</file>\n' % (status1.filename))
98 xmlfile.write(' <update>%s</update>\n' % (status1.updatedate[0:10]))
99 xmlfile.write(' <strings>\n')
100 xmlfile.write(' <count>%u</count>\n' % (status1.count))
101 xmlfile.write(' <translated>%u</translated>\n' % (status1.translated))
102 xmlfile.write(' <fuzzy>%u</fuzzy>\n' % (status1.fuzzy))
103 xmlfile.write(' <untranslated>%u</untranslated>\n' % (status1.untranslated))
104 xmlfile.write(' </strings>\n')
105 if status1.translators: #If translators exists...
106 xmlfile.write(' <translators>\n')
107 for translator in status1.translators: #For all translators...
108 if (translator.ismaintainer): #If maintainer...
109 xmlfile.write(' <translator maintainer="1">\n')
110 else: #If NOT maintainer...
111 xmlfile.write(' <translator>\n')
112 xmlfile.write(' <name>%s</name>\n' % (translator.name))
113 if (translator.mail): #If mail address exists...
114 xmlfile.write(' <mail>%s</mail>\n' % (translator.mail))
115 xmlfile.write(' </translator>\n')
116 xmlfile.write(' </translators>\n')
117 xmlfile.write(' </translation>\n')
118 xmlfile.write(' </translations>\n')
119 xmlfile.write('</status>\n')
122 def writeToHtmlFile(self, htmlpath):
123 htmlfile = codecs.open(htmlpath, 'w', 'utf-8')
125 htmlfile.write('<!DOCTYPE html>\n')
126 htmlfile.write('<html lang="en">\n')
127 htmlfile.write('<head>\n')
128 htmlfile.write(' <title>Translations Status</title>\n')
129 htmlfile.write(' <meta http-equiv="content-type" content="text/html; charset=UTF-8">\n')
130 htmlfile.write(' <style>\n')
131 htmlfile.write(' <!--\n')
132 htmlfile.write(' body {\n')
133 htmlfile.write(' font-family: Calibri,Helvetica,Arial,sans-serif;\n')
134 htmlfile.write(' }\n')
135 htmlfile.write(' h1, h2, h3, h4, h5, h6 {\n')
136 htmlfile.write(' font-family: Cambria,"Times New Roman",Times,serif;\n')
137 htmlfile.write(' }\n')
138 htmlfile.write(' .status {\n')
139 htmlfile.write(' border-collapse: collapse;\n')
140 htmlfile.write(' border: 1px solid #d2d2d2;\n')
141 htmlfile.write(' }\n')
142 htmlfile.write(' .status th, .status td {\n')
143 htmlfile.write(' padding: .3em;\n')
144 htmlfile.write(' border: 1px solid #d2d2d2;\n')
145 htmlfile.write(' }\n')
146 htmlfile.write(' .status th {\n')
147 htmlfile.write(' background: #f2f2f2;\n')
148 htmlfile.write(' }\n')
149 htmlfile.write(' .status tr:nth-child(odd) {\n')
150 htmlfile.write(' background: #f9f9f9;\n')
151 htmlfile.write(' }\n')
152 htmlfile.write(' .left { text-align: left; }\n')
153 htmlfile.write(' .center { text-align: center; }\n')
154 htmlfile.write(' .right { text-align: right; }\n')
156 htmlfile.write(' .translated { color: #2D802B; }\n')
157 htmlfile.write(' .fuzzy { color: #05359B; }\n')
158 htmlfile.write(' .untranslated { color: #D42323; }\n')
159 htmlfile.write(' -->\n')
160 htmlfile.write(' </style>\n')
161 htmlfile.write('</head>\n')
162 htmlfile.write('<body>\n')
163 htmlfile.write('<h1>Translations Status</h1>\n')
164 htmlfile.write('<p>Status from <strong>%s</strong>:</p>\n' % (time.strftime('%Y-%m-%d')))
165 for project in self._projects: #For all projects...
166 htmlfile.write('<h2>%s</h2>\n' % (project.name))
167 htmlfile.write('<table class="status">\n')
168 htmlfile.write(' <tr>\n')
169 htmlfile.write(' <th class="left">Language</th>\n')
170 htmlfile.write(' <th class="right">Total</th>\n')
171 htmlfile.write(' <th class="right translated">Translated</th>\n')
172 htmlfile.write(' <th class="right fuzzy">Fuzzy</th>\n')
173 htmlfile.write(' <th class="right untranslated">Untranslated</th>\n')
174 htmlfile.write(' <th class="right">Complete</th>\n')
175 htmlfile.write(' <th class="center">Last Update</th>\n')
176 htmlfile.write(' </tr>\n')
177 for language in project.languages: #For all (sorted) languages...
178 status1 = project[language]
179 htmlfile.write(' <tr>\n')
180 htmlfile.write(' <td class="left">%s</td>\n' % (status1.language))
181 if status1.template: #If a template file...
182 if status1.count > 0: #If KNOWN status...
183 htmlfile.write(' <td class="right">%u</td>\n' % (status1.count))
184 htmlfile.write(' <td class="right translated">%u</td>\n' % (status1.count))
185 htmlfile.write(' <td class="right fuzzy">0</td>\n')
186 htmlfile.write(' <td class="right untranslated">0</td>\n')
187 htmlfile.write(' <td class="right">100 %</td>\n')
188 else: #If UNKNOWN status...
189 htmlfile.write(' <td class="right">-</td>\n')
190 htmlfile.write(' <td class="right translated">-</td>\n')
191 htmlfile.write(' <td class="right fuzzy">-</td>\n')
192 htmlfile.write(' <td class="right untranslated">-</td>\n')
193 htmlfile.write(' <td class="right">-</td>\n')
194 htmlfile.write(' <td class="center">%s</td>\n' % (status1.updatedate[0:10]))
195 else: #If NOT a template file...
196 if status1.count > 0: #If KNOWN status...
197 htmlfile.write(' <td class="right">%u</td>\n' % (status1.count))
198 htmlfile.write(' <td class="right translated">%u</td>\n' % (status1.translated))
199 htmlfile.write(' <td class="right fuzzy">%u</td>\n' % (status1.fuzzy))
200 htmlfile.write(' <td class="right untranslated">%u</td>\n' % (status1.untranslated))
201 htmlfile.write(' <td class="right">%u %%</td>\n' % (status1.complete))
202 else: #If UNKNOWN status...
203 htmlfile.write(' <td class="right">-</td>\n')
204 htmlfile.write(' <td class="right translated">-</td>\n')
205 htmlfile.write(' <td class="right fuzzy">-</td>\n')
206 htmlfile.write(' <td class="right untranslated">-</td>\n')
207 htmlfile.write(' <td class="right">-</td>\n')
208 htmlfile.write(' <td class="center">%s</td>\n' % (status1.updatedate[0:10]))
209 htmlfile.write(' </tr>\n')
210 htmlfile.write('</table>\n')
213 htmlfile.write('<h2>Translators</h2>\n')
214 htmlfile.write('<table class="status">\n')
215 htmlfile.write(' <tr>\n')
216 htmlfile.write(' <th class="left">Language</th>\n')
217 for project in self._projects: #For all projects...
218 htmlfile.write(' <th class="left">%s</th>\n' % project.name)
219 htmlfile.write(' </tr>\n')
220 for language in self.noneTemplateLanguages: #For all NONE template languages...
221 htmlfile.write(' <tr>\n')
222 htmlfile.write(' <td>%s</td>\n' % language)
223 for project in self._projects: #For all projects...
224 status1 = project[language]
226 htmlfile.write(' <td>')
227 if status1.translators: #If translators exists...
228 for translator in status1.translators: #For all translators...
229 if (translator.ismaintainer): #If maintainer...
230 if (translator.mail): #If mail address exists...
231 htmlfile.write('<strong title="Maintainer"><a href="mailto:%s">%s</a></strong><br>' % (translator.mail, translator.name))
232 else: #If NO mail address exists...
233 htmlfile.write('<strong title="Maintainer">%s</strong><br>' % (translator.name))
234 else: #If NOT maintainer...
235 if (translator.mail): #If mail address exists...
236 htmlfile.write('<a href="mailto:%s">%s</a><br>' % (translator.mail, translator.name))
237 else: #If NO mail address exists...
238 htmlfile.write('%s<br>' % (translator.name))
239 htmlfile.write('</td>\n')
241 htmlfile.write(' <td></td>\n')
242 htmlfile.write(' </tr>\n')
243 htmlfile.write('</table>\n')
245 htmlfile.write('</body>\n')
246 htmlfile.write('</html>\n')
249 def writeToMdFile(self, mdpath):
250 mdfile = codecs.open(mdpath, 'w', 'utf-8')
252 mdfile.write('# Translations Status\n\n')
253 mdfile.write('Status from **%s**:\n\n' % (time.strftime('%Y-%m-%d')))
254 for project in self._projects: #For all projects...
255 mdfile.write('## %s\n\n' % (project.name))
256 mdfile.write('| Language | Total | Translated | Fuzzy | Untranslated | Complete | Last Update |\n')
257 mdfile.write('|:---------------------|------:|-----------:|------:|-------------:|---------:|:-----------:|\n')
258 for language in project.languages: #For all (sorted) languages...
259 status1 = project[language]
260 formatedlanguage = status1.language.ljust(20)
261 formatedupdatedate = status1.updatedate[0:10].center(11)
262 if status1.template: #If a template file...
263 if status1.count > 0: #If KNOWN status...
264 formatedcount = str(status1.count).rjust(5)
265 formatedtranslated = str(status1.count).rjust(10)
266 mdfile.write('| %s | %s | %s | 0 | 0 | 100 %% | %s |\n' % (formatedlanguage, formatedcount, formatedtranslated, formatedupdatedate))
267 else: #If UNKNOWN status...
268 mdfile.write('| %s | - | - | - | - | - | %s |\n' % (formatedlanguage, formatedupdatedate))
269 else: #If NOT a template file...
270 if status1.count > 0: #If KNOWN status...
271 formatedcount = str(status1.count).rjust(5)
272 formatedtranslated = str(status1.translated).rjust(10)
273 formatedfuzzy = str(status1.fuzzy).rjust(5)
274 formateduntranslated = str(status1.untranslated).rjust(12)
275 formatedcomplete = str(status1.complete).rjust(6)
276 mdfile.write('| %s | %s | %s | %s | %s | %s %% | %s |\n' % (formatedlanguage, formatedcount, formatedtranslated, formatedfuzzy, formateduntranslated, formatedcomplete, formatedupdatedate))
277 else: #If UNKNOWN status...
278 mdfile.write('| %s | - | - | - | - | - | %s |\n' % (formatedlanguage, formatedupdatedate))
282 mdfile.write('## Translators\n')
283 for project in self._projects: #For all projects...
284 mdfile.write('\n### %s\n' % project.name)
285 for language in self.noneTemplateLanguages: #For all NONE template languages...
286 status1 = project[language]
288 if status1.translators: #If translators exists...
289 mdfile.write('\n * %s\n' % language)
290 for translator in status1.translators: #For all translators...
291 if (translator.ismaintainer): #If maintainer...
292 if (translator.mail): #If mail address exists...
293 mdfile.write(' - [%s](mailto:%s) *Maintainer*\n' % (translator.name, translator.mail.replace(" ", "%20")))
294 else: #If NO mail address exists...
295 mdfile.write(' - %s *Maintainer*\n' % (translator.name))
296 else: #If NOT maintainer...
297 if (translator.mail): #If mail address exists...
298 mdfile.write(' - [%s](mailto:%s)\n' % (translator.name, translator.mail.replace(" ", "%20")))
299 else: #If NO mail address exists...
300 mdfile.write(' - %s\n' % (translator.name))
304 class Project(object):
305 def __getitem__(self, key):
306 for status in self._status: #For all status...
307 if status.language == key:
321 ''' Return a list with all languages '''
323 for status in self._status: #For all status...
324 temp.append(status.language)
329 def noneTemplateLanguages(self):
330 ''' Return a list with all NONE template languages '''
332 for status in self._status: #For all status...
333 if not status.template: #If NOT a template...
334 temp.append(status.language)
338 class Status(object):
341 return self._filepath
345 return os.path.basename(self._filepath)
349 return self._template
357 if self._poeditlanguage: #If "X-Poedit-Language"...
358 return self._poeditlanguage
359 else: #If NOT "X-Poedit-Language"...
360 return os.path.splitext(self.filename)[0]
367 def translated(self):
368 return self._translated
371 def untranslated(self):
372 return self._untranslated
380 return self._complete
383 def updatedate(self):
384 return self._updatedate
387 def translators(self):
388 return self._translators
390 def calculateCompleteness(self):
392 self._complete = math.floor(((self._translated + self._fuzzy) * 100 / self._count))
396 class Translator(object):
397 def __init__(self, name, mail, ismaintainer):
400 self.ismaintainer = ismaintainer
402 class PoProject(Project):
403 def __init__(self, name, potfile, podir):
408 for itemname in os.listdir(podir): #For all dir items...
409 fullitempath = os.path.abspath(os.path.join(podir, itemname))
410 if os.path.isfile(fullitempath): #If a file...
411 filename = os.path.splitext(itemname)
412 if str.lower(filename[1]) == '.po': #If a PO file...
413 self._status.append(PoStatus(fullitempath, False))
416 self._status.append(PoStatus(os.path.abspath(potfile), True))
418 class PoStatus(Status):
419 def __init__(self, filepath, template):
420 self._filepath = filepath
421 self._template = template
422 self._charset = self._getCharsetFromPoFile(filepath)
425 self._untranslated = 0
428 self._porevisiondate = ''
429 self._potcreationdate = ''
430 self._poeditlanguage = ''
431 self._translators = []
433 if self._charset == '': #If NO charset found...
436 if os.access(filepath, os.R_OK): #If PO(T) file can read...
437 reMsgId = re.compile('^msgid "(.*)"$', re.IGNORECASE)
438 reMsgStr = re.compile('^msgstr "(.*)"$', re.IGNORECASE)
439 reMsgContinued = re.compile('^"(.*)"$', re.IGNORECASE)
440 reTranslator = re.compile('^# \* (.*)$', re.IGNORECASE)
441 rePoRevisionDate = re.compile('PO-Revision-Date: ([0-9 :\+\-]+)', re.IGNORECASE)
442 rePotCreationDate = re.compile('POT-Creation-Date: ([0-9 :\+\-]+)', re.IGNORECASE)
443 rePoeditLanguage = re.compile('X-Poedit-Language: ([A-Z \(\)\-_]+)', re.IGNORECASE)
449 bIsMaintainer = False
451 encoding = self._charset.lower()
452 pofile = codecs.open(filepath, 'r', encoding)
453 for line in pofile: #For all lines...
455 if line: #If NOT empty line...
456 if line[0] != '#': #If NOT comment line...
457 if reMsgId.findall(line): #If "msgid"...
459 tmp = reMsgId.findall(line)
461 elif reMsgStr.findall(line): #If "msgstr"...
463 tmp = reMsgStr.findall(line)
465 elif reMsgContinued.findall(line): #If "msgid" or "msgstr" continued...
466 tmp = reMsgContinued.findall(line)
468 sMsgId = sMsgId + tmp[0]
469 elif iMsgStarted == 2:
470 sMsgStr = sMsgStr + tmp[0]
471 else: #If comment line...
473 if line.startswith('#,'): #If "Reference" line...
474 if line.find('fuzzy') > -1: #If "fuzzy"...
476 elif line.startswith('# Maintainer:'): #If maintainer list starts...
478 elif line.startswith('# Translators:'): #If translators list starts...
479 bIsMaintainer = False
480 elif reTranslator.findall(line): #If translator/maintainer...
481 translator = reTranslator.findall(line)
482 if re.findall('\<(.*)\>', translator[0]): #If mail address exists...
483 tmp = re.findall('(.*) \<(.*)\>', translator[0])
486 else: #If mail address NOT exists...
487 sName = translator[0]
489 self._translators.append(Translator(sName, sMail, bIsMaintainer))
490 else: #If empty line...
493 if iMsgStarted == 0: #If NOT inside a translation...
496 if bIsFuzzy == False: #If NOT a fuzzy translation...
498 self._translated += 1
500 self._untranslated += 1
501 else: #If a fuzzy translation...
504 tmp = rePoRevisionDate.findall(sMsgStr)
505 if tmp: #If "PO-Revision-Date"...
506 #TODO: Convert to date!
507 self._porevisiondate = tmp[0]
508 tmp = rePotCreationDate.findall(sMsgStr)
509 if tmp: #If "POT-Creation-Date"...
510 #TODO: Convert to date!
511 self._potcreationdate = tmp[0]
512 tmp = rePoeditLanguage.findall(sMsgStr)
513 if tmp: #If "X-Poedit-Language"...
514 self._poeditlanguage = tmp[0]
520 if sMsgId != '': #If a translation remained...
522 if bIsFuzzy == False: #If NOT a fuzzy translation...
524 self._translated += 1
526 self._untranslated += 1
527 else: #If a fuzzy translation...
530 self.calculateCompleteness()
533 def updatedate(self):
534 if self._template: #if template...
535 return self._potcreationdate
536 else: #if NOT template...
537 return self._porevisiondate
539 def _getCharsetFromPoFile(self, filepath):
541 if os.access(filepath, os.R_OK): #If PO(T) file can read...
542 reContentTypeCharset = re.compile('charset=([A-Z0-9\-]+)', re.IGNORECASE)
543 rePoeditSourceCharset = re.compile('X-Poedit-SourceCharset: ([A-Z0-9\-]+)', re.IGNORECASE)
545 pofile = open(filepath, 'r', errors='ignore')
547 for line in pofile: #For all lines...
550 tmp = reContentTypeCharset.findall(line)
551 if tmp: #If "Content-Type-Charset"...
554 tmp = rePoeditSourceCharset.findall(line)
555 if tmp: #If "X-Poedit-SourceCharset"...
561 class InnoSetupProject(Project):
562 def __init__(self, name, templatefile, translationsdir):
566 #Translations files...
567 for itemname in os.listdir(translationsdir): #For all dir items...
568 fullitempath = os.path.abspath(os.path.join(translationsdir, itemname))
569 if os.path.isfile(fullitempath): #If a file...
570 filename = os.path.splitext(itemname)
571 if str.lower(filename[1]) == '.isl': #If a ISL file...
572 if filename[0] != 'English': #If NOT the English file...
573 self._status.append(InnoSetupStatus(fullitempath, False))
576 self._status.append(InnoSetupStatus(os.path.abspath(templatefile), True))
578 class InnoSetupStatus(Status):
579 def __init__(self, filepath, template):
580 self._filepath = filepath
581 self._template = template
584 self._untranslated = 0
586 self._updatedate = ''
587 self._translators = []
591 if self._template: #if template...
593 else: #if NOT template...
594 filename = os.path.splitext(self.filename)
595 return filename[0].replace('_', '')
597 class ReadmeProject(Project):
598 def __init__(self, name, templatefile, translationsdir):
602 #Translations files...
603 for itemname in os.listdir(translationsdir): #For all dir items...
604 fullitempath = os.path.abspath(os.path.join(translationsdir, itemname))
605 if os.path.isfile(fullitempath): #If a file...
606 filename = os.path.splitext(itemname)
607 if str.lower(filename[1]) == '.txt': #If a TXT file...
608 self._status.append(ReadmeStatus(fullitempath, False))
611 self._status.append(ReadmeStatus(os.path.abspath(templatefile), True))
613 class ReadmeStatus(Status):
614 def __init__(self, filepath, template):
615 self._filepath = filepath
616 self._template = template
619 self._untranslated = 0
621 self._updatedate = ''
622 self._translators = []
626 if self._template: #if template...
628 else: #if NOT template...
629 filename = os.path.splitext(self.filename)
630 return filename[0].replace('ReadMe-', '')
633 parser = argparse.ArgumentParser()
634 parser.add_argument('-f', '--format', nargs='*', default='md', type=str.lower, choices=['xml', 'html', 'md'])
635 args = parser.parse_args()
637 status = TranslationsStatus()
638 status.addProject(PoProject('WinMerge', 'WinMerge/English.pot', 'WinMerge'))
639 status.addProject(PoProject('ShellExtension', 'ShellExtension/English.pot', 'ShellExtension'))
640 status.addProject(InnoSetupProject('InnoSetup', 'InnoSetup/English.isl', 'InnoSetup'))
641 status.addProject(ReadmeProject('Docs/Readme', 'Docs/ReadMe.txt', 'Docs/Readme'))
642 if 'xml' in args.format:
643 status.writeToXmlFile('TranslationsStatus.xml')
644 if 'html' in args.format:
645 status.writeToHtmlFile('TranslationsStatus.html')
646 if 'md' in args.format:
647 status.writeToMdFile('TranslationsStatus.md')
650 if __name__ == "__main__":