SVGHMI: still WIP, now POT file is properly generated with utf-8 encoding and POEdit is launched when pressing button. svghmi
authorEdouard Tisserant
Tue, 19 Jan 2021 11:57:13 +0100
branchsvghmi
changeset 3112 bd20f9112014
parent 3111 5d9ae04ee50f
child 3113 18133b90196e
SVGHMI: still WIP, now POT file is properly generated with utf-8 encoding and POEdit is launched when pressing button.
svghmi/gen_index_xhtml.xslt
svghmi/i18n.py
svghmi/i18n.ysl2
svghmi/svghmi.py
--- a/svghmi/gen_index_xhtml.xslt	Mon Jan 18 10:32:13 2021 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Tue Jan 19 11:57:13 2021 +0100
@@ -1,6 +1,6 @@
 <?xml version="1.0"?>
-<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:str="http://exslt.org/strings" xmlns:func="http://exslt.org/functions" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:debug="debug" xmlns:preamble="preamble" xmlns:declarations="declarations" xmlns:definitions="definitions" xmlns:epilogue="epilogue" xmlns:ns="beremiz" version="1.0" extension-element-prefixes="ns func exsl regexp str dyn" exclude-result-prefixes="ns func exsl regexp str dyn debug preamble epilogue declarations definitions">
-  <xsl:output cdata-section-elements="xhtml:script" method="xml"/>
+<xsl:stylesheet xmlns:ns="beremiz" xmlns:definitions="definitions" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:func="http://exslt.org/functions" xmlns:epilogue="epilogue" xmlns:preamble="preamble" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:svg="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:str="http://exslt.org/strings" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:exsl="http://exslt.org/common" xmlns:declarations="declarations" xmlns:debug="debug" exclude-result-prefixes="ns func exsl regexp str dyn debug preamble epilogue declarations definitions" extension-element-prefixes="ns func exsl regexp str dyn" version="1.0">
+  <xsl:output method="xml" cdata-section-elements="xhtml:script"/>
   <xsl:variable name="svg" select="/svg:svg"/>
   <xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/>
   <xsl:variable name="hmitree" select="ns:GetHMITree()"/>
@@ -573,22 +573,14 @@
     <xsl:text>    jumps: [
 </xsl:text>
     <xsl:for-each select="$parsed_widgets/widget[@id = $all_page_widgets/@id and @type='Jump']">
-      <xsl:variable name="_id" select="@id"/>
-      <xsl:variable name="opts">
-        <xsl:call-template name="jump_widget_activity">
-          <xsl:with-param name="hmi_element" select="$hmi_elements[@id=$_id]"/>
-        </xsl:call-template>
-      </xsl:variable>
-      <xsl:if test="string-length($opts)&gt;0">
-        <xsl:text>        hmi_widgets["</xsl:text>
-        <xsl:value-of select="@id"/>
-        <xsl:text>"]</xsl:text>
-        <xsl:if test="position()!=last()">
-          <xsl:text>,</xsl:text>
-        </xsl:if>
-        <xsl:text>
-</xsl:text>
+      <xsl:text>        hmi_widgets["</xsl:text>
+      <xsl:value-of select="@id"/>
+      <xsl:text>"]</xsl:text>
+      <xsl:if test="position()!=last()">
+        <xsl:text>,</xsl:text>
       </xsl:if>
+      <xsl:text>
+</xsl:text>
     </xsl:for-each>
     <xsl:text>    ],
 </xsl:text>
@@ -706,6 +698,11 @@
       <xsl:text>All units must be set to "px" in Inkscape's document properties</xsl:text>
     </xsl:message>
   </xsl:template>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:text/@inkscape:label[starts-with(., '_')]">
+    <xsl:attribute name="{name()}">
+      <xsl:value-of select="substring(., 2)"/>
+    </xsl:attribute>
+  </xsl:template>
   <xsl:variable name="hmi_lists_descs" select="$parsed_widgets/widget[@type = 'List']"/>
   <xsl:variable name="hmi_lists" select="$hmi_elements[@id = $hmi_lists_descs/@id]"/>
   <xsl:variable name="targets_not_to_unlink" select="$hmi_lists/descendant-or-self::svg:*"/>
@@ -929,6 +926,60 @@
     <xsl:text>
 </xsl:text>
   </xsl:template>
+  <xsl:template mode="extract_i18n" match="svg:tspan">
+    <xsl:if test="string-length(.) &gt; 0">
+      <line>
+        <xsl:value-of select="."/>
+      </line>
+    </xsl:if>
+  </xsl:template>
+  <xsl:template mode="extract_i18n" match="svg:text">
+    <msg>
+      <xsl:attribute name="id">
+        <xsl:value-of select="@id"/>
+      </xsl:attribute>
+      <xsl:attribute name="label">
+        <xsl:value-of select="substring(@inkscape:label,2)"/>
+      </xsl:attribute>
+      <xsl:apply-templates mode="extract_i18n" select="svg:*"/>
+    </msg>
+  </xsl:template>
+  <xsl:variable name="translatable_texts" select="//svg:text[starts-with(@inkscape:label, '_')]"/>
+  <xsl:variable name="translatable_strings">
+    <xsl:apply-templates mode="extract_i18n" select="$translatable_texts"/>
+  </xsl:variable>
+  <preamble:i18n/>
+  <xsl:template match="preamble:i18n">
+    <xsl:text>
+</xsl:text>
+    <xsl:text>/* </xsl:text>
+    <xsl:value-of select="local-name()"/>
+    <xsl:text> */
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:variable name="translations" select="ns:GetTranslations($translatable_strings)"/>
+    <xsl:text>var translations = {
+</xsl:text>
+    <xsl:for-each select="$translations/*">
+      <xsl:text>    "</xsl:text>
+      <xsl:value-of select="local-name()"/>
+      <xsl:text>":{
+</xsl:text>
+      <xsl:text>    }</xsl:text>
+      <xsl:if test="position()!=last()">
+        <xsl:text>,</xsl:text>
+      </xsl:if>
+      <xsl:text>
+</xsl:text>
+    </xsl:for-each>
+    <xsl:text>};
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+  </xsl:template>
   <xsl:template mode="hmi_widgets" match="svg:*">
     <xsl:variable name="widget" select="func:widget(@id)"/>
     <xsl:variable name="eltid" select="@id"/>
@@ -1295,16 +1346,12 @@
 </xsl:text>
     <xsl:text>    overshot(new_val, max) {
 </xsl:text>
-    <xsl:text>        // TODO: use a Toast
-</xsl:text>
     <xsl:text>    }
 </xsl:text>
     <xsl:text>
 </xsl:text>
     <xsl:text>    undershot(new_val, min) {
 </xsl:text>
-    <xsl:text>        // TODO: use a Toast
-</xsl:text>
     <xsl:text>    }
 </xsl:text>
     <xsl:text>
@@ -2023,9 +2070,19 @@
 </xsl:text>
     <xsl:text>    dispatch(value) {
 </xsl:text>
+    <xsl:text>        this.display_val = value;
+</xsl:text>
+    <xsl:text>        this.request_animate();
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    animate(){
+</xsl:text>
     <xsl:text>        if(this.value_elt)
 </xsl:text>
-    <xsl:text>            this.value_elt.textContent = String(value);
+    <xsl:text>            this.value_elt.textContent = String(this.display_val);
 </xsl:text>
     <xsl:text>        let [min,max,start,end] = this.range;
 </xsl:text>
@@ -2033,21 +2090,29 @@
 </xsl:text>
     <xsl:text>        let [rx,ry] = this.proportions;
 </xsl:text>
-    <xsl:text>        let tip = start + (end-start)*Number(value)/(max-min);
+    <xsl:text>        let tip = start + (end-start)*Number(this.display_val)/(max-min);
 </xsl:text>
     <xsl:text>        let size = 0;
 </xsl:text>
-    <xsl:text>        if (tip-start &gt; Math.PI) {
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        if (tip-start &gt; Math.PI)
 </xsl:text>
     <xsl:text>            size = 1;
 </xsl:text>
-    <xsl:text>        } else {
+    <xsl:text>        else
 </xsl:text>
     <xsl:text>            size = 0;
 </xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        this.path_elt.setAttribute('d', "M "+(cx+rx*Math.cos(start))+","+(cy+ry*Math.sin(start))+" A "+rx+","+ry+" 0 "+size+" 1 "+(cx+rx*Math.cos(tip))+","+(cy+ry*Math.sin(tip)));
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        this.path_elt.setAttribute('d', "M "+(cx+rx*Math.cos(start))+","+(cy+ry*Math.sin(start))+
+</xsl:text>
+    <xsl:text>                                        " A "+rx+","+ry+
+</xsl:text>
+    <xsl:text>                                        " 0 "+size+
+</xsl:text>
+    <xsl:text>                                        " 1 "+(cx+rx*Math.cos(tip))+","+(cy+ry*Math.sin(tip)));
 </xsl:text>
     <xsl:text>    }
 </xsl:text>
@@ -2055,41 +2120,31 @@
 </xsl:text>
     <xsl:text>    init() {
 </xsl:text>
-    <xsl:text>        let start = Number(this.path_elt.getAttribute('sodipodi:start'));
-</xsl:text>
-    <xsl:text>        let end = Number(this.path_elt.getAttribute('sodipodi:end'));
-</xsl:text>
-    <xsl:text>        let cx = Number(this.path_elt.getAttribute('sodipodi:cx'));
-</xsl:text>
-    <xsl:text>        let cy = Number(this.path_elt.getAttribute('sodipodi:cy'));
-</xsl:text>
-    <xsl:text>        let rx = Number(this.path_elt.getAttribute('sodipodi:rx'));
-</xsl:text>
-    <xsl:text>        let ry = Number(this.path_elt.getAttribute('sodipodi:ry'));
-</xsl:text>
-    <xsl:text>        if (ry == 0) {
+    <xsl:text>        let [start, end, cx, cy, rx, ry] = ["start", "end", "cx", "cy", "rx", "ry"].
+</xsl:text>
+    <xsl:text>            map(tag=&gt;Number(this.path_elt.getAttribute('sodipodi:'+tag)))
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        if (ry == 0) 
 </xsl:text>
     <xsl:text>            ry = rx;
 </xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        if (start &gt; end) {
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        if (start &gt; end)
 </xsl:text>
     <xsl:text>            end = end + 2*Math.PI;
 </xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>        let min = this.min_elt ?
-</xsl:text>
-    <xsl:text>                  Number(this.min_elt.textContent) :
-</xsl:text>
-    <xsl:text>                  this.args.length &gt;= 1 ? this.args[0] : 0;
-</xsl:text>
-    <xsl:text>        let max = this.max_elt ?
-</xsl:text>
-    <xsl:text>                  Number(this.max_elt.textContent) :
-</xsl:text>
-    <xsl:text>                  this.args.length &gt;= 2 ? this.args[1] : 100;
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        let [min,max] = [[this.min_elt,0],[this.max_elt,100]].map(([elt,def],i)=&gt;elt?
+</xsl:text>
+    <xsl:text>            Number(elt.textContent) :
+</xsl:text>
+    <xsl:text>            this.args.length &gt;= i+1 ? this.args[i] : def);
+</xsl:text>
+    <xsl:text>
 </xsl:text>
     <xsl:text>        this.range = [min, max, start, end];
 </xsl:text>
@@ -2117,8 +2172,6 @@
       </xsl:with-param>
       <xsl:with-param name="mandatory" select="'no'"/>
     </xsl:call-template>
-    <xsl:text>
-</xsl:text>
   </xsl:template>
   <xsl:template mode="widget_class" match="widget[@type='CircularSlider']">
     <xsl:text>class CircularSliderWidget extends Widget{
@@ -3835,10 +3888,10 @@
         <xsl:text>text box button highlight</xsl:text>
       </xsl:with-param>
     </xsl:call-template>
-    <xsl:text>    // It is assumed that list content conforms to Array interface.
-</xsl:text>
     <xsl:text>    content: [
 </xsl:text>
+    <xsl:text>    /* TODO : Support HMI:List */
+</xsl:text>
     <xsl:for-each select="arg">
       <xsl:text>"</xsl:text>
       <xsl:value-of select="@value"/>
@@ -4638,142 +4691,146 @@
     <xsl:text>    }
 </xsl:text>
   </xsl:template>
-  <xsl:template name="jump_widget_activity">
-    <xsl:param name="hmi_element"/>
-    <xsl:call-template name="defs_by_labels">
-      <xsl:with-param name="hmi_element" select="$hmi_element"/>
-      <xsl:with-param name="labels">
-        <xsl:text>active inactive</xsl:text>
-      </xsl:with-param>
-      <xsl:with-param name="mandatory" select="'no'"/>
-    </xsl:call-template>
-  </xsl:template>
-  <xsl:template name="jump_widget_disability">
-    <xsl:param name="hmi_element"/>
-    <xsl:call-template name="defs_by_labels">
-      <xsl:with-param name="hmi_element" select="$hmi_element"/>
-      <xsl:with-param name="labels">
-        <xsl:text>disabled</xsl:text>
-      </xsl:with-param>
-      <xsl:with-param name="mandatory" select="'no'"/>
-    </xsl:call-template>
+  <xsl:template mode="widget_class" match="widget[@type='Jump']">
+    <xsl:text>    class JumpWidget extends Widget{
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        activable = false;
+</xsl:text>
+    <xsl:text>        active = false;
+</xsl:text>
+    <xsl:text>        disabled = false;
+</xsl:text>
+    <xsl:text>        frequency = 2;
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        update_activity() {
+</xsl:text>
+    <xsl:text>            if(this.active) {
+</xsl:text>
+    <xsl:text>                 /* show active */ 
+</xsl:text>
+    <xsl:text>                 this.active_elt.setAttribute("style", this.active_elt_style);
+</xsl:text>
+    <xsl:text>                 /* hide inactive */ 
+</xsl:text>
+    <xsl:text>                 this.inactive_elt.setAttribute("style", "display:none");
+</xsl:text>
+    <xsl:text>            } else {
+</xsl:text>
+    <xsl:text>                 /* show inactive */ 
+</xsl:text>
+    <xsl:text>                 this.inactive_elt.setAttribute("style", this.inactive_elt_style);
+</xsl:text>
+    <xsl:text>                 /* hide active */ 
+</xsl:text>
+    <xsl:text>                 this.active_elt.setAttribute("style", "display:none");
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        make_on_click() {
+</xsl:text>
+    <xsl:text>            let that = this;
+</xsl:text>
+    <xsl:text>            const name = this.args[0];
+</xsl:text>
+    <xsl:text>            return function(evt){
+</xsl:text>
+    <xsl:text>                /* TODO: suport path pointing to local variable whom value 
+</xsl:text>
+    <xsl:text>                   would be an HMI_TREE index to jump to a relative page */
+</xsl:text>
+    <xsl:text>                const index = that.indexes.length &gt; 0 ? that.indexes[0] + that.offset : undefined;
+</xsl:text>
+    <xsl:text>                switch_page(name, index);
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        notify_page_change(page_name, index) {
+</xsl:text>
+    <xsl:text>            if(this.activable) {
+</xsl:text>
+    <xsl:text>                const ref_index = this.indexes.length &gt; 0 ? this.indexes[0] + this.offset : undefined;
+</xsl:text>
+    <xsl:text>                const ref_name = this.args[0];
+</xsl:text>
+    <xsl:text>                this.active = ((ref_name == undefined || ref_name == page_name) &amp;&amp; index == ref_index);
+</xsl:text>
+    <xsl:text>                this.update_activity();
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>        dispatch(value) {
+</xsl:text>
+    <xsl:text>            this.disabled = !Number(value);
+</xsl:text>
+    <xsl:text>            if(this.disabled) {
+</xsl:text>
+    <xsl:text>              /* show disabled */ 
+</xsl:text>
+    <xsl:text>              this.disabled_elt.setAttribute("style", this.disabled_elt_style);
+</xsl:text>
+    <xsl:text>              /* hide inactive */ 
+</xsl:text>
+    <xsl:text>              this.inactive_elt.setAttribute("style", "display:none");
+</xsl:text>
+    <xsl:text>              /* hide active */ 
+</xsl:text>
+    <xsl:text>              this.active_elt.setAttribute("style", "display:none");
+</xsl:text>
+    <xsl:text>            } else {
+</xsl:text>
+    <xsl:text>              /* hide disabled */ 
+</xsl:text>
+    <xsl:text>              this.disabled_elt.setAttribute("style", "display:none");
+</xsl:text>
+    <xsl:text>              this.update_activity();
+</xsl:text>
+    <xsl:text>            }
+</xsl:text>
+    <xsl:text>        }
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
   </xsl:template>
   <xsl:template mode="widget_defs" match="widget[@type='Jump']">
     <xsl:param name="hmi_element"/>
     <xsl:variable name="activity">
-      <xsl:call-template name="jump_widget_activity">
+      <xsl:call-template name="defs_by_labels">
         <xsl:with-param name="hmi_element" select="$hmi_element"/>
+        <xsl:with-param name="labels">
+          <xsl:text>active inactive</xsl:text>
+        </xsl:with-param>
+        <xsl:with-param name="mandatory" select="'no'"/>
       </xsl:call-template>
     </xsl:variable>
     <xsl:variable name="have_activity" select="string-length($activity)&gt;0"/>
     <xsl:value-of select="$activity"/>
     <xsl:variable name="disability">
-      <xsl:call-template name="jump_widget_disability">
+      <xsl:call-template name="defs_by_labels">
         <xsl:with-param name="hmi_element" select="$hmi_element"/>
+        <xsl:with-param name="labels">
+          <xsl:text>disabled</xsl:text>
+        </xsl:with-param>
+        <xsl:with-param name="mandatory" select="'no'"/>
       </xsl:call-template>
     </xsl:variable>
     <xsl:variable name="have_disability" select="$have_activity and string-length($disability)&gt;0"/>
     <xsl:value-of select="$disability"/>
-    <xsl:if test="$have_activity">
-      <xsl:text>    active: false,
-</xsl:text>
-      <xsl:if test="$have_disability">
-        <xsl:text>    disabled: false,
-</xsl:text>
-        <xsl:text>    frequency: 2,
-</xsl:text>
-        <xsl:text>    dispatch: function(value) {
-</xsl:text>
-        <xsl:text>        this.disabled = !Number(value);
-</xsl:text>
-        <xsl:text>        this.update();
-</xsl:text>
-        <xsl:text>    },
-</xsl:text>
-      </xsl:if>
-      <xsl:text>    update: function(){
-</xsl:text>
-      <xsl:if test="$have_disability">
-        <xsl:text>      if(this.disabled) {
-</xsl:text>
-        <xsl:text>        /* show disabled */ 
-</xsl:text>
-        <xsl:text>        this.disabled_elt.setAttribute("style", this.active_elt_style);
-</xsl:text>
-        <xsl:text>        /* hide inactive */ 
-</xsl:text>
-        <xsl:text>        this.inactive_elt.setAttribute("style", "display:none");
-</xsl:text>
-        <xsl:text>        /* hide active */ 
-</xsl:text>
-        <xsl:text>        this.active_elt.setAttribute("style", "display:none");
-</xsl:text>
-        <xsl:text>      } else {
-</xsl:text>
-        <xsl:text>        /* hide disabled */ 
-</xsl:text>
-        <xsl:text>        this.disabled_elt.setAttribute("style", "display:none");
-</xsl:text>
-      </xsl:if>
-      <xsl:text>        if(this.active) {
-</xsl:text>
-      <xsl:text>             /* show active */ 
-</xsl:text>
-      <xsl:text>             this.active_elt.setAttribute("style", this.active_elt_style);
-</xsl:text>
-      <xsl:text>             /* hide inactive */ 
-</xsl:text>
-      <xsl:text>             this.inactive_elt.setAttribute("style", "display:none");
-</xsl:text>
-      <xsl:text>        } else {
-</xsl:text>
-      <xsl:text>             /* show inactive */ 
-</xsl:text>
-      <xsl:text>             this.inactive_elt.setAttribute("style", this.inactive_elt_style);
-</xsl:text>
-      <xsl:text>             /* hide active */ 
-</xsl:text>
-      <xsl:text>             this.active_elt.setAttribute("style", "display:none");
-</xsl:text>
-      <xsl:text>        }
-</xsl:text>
-      <xsl:if test="$have_disability">
-        <xsl:text>      }
-</xsl:text>
-      </xsl:if>
-      <xsl:text>    },
-</xsl:text>
-    </xsl:if>
-    <xsl:if test="$have_activity">
-      <xsl:text>    notify_page_change: function(page_name, index){
-</xsl:text>
-      <xsl:text>        const ref_index = this.indexes.length &gt; 0 ? this.indexes[0] + this.offset : undefined;
-</xsl:text>
-      <xsl:text>        const ref_name = this.args[0];
-</xsl:text>
-      <xsl:text>        this.active =((ref_name == undefined || ref_name == page_name) &amp;&amp; index == ref_index);
-</xsl:text>
-      <xsl:text>        this.update();
-</xsl:text>
-      <xsl:text>    },
-</xsl:text>
-    </xsl:if>
-    <xsl:text>    make_on_click(){
-</xsl:text>
-    <xsl:text>        let that = this;
-</xsl:text>
-    <xsl:text>        const name = this.args[0];
-</xsl:text>
-    <xsl:text>        return function(evt){
-</xsl:text>
-    <xsl:text>            const index = that.indexes.length &gt; 0 ? that.indexes[0] + that.offset : undefined;
-</xsl:text>
-    <xsl:text>            switch_page(name, index);
-</xsl:text>
-    <xsl:text>        }
-</xsl:text>
-    <xsl:text>    },
-</xsl:text>
     <xsl:text>    init: function() {
 </xsl:text>
     <xsl:text>        this.element.onclick = this.make_on_click();
@@ -4783,6 +4840,8 @@
 </xsl:text>
       <xsl:text>        this.inactive_elt_style = this.inactive_elt.getAttribute("style");
 </xsl:text>
+      <xsl:text>        this.activable = true;
+</xsl:text>
     </xsl:if>
     <xsl:choose>
       <xsl:when test="$have_disability">
@@ -5360,13 +5419,23 @@
 </xsl:text>
     <xsl:text>    dispatch(value) {
 </xsl:text>
+    <xsl:text>        this.display_val = value;
+</xsl:text>
+    <xsl:text>        this.request_animate();
+</xsl:text>
+    <xsl:text>    }
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>    animate(){
+</xsl:text>
     <xsl:text>        if(this.value_elt)
 </xsl:text>
-    <xsl:text>            this.value_elt.textContent = String(value);
+    <xsl:text>            this.value_elt.textContent = String(this.display_val);
 </xsl:text>
     <xsl:text>        let [min,max,totallength] = this.range;
 </xsl:text>
-    <xsl:text>        let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
+    <xsl:text>        let length = Math.max(0,Math.min(totallength,(Number(this.display_val)-min)*totallength/(max-min)));
 </xsl:text>
     <xsl:text>        let tip = this.range_elt.getPointAtLength(length);
 </xsl:text>
@@ -5378,17 +5447,13 @@
 </xsl:text>
     <xsl:text>    init() {
 </xsl:text>
-    <xsl:text>        let min = this.min_elt ?
-</xsl:text>
-    <xsl:text>                    Number(this.min_elt.textContent) :
-</xsl:text>
-    <xsl:text>                    this.args.length &gt;= 1 ? this.args[0] : 0;
-</xsl:text>
-    <xsl:text>        let max = this.max_elt ?
-</xsl:text>
-    <xsl:text>                    Number(this.max_elt.textContent) :
-</xsl:text>
-    <xsl:text>                    this.args.length &gt;= 2 ? this.args[1] : 100;
+    <xsl:text>        let [min,max] = [[this.min_elt,0],[this.max_elt,100]].map(([elt,def],i)=&gt;elt?
+</xsl:text>
+    <xsl:text>            Number(elt.textContent) :
+</xsl:text>
+    <xsl:text>            this.args.length &gt;= i+1 ? this.args[i] : def);
+</xsl:text>
+    <xsl:text>
 </xsl:text>
     <xsl:text>        this.range = [min, max, this.range_elt.getTotalLength()]
 </xsl:text>
@@ -5416,8 +5481,6 @@
       </xsl:with-param>
       <xsl:with-param name="mandatory" select="'no'"/>
     </xsl:call-template>
-    <xsl:text>
-</xsl:text>
   </xsl:template>
   <xsl:template mode="widget_class" match="widget[@type='MultiState']">
     <xsl:text>class MultiStateWidget extends Widget{
@@ -6396,7 +6459,7 @@
     <xsl:comment>
       <xsl:apply-templates select="document('')/*/debug:*"/>
     </xsl:comment>
-    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <html xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/1999/xhtml">
       <head/>
       <body style="margin:0;overflow:hidden;user-select:none;touch-action:none;">
         <xsl:copy-of select="$result_svg"/>
--- a/svghmi/i18n.py	Mon Jan 18 10:32:13 2021 +0100
+++ b/svghmi/i18n.py	Tue Jan 19 11:57:13 2021 +0100
@@ -1,4 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz
+# Copyright (C) 2021: Edouard TISSERANT
+#
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import sys
+import subprocess
 import time
+import wx
+
+def open_pofile(pofile):
+    """ Opens PO file with POEdit """
+    
+    if sys.platform.startswith('win'):
+        from six.moves import winreg
+        poedit_cmd = None
+        try:
+            poedit_cmd = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
+                                           'SOFTWARE\\Classes\\poedit\\shell\\open\\command')
+            poedit_path = poedit_cmd.replace('"%1"', '').strip().replace('"', '')
+        except OSError:
+            poedit_path = None
+
+    else:
+        try:
+            poedit_path = subprocess.check_output("command -v poedit", shell=True).strip()
+        except subprocess.CalledProcessError:
+            poedit_path = None
+
+    if poedit_path is None:
+        wx.MessageBox("POEdit is not found or installed !")
+    else:
+        subprocess.Popen([poedit_path,pofile])
 
 locpfx = '#:svghmi.svg:'
 
@@ -15,22 +51,71 @@
 "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"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
 "Generated-By: SVGHMI 1.0\\n"
 
 '''
+escapes = []
+
+def make_escapes(pass_iso8859):
+    global escapes
+    escapes = [chr(i) for i in range(256)]
+    if pass_iso8859:
+        # Allow iso-8859 characters to pass through so that e.g. 'msgid
+        # "Höhe"' would result not result in 'msgid "H\366he"'.  Otherwise we
+        # escape any character outside the 32..126 range.
+        mod = 128
+    else:
+        mod = 256
+    for i in range(mod):
+        if not(32 <= i <= 126):
+            escapes[i] = "\\%03o" % i
+    escapes[ord('\\')] = '\\\\'
+    escapes[ord('\t')] = '\\t'
+    escapes[ord('\r')] = '\\r'
+    escapes[ord('\n')] = '\\n'
+    escapes[ord('\"')] = '\\"'
+
+make_escapes(pass_iso8859 = True)
+
+EMPTYSTRING = ''
+
+def escape(s):
+    global escapes
+    s = list(s)
+    for i in range(len(s)):
+        s[i] = escapes[ord(s[i])]
+    return EMPTYSTRING.join(s)
+
+def normalize(s):
+    # This converts the various Python string types into a format that is
+    # appropriate for .po files, namely much closer to C style.
+    lines = s.split('\n')
+    if len(lines) == 1:
+        s = '"' + escape(s) + '"'
+    else:
+        if not lines[-1]:
+            del lines[-1]
+            lines[-1] = lines[-1] + '\n'
+        for i in range(len(lines)):
+            lines[i] = escape(lines[i])
+        lineterm = '\\n"\n"'
+        s = '""\n"' + lineterm.join(lines) + '"'
+    return s
+
 
 class POTWriter:
     def __init__(self):
         self.__messages = {}
 
-    def ImportMessages(self, msgs):    
+    def ImportMessages(self, msgs):
         for msg in msgs:
-            self.addentry("\n".join([line.text for line in msg]), msg.get("label"), msg.get("id"))
+            self.addentry("\n".join([line.text.encode("utf-8") for line in msg]), msg.get("label"), msg.get("id"))
 
     def addentry(self, msg, label, svgid):
         entry = (label, svgid)
+        print(entry)
         self.__messages.setdefault(msg, set()).add(entry)
 
     def write(self, fp):
@@ -47,12 +132,12 @@
             rentries = reverse[rkey]
             rentries.sort()
             for k, v in rentries:
-                v = v.keys()
+                v = list(v)
                 v.sort()
                 locline = locpfx
                 for label, svgid in v:
                     d = {'label': label, 'svgid': svgid}
-                    s = _(' %(label)s:%(svgid)d') % d
+                    s = _(' %(label)s:%(svgid)s') % d
                     if len(locline) + len(s) <= 78:
                         locline = locline + s
                     else:
--- a/svghmi/i18n.ysl2	Mon Jan 18 10:32:13 2021 +0100
+++ b/svghmi/i18n.ysl2	Tue Jan 19 11:57:13 2021 +0100
@@ -10,7 +10,7 @@
 template "svg:text", mode="extract_i18n" {
     msg {
         attrib "id" value "@id";
-        attrib "label" value "@inkscape:label";
+        attrib "label" value "substring(@inkscape:label,2)";
         apply "svg:*", mode="extract_i18n";
     }
 }
--- a/svghmi/svghmi.py	Mon Jan 18 10:32:13 2021 +0100
+++ b/svghmi/svghmi.py	Tue Jan 19 11:57:13 2021 +0100
@@ -30,7 +30,7 @@
 import targets
 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
 from XSLTransform import XSLTransform
-from svghmi.i18n import POTWriter, POReader
+from svghmi.i18n import POTWriter, POReader, open_pofile
 
 HMI_TYPES_DESC = {
     "HMI_NODE":{},
@@ -455,19 +455,27 @@
             "method":   "_ImportSVG"
         },
         {
-            "bitmap":    "ImportSVG",  # should be something different
+            "bitmap":    "EditSVG",  # should be something different
             "name":    _("Inkscape"),
             "tooltip": _("Edit HMI"),
             "method":   "_StartInkscape"
         },
-
-        # TODO : Launch POEdit button for new languqge (opens POT)
-
-        # TODO : Launch POEdit button for existing languqge (opens one of existing PO)
+        {
+            "bitmap":    "OpenPOT",  # should be something different
+            "name":    _("New lang"),
+            "tooltip": _("Open non translated message catalog (POT) to start new language"),
+            "method":   "_OpenPOT"
+        },
+
+        {
+            "bitmap":    "EditPO",  # should be something different
+            "name":    _("Edit lang"),
+            "tooltip": _("Edit existing message catalog (PO) for specific language"),
+            "method":   "_EditPO"
+        },
 
         # TODO : HMITree button
         #        - can drag'n'drop variabes to Inkscape
-
     ]
 
     def _getSVGpath(self, project_path=None):
@@ -484,6 +492,9 @@
         if from_project_path is not None:
             shutil.copyfile(self._getSVGpath(from_project_path),
                             self._getSVGpath())
+            shutil.copyfile(self._getPOTpath(from_project_path),
+                            self._getPOTpath())
+            # XXX TODO copy .PO files
         return True
 
     def GetSVGGeometry(self):
@@ -522,8 +533,9 @@
 
         w = POTWriter()
         w.ImportMessages(msgs)
-        # XXX get POT path
-        # XXX save POT file
+
+        with open(self._getPOTpath(), 'w') as POT_file:
+            w.write(POT_file)
 
         # XXX scan existing PO files 
         # XXX read PO files
@@ -678,14 +690,24 @@
             open_poedit = dialog.ShowModal() == wx.ID_YES
             dialog.Destroy()
         if open_poedit:
-            # XXX TODO
-            pass
-
-    def _EditTranslation(self):
+            open_pofile(POFile)
+
+    def _EditPO(self):
         """ Select a specific translation and edit it with POEdit """
-        pass
-
-    def _EditNewTranslation(self):
+        project_path = self.CTNPath()
+        dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a PO file"), project_path, "",  _("PO files (*.po)|*.po"), wx.OPEN)
+        if dialog.ShowModal() == wx.ID_OK:
+            POFile = dialog.GetPath()
+            if os.path.isfile(POFile):
+                if os.path.dirname(POFile) == project_path:
+                    self._StartPOEdit(POFile)
+                else:
+                    self.GetCTRoot().logger.write_error(_("PO file misplaced: %s is not in %s\n") % (POFile,project_path))
+            else:
+                self.GetCTRoot().logger.write_error(_("PO file do not exist: %s\n") % POFile)
+        dialog.Destroy()
+
+    def _OpenPOT(self):
         """ Start POEdit with untouched empty catalog """
         POFile = self._getPOTpath()
         self._StartPOEdit(POFile)