# HG changeset patch # User Edouard Tisserant # Date 1610701865 -3600 # Node ID 079419e7228d47e0e6302f1dad4576b19d86eb77 # Parent ee0704cc6dc8822d9bfbf2a47ec5ef9a4e565059 SVGHMI: Intermediate commit while implementing i18n. WIP. diff -r ee0704cc6dc8 -r 079419e7228d svghmi/gen_index_xhtml.ysl2 --- 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 diff -r ee0704cc6dc8 -r 079419e7228d svghmi/i18n.py --- /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 , 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 \\n" +"Language-Team: LANGUAGE \\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) + + diff -r ee0704cc6dc8 -r 079419e7228d svghmi/i18n.ysl2 --- /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()" > ,` + } + | }; + | + +} diff -r ee0704cc6dc8 -r 079419e7228d svghmi/inline_svg.ysl2 --- 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 diff -r ee0704cc6dc8 -r 079419e7228d svghmi/svghmi.py --- 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]