Edouard@3112: #!/usr/bin/env python Edouard@3112: # -*- coding: utf-8 -*- Edouard@3112: Edouard@3112: # This file is part of Beremiz Edouard@3112: # Copyright (C) 2021: Edouard TISSERANT Edouard@3112: # Edouard@3112: # See COPYING file for copyrights details. Edouard@3112: Edouard@3112: from __future__ import absolute_import Edouard@3115: from lxml import etree edouard@3113: import os Edouard@3112: import sys Edouard@3112: import subprocess edouard@3108: import time edouard@3113: import ast Edouard@3112: import wx Edouard@3157: import re Edouard@3112: edouard@3133: # to have it for python 2, had to install edouard@3133: # https://pypi.org/project/pycountry/18.12.8/ edouard@3133: # python2 -m pip install pycountry==18.12.8 --user edouard@3133: import pycountry edouard@3133: Edouard@3157: cmd_parser = re.compile(r'(?:"([^"]+)"\s*|([^\s]+)\s*)?') Edouard@3157: Edouard@3112: def open_pofile(pofile): Edouard@3112: """ Opens PO file with POEdit """ Edouard@3112: Edouard@3112: if sys.platform.startswith('win'): Edouard@3112: from six.moves import winreg Edouard@3112: poedit_cmd = None Edouard@3112: try: Edouard@3112: poedit_cmd = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, Edouard@3112: 'SOFTWARE\\Classes\\poedit\\shell\\open\\command') Edouard@3157: cmd = re.findall(cmd_parser, poedit_cmd) Edouard@3157: dblquote_value,smpl_value = cmd[0] Edouard@3157: poedit_path = dblquote_value+smpl_value Edouard@3112: except OSError: Edouard@3112: poedit_path = None Edouard@3112: Edouard@3112: else: Edouard@3112: try: Edouard@3112: poedit_path = subprocess.check_output("command -v poedit", shell=True).strip() Edouard@3112: except subprocess.CalledProcessError: Edouard@3112: poedit_path = None Edouard@3112: Edouard@3112: if poedit_path is None: Edouard@3112: wx.MessageBox("POEdit is not found or installed !") Edouard@3112: else: Edouard@3112: subprocess.Popen([poedit_path,pofile]) edouard@3108: edouard@3113: def EtreeToMessages(msgs): edouard@3113: """ Converts XML tree from 'extract_i18n' templates into a list of tuples """ edouard@3113: messages = [] edouard@3113: edouard@3113: for msg in msgs: edouard@3113: messages.append(( Edouard@3115: "\n".join([line.text for line in msg]), edouard@3113: msg.get("label"), msg.get("id"))) edouard@3113: edouard@3113: return messages edouard@3113: edouard@3113: def SaveCatalog(fname, messages): edouard@3113: """ Save messages given as list of tupple (msg,label,id) in POT file """ edouard@3113: w = POTWriter() edouard@3113: w.ImportMessages(messages) edouard@3113: edouard@3113: with open(fname, 'w') as POT_file: edouard@3113: w.write(POT_file) Edouard@3114: edouard@3214: def GetPoFiles(dirpath): edouard@3214: po_files = [fname for fname in os.listdir(dirpath) if fname.endswith(".po")] edouard@3214: po_files.sort() Edouard@3218: return [(po_fname[:-3],os.path.join(dirpath, po_fname)) for po_fname in po_files] edouard@3214: edouard@3113: def ReadTranslations(dirpath): edouard@3144: """ Read all PO files from a directory and return a list of (langcode, translation_dict) tuples """ edouard@3113: edouard@3113: translations = [] Edouard@3218: for translation_name, po_path in GetPoFiles(dirpath): edouard@3113: r = POReader() Edouard@3218: with open(po_path, 'r') as PO_file: edouard@3113: r.read(PO_file) Edouard@3218: translations.append((translation_name, r.get_messages())) edouard@3113: return translations edouard@3113: edouard@3113: def MatchTranslations(translations, messages, errcallback): Edouard@3114: """ Edouard@3114: Matches translations against original message catalog, Edouard@3114: warn about inconsistancies, Edouard@3114: returns list of langs, and a list of (msgid, [translations]) tuples edouard@3113: """ edouard@3113: translated_messages = [] Edouard@3114: broken_lang = set() edouard@3113: for msgid,label,svgid in messages: edouard@3113: translated_message = [] edouard@3144: for langcode,translation in translations: edouard@3113: msg = translation.pop(msgid, None) edouard@3113: if msg is None: edouard@3144: broken_lang.add(langcode) Edouard@3198: errcallback(_('{}: Missing translation for "{}" (label:{}, id:{})\n').format(langcode,msgid,label,svgid)) Edouard@3115: translated_message.append(msgid) Edouard@3115: else: Edouard@3115: translated_message.append(msg) edouard@3113: translated_messages.append((msgid,translated_message)) edouard@3113: langs = [] edouard@3144: for langcode,translation in translations: edouard@3133: try: edouard@3144: l,c = langcode.split("_") edouard@3133: language_name = pycountry.languages.get(alpha_2 = l).name edouard@3133: country_name = pycountry.countries.get(alpha_2 = c).name edouard@3144: langname = "{} ({})".format(language_name, country_name) edouard@3133: except: edouard@3133: try: edouard@3144: langname = pycountry.languages.get(alpha_2 = langcode).name edouard@3133: except: edouard@3144: langname = langcode edouard@3144: edouard@3144: langs.append((langname,langcode)) edouard@3133: Edouard@3114: broken = False edouard@3113: for msgid, msg in translation.iteritems(): Edouard@3115: broken = True edouard@3144: errcallback(_('{}: Unused translation "{}":"{}"\n').format(langcode,msgid,msg)) edouard@3144: if broken or langcode in broken_lang: Edouard@3198: errcallback(_('Translation for {} is outdated, please edit {}.po, click "Catalog -> Update from POT File..." and select messages.pot.\n').format(langcode,langcode)) Edouard@3114: edouard@3113: edouard@3113: return langs,translated_messages edouard@3113: Edouard@3114: edouard@3113: def TranslationToEtree(langs,translated_messages): Edouard@3115: Edouard@3116: result = etree.Element("translations") Edouard@3116: Edouard@3116: langsroot = etree.SubElement(result, "langs") edouard@3144: for name, code in langs: edouard@3144: langel = etree.SubElement(langsroot, "lang", {"code":code}) edouard@3144: langel.text = name Edouard@3115: Edouard@3116: msgsroot = etree.SubElement(result, "messages") Edouard@3115: for msgid, msgs in translated_messages: Edouard@3115: msgidel = etree.SubElement(msgsroot, "msgid") Edouard@3115: for msg in msgs: Edouard@3115: msgel = etree.SubElement(msgidel, "msg") Edouard@3116: for line in msg.split("\n"): Edouard@3116: lineel = etree.SubElement(msgel, "line") Edouard@3116: lineel.text = escape(line.encode("utf-8")).decode("utf-8") Edouard@3116: Edouard@3116: return result Edouard@3115: Edouard@3114: edouard@3113: edouard@3108: locpfx = '#:svghmi.svg:' edouard@3108: edouard@3108: pot_header = '''\ edouard@3108: # SOME DESCRIPTIVE TITLE. edouard@3108: # Copyright (C) YEAR ORGANIZATION edouard@3108: # FIRST AUTHOR , YEAR. edouard@3108: # edouard@3108: msgid "" edouard@3108: msgstr "" edouard@3108: "Project-Id-Version: PACKAGE VERSION\\n" edouard@3108: "POT-Creation-Date: %(time)s\\n" edouard@3108: "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n" edouard@3108: "Last-Translator: FULL NAME \\n" edouard@3108: "Language-Team: LANGUAGE \\n" edouard@3108: "MIME-Version: 1.0\\n" edouard@3113: "Content-Type: text/plain; charset=UTF-8\\n" edouard@3113: "Content-Transfer-Encoding: 8bit\\n" edouard@3108: "Generated-By: SVGHMI 1.0\\n" edouard@3108: edouard@3108: ''' Edouard@3112: escapes = [] Edouard@3112: Edouard@3112: def make_escapes(pass_iso8859): Edouard@3112: global escapes Edouard@3112: escapes = [chr(i) for i in range(256)] Edouard@3112: if pass_iso8859: Edouard@3112: # Allow iso-8859 characters to pass through so that e.g. 'msgid Edouard@3112: # "Höhe"' would result not result in 'msgid "H\366he"'. Otherwise we Edouard@3112: # escape any character outside the 32..126 range. Edouard@3112: mod = 128 Edouard@3112: else: Edouard@3112: mod = 256 Edouard@3112: for i in range(mod): Edouard@3112: if not(32 <= i <= 126): Edouard@3112: escapes[i] = "\\%03o" % i Edouard@3112: escapes[ord('\\')] = '\\\\' Edouard@3112: escapes[ord('\t')] = '\\t' Edouard@3112: escapes[ord('\r')] = '\\r' Edouard@3112: escapes[ord('\n')] = '\\n' Edouard@3112: escapes[ord('\"')] = '\\"' Edouard@3112: Edouard@3112: make_escapes(pass_iso8859 = True) Edouard@3112: Edouard@3112: EMPTYSTRING = '' Edouard@3112: Edouard@3112: def escape(s): Edouard@3112: global escapes Edouard@3112: s = list(s) Edouard@3112: for i in range(len(s)): Edouard@3112: s[i] = escapes[ord(s[i])] Edouard@3112: return EMPTYSTRING.join(s) Edouard@3112: Edouard@3112: def normalize(s): Edouard@3112: # This converts the various Python string types into a format that is Edouard@3112: # appropriate for .po files, namely much closer to C style. Edouard@3112: lines = s.split('\n') Edouard@3112: if len(lines) == 1: Edouard@3112: s = '"' + escape(s) + '"' Edouard@3112: else: Edouard@3112: if not lines[-1]: Edouard@3112: del lines[-1] Edouard@3112: lines[-1] = lines[-1] + '\n' Edouard@3112: for i in range(len(lines)): Edouard@3112: lines[i] = escape(lines[i]) Edouard@3112: lineterm = '\\n"\n"' Edouard@3112: s = '""\n"' + lineterm.join(lines) + '"' Edouard@3112: return s Edouard@3112: edouard@3108: edouard@3108: class POTWriter: edouard@3108: def __init__(self): edouard@3108: self.__messages = {} edouard@3108: Edouard@3112: def ImportMessages(self, msgs): Edouard@3115: for msg, label, svgid in msgs: Edouard@3115: self.addentry(msg.encode("utf-8"), label, svgid) edouard@3108: edouard@3108: def addentry(self, msg, label, svgid): edouard@3108: entry = (label, svgid) edouard@3108: self.__messages.setdefault(msg, set()).add(entry) edouard@3108: edouard@3108: def write(self, fp): edouard@3108: timestamp = time.strftime('%Y-%m-%d %H:%M+%Z') edouard@3108: print >> fp, pot_header % {'time': timestamp} edouard@3108: reverse = {} edouard@3108: for k, v in self.__messages.items(): edouard@3108: keys = list(v) edouard@3108: keys.sort() edouard@3108: reverse.setdefault(tuple(keys), []).append((k, v)) edouard@3108: rkeys = reverse.keys() edouard@3108: rkeys.sort() edouard@3108: for rkey in rkeys: edouard@3108: rentries = reverse[rkey] edouard@3108: rentries.sort() edouard@3108: for k, v in rentries: Edouard@3112: v = list(v) edouard@3108: v.sort() edouard@3108: locline = locpfx edouard@3108: for label, svgid in v: edouard@3108: d = {'label': label, 'svgid': svgid} Edouard@3112: s = _(' %(label)s:%(svgid)s') % d edouard@3108: if len(locline) + len(s) <= 78: edouard@3108: locline = locline + s edouard@3108: else: edouard@3108: print >> fp, locline edouard@3108: locline = locpfx + s edouard@3108: if len(locline) > len(locpfx): edouard@3108: print >> fp, locline edouard@3108: print >> fp, 'msgid', normalize(k) edouard@3108: print >> fp, 'msgstr ""\n' edouard@3108: edouard@3108: edouard@3108: class POReader: edouard@3108: def __init__(self): edouard@3108: self.__messages = {} edouard@3108: edouard@3113: def get_messages(self): edouard@3113: return self.__messages edouard@3113: edouard@3113: def add(self, msgid, msgstr, fuzzy): edouard@3108: "Add a non-fuzzy translation to the dictionary." Edouard@3115: if not fuzzy and msgstr and msgid: Edouard@3115: self.__messages[msgid.decode('utf-8')] = msgstr.decode('utf-8') edouard@3108: edouard@3108: def read(self, fp): edouard@3108: ID = 1 edouard@3108: STR = 2 edouard@3108: edouard@3108: lines = fp.readlines() edouard@3108: section = None edouard@3108: fuzzy = 0 edouard@3108: edouard@3108: # Parse the catalog edouard@3108: lno = 0 edouard@3108: for l in lines: edouard@3108: lno += 1 edouard@3108: # If we get a comment line after a msgstr, this is a new entry edouard@3108: if l[0] == '#' and section == STR: edouard@3108: self.add(msgid, msgstr, fuzzy) edouard@3108: section = None edouard@3108: fuzzy = 0 edouard@3108: # Record a fuzzy mark edouard@3108: if l[:2] == '#,' and 'fuzzy' in l: edouard@3108: fuzzy = 1 edouard@3108: # Skip comments edouard@3108: if l[0] == '#': edouard@3108: continue edouard@3108: # Now we are in a msgid section, output previous section edouard@3108: if l.startswith('msgid') and not l.startswith('msgid_plural'): edouard@3108: if section == STR: edouard@3108: self.add(msgid, msgstr, fuzzy) edouard@3108: section = ID edouard@3108: l = l[5:] edouard@3108: msgid = msgstr = '' edouard@3108: is_plural = False edouard@3108: # This is a message with plural forms edouard@3108: elif l.startswith('msgid_plural'): edouard@3108: if section != ID: edouard@3108: print >> sys.stderr, 'msgid_plural not preceded by msgid on %s:%d' %\ edouard@3108: (infile, lno) edouard@3108: sys.exit(1) edouard@3108: l = l[12:] edouard@3108: msgid += '\0' # separator of singular and plural edouard@3108: is_plural = True edouard@3108: # Now we are in a msgstr section edouard@3108: elif l.startswith('msgstr'): edouard@3108: section = STR edouard@3108: if l.startswith('msgstr['): edouard@3108: if not is_plural: edouard@3108: print >> sys.stderr, 'plural without msgid_plural on %s:%d' %\ edouard@3108: (infile, lno) edouard@3108: sys.exit(1) edouard@3108: l = l.split(']', 1)[1] edouard@3108: if msgstr: edouard@3108: msgstr += '\0' # Separator of the various plural forms edouard@3108: else: edouard@3108: if is_plural: edouard@3108: print >> sys.stderr, 'indexed msgstr required for plural on %s:%d' %\ edouard@3108: (infile, lno) edouard@3108: sys.exit(1) edouard@3108: l = l[6:] edouard@3108: # Skip empty lines edouard@3108: l = l.strip() edouard@3108: if not l: edouard@3108: continue edouard@3108: l = ast.literal_eval(l) edouard@3108: if section == ID: edouard@3108: msgid += l edouard@3108: elif section == STR: edouard@3108: msgstr += l edouard@3108: else: edouard@3108: print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \ edouard@3108: 'before:' edouard@3108: print >> sys.stderr, l edouard@3108: sys.exit(1) edouard@3108: # Add last entry edouard@3108: if section == STR: edouard@3108: self.add(msgid, msgstr, fuzzy) edouard@3108: edouard@3108: