SVGHMI: Intermediate commit while implementing i18n. WIP.
--- 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]