SVGHMI: Intermediate commit while implementing i18n. WIP. svghmi
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Fri, 15 Jan 2021 10:11:05 +0100
branchsvghmi
changeset 3108 079419e7228d
parent 3107 ee0704cc6dc8
child 3110 476bd870313d
SVGHMI: Intermediate commit while implementing i18n. WIP.
svghmi/gen_index_xhtml.ysl2
svghmi/i18n.py
svghmi/i18n.ysl2
svghmi/inline_svg.ysl2
svghmi/svghmi.py
--- a/svghmi/gen_index_xhtml.ysl2	Tue Jan 05 01:23:45 2021 +0100
+++ b/svghmi/gen_index_xhtml.ysl2	Fri Jan 15 10:11:05 2021 +0100
@@ -51,6 +51,8 @@
 
     include inline_svg.ysl2
 
+    include i18n.ysl2
+
     include widgets_common.ysl2
 
     include widget_*.ysl2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/i18n.py	Fri Jan 15 10:11:05 2021 +0100
@@ -0,0 +1,151 @@
+import time
+
+locpfx = '#:svghmi.svg:'
+
+pot_header = '''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"POT-Creation-Date: %(time)s\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL@li.org>\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=CHARSET\\n"
+"Content-Transfer-Encoding: ENCODING\\n"
+"Generated-By: SVGHMI 1.0\\n"
+
+'''
+
+class POTWriter:
+    def __init__(self):
+        self.__messages = {}
+
+    def ImportMessages(self, msgs):    
+        for msg in msgs:
+            self.addentry("\n".join([line.text for line in msg]), msg.get("label"), msg.get("id"))
+
+    def addentry(self, msg, label, svgid):
+        entry = (label, svgid)
+        self.__messages.setdefault(msg, set()).add(entry)
+
+    def write(self, fp):
+        timestamp = time.strftime('%Y-%m-%d %H:%M+%Z')
+        print >> fp, pot_header % {'time': timestamp}
+        reverse = {}
+        for k, v in self.__messages.items():
+            keys = list(v)
+            keys.sort()
+            reverse.setdefault(tuple(keys), []).append((k, v))
+        rkeys = reverse.keys()
+        rkeys.sort()
+        for rkey in rkeys:
+            rentries = reverse[rkey]
+            rentries.sort()
+            for k, v in rentries:
+                v = v.keys()
+                v.sort()
+                locline = locpfx
+                for label, svgid in v:
+                    d = {'label': label, 'svgid': svgid}
+                    s = _(' %(label)s:%(svgid)d') % d
+                    if len(locline) + len(s) <= 78:
+                        locline = locline + s
+                    else:
+                        print >> fp, locline
+                        locline = locpfx + s
+                if len(locline) > len(locpfx):
+                    print >> fp, locline
+                print >> fp, 'msgid', normalize(k)
+                print >> fp, 'msgstr ""\n'
+
+
+class POReader:
+    def __init__(self):
+        self.__messages = {}
+
+    def add(msgid, msgstr, fuzzy):
+        "Add a non-fuzzy translation to the dictionary."
+        if not fuzzy and msgstr:
+            self.__messages[msgid] = msgstr
+
+    def read(self, fp):
+        ID = 1
+        STR = 2
+
+        lines = fp.readlines()
+        section = None
+        fuzzy = 0
+
+        # Parse the catalog
+        lno = 0
+        for l in lines:
+            lno += 1
+            # If we get a comment line after a msgstr, this is a new entry
+            if l[0] == '#' and section == STR:
+                self.add(msgid, msgstr, fuzzy)
+                section = None
+                fuzzy = 0
+            # Record a fuzzy mark
+            if l[:2] == '#,' and 'fuzzy' in l:
+                fuzzy = 1
+            # Skip comments
+            if l[0] == '#':
+                continue
+            # Now we are in a msgid section, output previous section
+            if l.startswith('msgid') and not l.startswith('msgid_plural'):
+                if section == STR:
+                    self.add(msgid, msgstr, fuzzy)
+                section = ID
+                l = l[5:]
+                msgid = msgstr = ''
+                is_plural = False
+            # This is a message with plural forms
+            elif l.startswith('msgid_plural'):
+                if section != ID:
+                    print >> sys.stderr, 'msgid_plural not preceded by msgid on %s:%d' %\
+                        (infile, lno)
+                    sys.exit(1)
+                l = l[12:]
+                msgid += '\0' # separator of singular and plural
+                is_plural = True
+            # Now we are in a msgstr section
+            elif l.startswith('msgstr'):
+                section = STR
+                if l.startswith('msgstr['):
+                    if not is_plural:
+                        print >> sys.stderr, 'plural without msgid_plural on %s:%d' %\
+                            (infile, lno)
+                        sys.exit(1)
+                    l = l.split(']', 1)[1]
+                    if msgstr:
+                        msgstr += '\0' # Separator of the various plural forms
+                else:
+                    if is_plural:
+                        print >> sys.stderr, 'indexed msgstr required for plural on  %s:%d' %\
+                            (infile, lno)
+                        sys.exit(1)
+                    l = l[6:]
+            # Skip empty lines
+            l = l.strip()
+            if not l:
+                continue
+            l = ast.literal_eval(l)
+            if section == ID:
+                msgid += l
+            elif section == STR:
+                msgstr += l
+            else:
+                print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
+                      'before:'
+                print >> sys.stderr, l
+                sys.exit(1)
+        # Add last entry
+        if section == STR:
+            self.add(msgid, msgstr, fuzzy)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/i18n.ysl2	Fri Jan 15 10:11:05 2021 +0100
@@ -0,0 +1,32 @@
+// i18n.ysl2
+
+
+template "svg:tspan", mode="extract_i18n" {
+    if "string-length(.) > 0" line {
+        value ".";
+    }
+}
+
+template "svg:text", mode="extract_i18n" {
+    msg {
+        attrib "id" value "@id";
+        attrib "label" value "@inkscape:label";
+        apply "svg:*", mode="extract_i18n";
+    }
+}
+
+const "translatable_texts", "//svg:text[starts-with(@inkscape:label, '_')]";
+const "translatable_strings" apply "$translatable_texts", mode="extract_i18n";
+
+emit "preamble:i18n" {
+    const "translations", "ns:GetTranslations($translatable_strings)";
+    | var translations = {
+    foreach "$translations/*" {
+    |     "«local-name()»":{
+        /* TODO */
+    |     }`if "position()!=last()" > ,`
+    }
+    | };
+    |
+
+}
--- a/svghmi/inline_svg.ysl2	Tue Jan 05 01:23:45 2021 +0100
+++ b/svghmi/inline_svg.ysl2	Fri Jan 15 10:11:05 2021 +0100
@@ -37,6 +37,11 @@
     error > All units must be set to "px" in Inkscape's document properties
 }
 
+// remove i18n markers, so that defs_by_labels can find text elements
+svgtmpl "svg:text/@inkscape:label[starts-with(., '_')]", mode="inline_svg" {
+    attrib "{name()}" > «substring(., 2)»
+}
+
 ////// Clone unlinking
 //
 // svg:use (inkscape's clones) inside a widgets are
--- a/svghmi/svghmi.py	Tue Jan 05 01:23:45 2021 +0100
+++ b/svghmi/svghmi.py	Fri Jan 15 10:11:05 2021 +0100
@@ -30,6 +30,7 @@
 import targets
 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
 from XSLTransform import XSLTransform
+from svghmi.i18n import POTWriter, POReader
 
 HMI_TYPES_DESC = {
     "HMI_NODE":{},
@@ -460,9 +461,9 @@
             "method":   "_StartInkscape"
         },
 
-        # TODO : Launch POEdit button
-        #        PO -> SVG layers button
-        #        SVG layers -> PO
+        # TODO : Launch POEdit button for new languqge (opens POT)
+
+        # TODO : Launch POEdit button for existing languqge (opens one of existing PO)
 
         # TODO : HMITree button
         #        - can drag'n'drop variabes to Inkscape
@@ -474,6 +475,10 @@
             project_path = self.CTNPath()
         return os.path.join(project_path, "svghmi.svg")
 
+    def _getPOTpath(self, project_path=None):
+        if project_path is None:
+            project_path = self.CTNPath()
+        return os.path.join(project_path, "messages.pot")
 
     def OnCTNSave(self, from_project_path=None):
         if from_project_path is not None:
@@ -513,6 +518,19 @@
         res = [hmi_tree_root.etree(add_hash=True)]
         return res
 
+    def GetTranslations(self, _context, msgs):
+
+        w = POTWriter()
+        w.ImportMessages(msgs)
+        # XXX get POT path
+        # XXX save POT file
+
+        # XXX scan existing PO files 
+        # XXX read PO files
+
+        r = POReader()
+        return None # XXX return all langs from all POs
+
     def CTNGenerate_C(self, buildpath, locations):
 
         location_str = "_".join(map(str, self.GetCurrentLocation()))
@@ -532,7 +550,8 @@
             # TODO : move to __init__
             transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"),
                           [("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()),
-                           ("GetHMITree", lambda *_ignored:self.GetHMITree())])
+                           ("GetHMITree", lambda *_ignored:self.GetHMITree()),
+                           ("GetTranslations", self.GetTranslations)])
 
 
             # load svg as a DOM with Etree
@@ -649,6 +668,28 @@
                 svgfile = None
             open_svg(svgfile)
 
+    def _StartPOEdit(self, POFile):
+        open_poedit = True
+        if not self.GetCTRoot().CheckProjectPathPerm():
+            dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
+                                      _("You don't have write permissions.\nOpen POEdit anyway ?"),
+                                      _("Open POEdit"),
+                                      wx.YES_NO | wx.ICON_QUESTION)
+            open_poedit = dialog.ShowModal() == wx.ID_YES
+            dialog.Destroy()
+        if open_poedit:
+            # XXX TODO
+            pass
+
+    def _EditTranslation(self):
+        """ Select a specific translation and edit it with POEdit """
+        pass
+
+    def _EditNewTranslation(self):
+        """ Start POEdit with untouched empty catalog """
+        POFile = self._getPOTpath()
+        self._StartPOEdit(POFile)
+
     def CTNGlobalInstances(self):
         # view_name = self.BaseParams.getName()
         # return [ (view_name + "_" + name, iec_type, "") for name, iec_type in SPECIAL_NODES]