]> WPIA git - motion.git/commitdiff
add: make app multilanguage with two languages: EN + DE
authorINOPIAE <m.maengel@inopiae.de>
Wed, 18 Mar 2020 11:07:00 +0000 (12:07 +0100)
committerINOPIAE <m.maengel@inopiae.de>
Sun, 15 Nov 2020 03:17:15 +0000 (04:17 +0100)
Change-Id: I96713d380fedc4b6ff3424785cc10e554ce3fd7b

config.py.example
motion.py
requirements.txt
templates/base.html
templates/index.html
templates/motion.html
templates/proxy.html
templates/single_motion.html
tests/test_motion.py
translations/babel.cfg [new file with mode: 0644]

index 83cb5fb83a499aa7a98ca328062b4d291aa8aa88..56ce4bc6c275e21445c0f4bb3b31d1e5f8e83458 100644 (file)
@@ -18,3 +18,8 @@ MAX_PROXY=2  # user is allowed to hold up to MAX_PROXY votes
 DURATION={'hostname':[3, 7, 14]} # duration period for motions
 
 #DEBUGUSER={'hostname':'username/create:* vote:*'} # remove # at beginning of line to use local debuguser
+
+LANGUAGES = {
+    'en': 'English',
+    'de': 'Deutsch'
+}
index 9e70c991dcd1fbe4354e7b9845749ad419ac964c..9326152ae8d99b9e96cea21005b0e2d5e35db293 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -3,11 +3,13 @@ from flask import Flask
 from flask import render_template, redirect
 from flask import request
 from functools import wraps
+from flask_babel import Babel, gettext
 import postgresql
 import filters
 from flaskext.markdown import Markdown
 from markdown.extensions import Extension
 from datetime import date, time, datetime
+import gettext
 
 def get_db():
     db = getattr(g, '_database', None)
@@ -18,6 +20,8 @@ def get_db():
 
 app = Flask(__name__)
 app.register_blueprint(filters.blueprint)
+babel = Babel(app)
+gettext.install('motion')
 
 class EscapeHtml(Extension):
     def extendMarkdown(self, md, md_globals):
@@ -55,6 +59,10 @@ debuguser = ConfigProxy("DEBUGUSER")
 
 max_proxy=app.config.get("MAX_PROXY")
 
+@babel.localeselector
+def get_locale():
+    return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
+
 @app.before_request
 def lookup_user():
     global prefix
@@ -248,18 +256,18 @@ def rel_redirect(loc):
 def put_motion():
     cat=request.form.get("category", "")
     if cat not in get_allowed_cats("create"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     time = int(request.form.get("days", "3"));
     if time not in times.per_host:
-        return "Error, invalid length", 400
+        return _('Error, invalid length'), 400
     title=request.form.get("title", "")
     title=title.strip()
     if title =='':
-        return "Error, missing title", 400
+        return _('Error, missing title'), 400
     content=request.form.get("content", "")
     content=content.strip()
     if content =='':
-        return "Error, missing content", 400
+        return _('Error, missing content'), 400
 
     db = get_db()
     with db.xact():
@@ -285,14 +293,14 @@ def validate_motion_access(privilege):
             with db.xact():
                 rv = db.prepare("SELECT id, type, deadline < CURRENT_TIMESTAMP AS expired, canceled FROM motion WHERE identifier=$1 AND host=$2")(motion, request.host);
                 if len(rv) == 0:
-                    return "Error, Not found", 404
+                    return _('Error, Not found'), 404
                 id = rv[0].get("id")
                 if not may(privilege, rv[0].get("type")):
-                    return "Forbidden", 403
+                    return _('Forbidden'), 403
                 if rv[0].get("canceled") is not None:
-                    return "Error, motion was canceled", 403
+                    return _('Error, motion was canceled'), 403
                 if rv[0].get("expired"):
-                    return "Error, out of time", 403
+                    return _('Error, out of time'), 403
             return f(motion, id)
         decorated_function.__name__ = f.__name__
         return decorated_function
@@ -311,7 +319,7 @@ def validate_motion_access_vote(privilege):
 @validate_motion_access('cancel')
 def cancel_motion(motion, id):
     if request.form.get("reason", "none") == "none":
-        return "Error, form requires reason", 500
+        return _('Error, form requires reason'), 500
     rv = get_db().prepare("UPDATE motion SET canceled=CURRENT_TIMESTAMP, cancelation_reason=$1, canceled_by=$2 WHERE identifier=$3 AND host=$4 AND canceled is NULL")(request.form.get("reason", ""), g.voter, motion, request.host)
     return motion_edited(motion)
 
@@ -330,7 +338,7 @@ def show_motion(motion):
                          + "WHERE motion.identifier=$1 AND motion.host=$3")
     resultmotion = p(motion, g.voter, request.host)
     if len(resultmotion) == 0:
-        return "Error, Not found", 404
+        return _('Error, Not found'), 404
 
     p = get_db().prepare("SELECT voter.email FROM vote INNER JOIN voter ON vote.proxy_id = voter.id WHERE vote.motion_id=$1 AND vote.voter_id=$2 AND vote.proxy_id <> vote.voter_id")
     resultproxyname = p(resultmotion[0][0], g.voter)
@@ -359,7 +367,7 @@ def vote(motion, voter, id):
     if (voterid != g.voter):
         rv = db.prepare("SELECT voter_id FROM proxy WHERE proxy.revoked IS NULL AND proxy.proxy_id = $1 AND proxy.voter_id = $2")(g.voter, voterid);
         if len(rv) == 0:
-            return "Error, proxy not found.", 400
+            return _('Error, proxy not found.'), 400
 
     p = db.prepare("SELECT * FROM vote WHERE motion_id = $1 AND voter_id = $2")
     rv = p(id, voterid)
@@ -372,39 +380,39 @@ def vote(motion, voter, id):
 @app.route("/proxy")
 def proxy():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"))
 
 @app.route("/proxy/add", methods=['POST'])
 def add_proxy():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     voter=request.form.get("voter", "")
     proxy=request.form.get("proxy", "")
     if voter == proxy :
-        return "Error, voter equals proxy.", 400
+        return _('Error, voter equals proxy.'), 400
     rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(voter);
     if len(rv) == 0:
-        return "Error, voter not found.", 400
+        return _('Error, voter not found.'), 400
     voterid = rv[0].get("id")
     rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(proxy);
     if len(rv) == 0:
-        return "Error, proxy not found.", 400
+        return _('Error, proxy not found.'), 400
     proxyid = rv[0].get("id")
     rv = get_db().prepare("SELECT id FROM proxy WHERE voter_id=$1 AND revoked is NULL")(voterid);
     if len(rv) != 0:
-        return "Error, proxy allready given.", 400
+        return _('Error, proxy allready given.'), 400
     rv = get_db().prepare("SELECT COUNT(id) as c FROM proxy WHERE proxy_id=$1 AND revoked is NULL GROUP BY proxy_id")(proxyid);
     if len(rv) != 0:
         if rv[0].get("c") >= max_proxy:
-            return "Error, Max proxy for '" + proxy + "' reached.", 400
+            return "Error, Max proxy for '%s' reached." % (proxy), 400
     rv = get_db().prepare("INSERT INTO proxy(voter_id, proxy_id, granted_by) VALUES ($1,$2,$3)")(voterid, proxyid, g.voter)
     return rel_redirect("/proxy")
 
 @app.route("/proxy/revoke", methods=['POST'])
 def revoke_proxy():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     id=request.form.get("id", "")
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE id=$2")(g.voter, int(id))
     return rel_redirect("/proxy")
@@ -412,7 +420,7 @@ def revoke_proxy():
 @app.route("/proxy/revokeall", methods=['POST'])
 def revoke_proxy_all():
     if not may_admin("proxyadmin"):
-        return "Forbidden", 403
+        return _('Forbidden'), 403
     rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
     return rel_redirect("/proxy")
 
index 0713e61650a17e769757df9b9d33dad7e936d675..f2f536ce0260d7a32996d300f1248e7b05161c81 100644 (file)
@@ -1,5 +1,6 @@
 click==6.7
 Flask==0.12.2
+Flask-Babel==1.0.0
 itsdangerous==0.24
 Jinja2==2.10
 MarkupSafe==1.0
index c65a9484050021a70375e67986bfd032d2bc767d..b777e335276ffbafb7a61a39469555e3132c4fb6 100644 (file)
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
 <head>
-<title>{% block title %}Motion list{% endblock %}</title>
+<title>{% block title %}{{_('Motion list')}}{% endblock %}</title>
 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
 <style type="text/css">
 .form-inline .motion {
@@ -58,27 +58,27 @@ form {
 </head>
 <body>
 <nav class="navbar navbar-expand-lg navbar-light bg-light">
-  <a class="navbar-brand" href="../">{{'Motion list'}}</a>
+  <a class="navbar-brand" href="../">{{_('Motion list')}}</a>
   <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
     <span class="navbar-toggler-icon"></span>
   </button>
    <div class="collapse navbar-collapse" id="navbarNavDropdown">
     <ul class="navbar-nav">
       <li class="nav-item">
-        <a class="nav-link" href="/">{{'Home'}}</a>
+        <a class="nav-link" href="/">{{_('Home')}}</a>
       </li>
 {%- if may_proxyadmin %}
       <li class="nav-item">
-        <a class="nav-link" href="/proxy">{{'Proxy management'}}</a>
+        <a class="nav-link" href="/proxy">{{_('Proxy management')}}</a>
       </li>
 {%- endif %}
       <li class="nav-item">
-        <a class="nav-link">{{'User'}}: {{g.user}}
+        <a class="nav-link">{{_('User')}}: {{g.user}}
 {%- if g.proxies_given %}
-         <br/>proxy granted to: {{g.proxies_given}}
+         <br/>{{_('proxy granted to')}}: {{g.proxies_given}}
 {%- endif %}
 {%- if g.proxies_received %}
-         <br/>holds proxy of: {{g.proxies_received}}
+         <br/>{{_('holds proxy of')}}: {{g.proxies_received}}
 {%- endif %}
         </a>
       </li>
@@ -90,9 +90,9 @@ form {
 <!-- Footer -->
   <footer class="page-footer">
     <div class="footer-copyright text-center py-3">
-      <p>&copy; {{footer.version_year}} Copyright: <a href="{{footer.copyright_link}}">{{footer.copyright_name}}</a> 
-      | <a href="{{footer.imprint_link}}">Imprint</a> 
-      | <a href="{{footer.dataprotection_link}}">Data protection</a></p>
+      <p>&copy; {{footer.version_year}} {{_('Copyright')}}: <a href="{{footer.copyright_link}}">{{footer.copyright_name}}</a> 
+      | <a href="{{footer.imprint_link}}">{{_('Imprint')}}</a> 
+      | <a href="{{footer.dataprotection_link}}">{{_('Data protection')}}</a></p>
     </div>
   </footer>
 </body>
index 16adecc2a9127fda861e405ce655b348eccc9ec7..e3d1bad1e823c1cc35c9c0fab3893d7799605480 100644 (file)
@@ -2,10 +2,12 @@
 {% block body %}
 <div class="container">
 {%- if categories|length != 0 %}
+<div class="card">
+</div>
 <form action="/motion" method="POST" class="form-inline">
 <div class="motion card">
   <div class="motion-title card-heading alert-light from-group">
-    <input class="form-control motion-title-input" placeholder="Motion title" type="text" name="title" id="title" required="yes">
+    <input class="form-control motion-title-input" placeholder="{{_('Motion title')}}" type="text" name="title" id="title" required="yes">
     {%- if categories|length == 1 %}
     <input type="text" class="float form-control" maxwidth="10" disabled value="{{categories[0]}}">
     <input type="hidden" name="category" value="{{categories[0]}}">
     </select>
   </div>
   <div class="card-body">
-    <textarea class="form-control" placeholder="Motion content" name="content" rows="8"></textarea><br>
-    Editing note: Markdown is used formatting.<br>
-    To add a line break add two lines, to enter a link use [text](https://domain.tld/link)<br>
-    <button class="btn btn-primary" type="submit">Submit Motion</button>
+    <textarea class="form-control" placeholder="{{_('Motion content')}}" name="content" rows="8"></textarea><br>
+    {{_('Editing note: Markdown is used formatting.')}}<br>
+    {{_('To add a line break add two lines, to enter a link use [text](https//domain.tld/link)')}}<br>
+    <button class="btn btn-primary" type="submit">{{_('Submit Motion')}}</button>
   </div>
 </div>
 </form>
 {%- endif %}
 {%- if prev %}
 {%- if prev == -1 %}
-<a href="/" class="btn btn-primary">Prev</a>
+<a href="/" class="btn btn-primary">{{_('Prev')}}</a>
 {%- else %}
-<a href="/?start={{ prev }}" class="btn btn-primary">Prev</a>
+<a href="/?start={{ prev }}" class="btn btn-primary">{{_('Prev')}}</a>
 {%- endif %}
 {%- endif %}
 {%- for motion in motions %}
 {% include 'motion.html' %}
 {%- endfor %}
 {%- if more %}
-<a href="/?start={{ more }}" class="btn btn-primary">Next</a>
+<a href="/?start={{ more }}" class="btn btn-primary">{{_('Next')}}</a>
 {%- endif %}
 </div>
 {%- endblock %}
index a363c0e75ef433ede3469058369d949995595b50..10440594230f7c34a095319f39c8c19001a7c3ec 100644 (file)
@@ -5,32 +5,33 @@
 {%- elif motion.yes is defined %}{% if motion.yes != None and motion.no != None and motion.yes > motion.no %} alert-success{% else %} alert-danger{% endif %}
 {%- else %} bg-light{%- endif -%}
 ">
-    <span class="title-text">{{motion.name}}</span> ({{ 'Running' if motion.running else ('Canceled' if motion.canceled != None else 'Finished') }})
+    <span class="title-text">{{motion.name}}</span> ({{ _('Running') if motion.running else (_('Canceled') if motion.canceled != None else _('Finished')) }})
     <span class="motion-type">{{motion.type}}</span>
     <div># {{motion.identifier}}
 {%- if singlemotion == False %}
-    <a class="btn btn-primary" href="/motion/{{motion.identifier}}" role="button">{{ 'Vote' if motion.running else 'Result' }}</a>
+    <a class="btn btn-primary" href="/motion/{{motion.identifier}}" role="button">{{ _('Vote') if motion.running else _('Result') }}</a>
 {%- endif %}
     </div>
     <div class="date">
-      <div>Proposed: {{motion.posed|timestamp}} (UTC) by {{motion.poser}}</div>
+      <div>{{_('Proposed')}}: {{motion.posed|timestamp}} (UTC) {{_('by')}} {{motion.poser}}</div>
 {%- if motion.canceled != None %}
-      <div>Canceled: {{motion.canceled|timestamp}} (UTC) by {{motion.canceler}}</div></div>
+      <div>{{_('Canceled')}}: {{motion.canceled|timestamp}} (UTC) {{_('by')}} {{motion.canceler}}</div>
 {%- else %}
-      <div>Votes until: {{motion.deadline|timestamp}} (UTC)</div></div>
+      <div>{{_('Votes until')}}: {{motion.deadline|timestamp}} (UTC)</div>
 {%- endif %}
+     </div>
   </div>
   <div class="card-body">
     <p>{{motion.content|markdown}}</p>
 {%- if motion.yes or motion.no or motion.abstain %}
     <p>
 {%- for vote in ['yes', 'no', 'abstain'] %}
-{{vote|capitalize}} <span class="badge badge-pill badge-secondary">{{motion[vote]}}</span><br>
+{{_(vote)|capitalize}} <span class="badge badge-pill badge-secondary">{{motion[vote]}}</span><br>
 {%- endfor %}
     </p>
 {%- endif %}
 {%- if motion.canceled != None %}
-    <p>Cancelation reason: {{motion.cancelation_reason}}</p>
+    <p>{{_('Cancelation reason')}}: {{motion.cancelation_reason}}</p>
 {%- endif %}
   </div>
 {%- block content %}{% endblock %}
index d81214c5d2b15fdfcd84584d5d4cbf8f6b422190..259606e6441e67001daeeac7f5e6da8d3bef2aaf 100644 (file)
@@ -4,8 +4,8 @@
 <form action="/proxy/add" method="POST">
   <table>
   <tr>
-    <td>Voter</td>
-    <td>Proxy</td>
+    <td>{{_('Voter')}}</td>
+    <td>{{_('Proxy')}}</td>
     <td></td>
   </tr>
   <tr>
@@ -24,7 +24,7 @@
       </select>
     </td>
     <td>
-      <button type="submit" class="btn btn-primary">Add</button>
+      <button type="submit" class="btn btn-primary">{{_('Add')}}</button>
     </td>
   </tr>
   </table>
 <form action="/proxy/revoke" method="POST">
 <div class="motion card" id="votes">
   <div class="card-heading text-white bg-info">
-    Granted Proxies
+    {{_('Granted Proxies')}}
   </div>
   <div class="card-body">
     <table>
       <thead>
-        <th>Voter</th>
-        <th>Proxy</th>
+        <th>{{_('Voter')}}</th>
+        <th>{{_('Proxy')}}</th>
         <th></th>
       </thead>
       {%- for row in proxies %}
       <tr>
         <td>{{row.voter_email}}</td>
         <td>{{row.proxy_email}}</td>
-        <td><button type="submit" class="btn btn-danger" name="id" value="{{row.id}}">Revoke</button></td>
+        <td><button type="submit" class="btn btn-danger" name="id" value="{{row.id}}">{{_('Revoke')}}</button></td>
       </tr>
       {%- endfor %}
     </table>
@@ -55,7 +55,7 @@
 </form>
 {%- endif %}
 <form action="/proxy/revokeall" method="POST">
-  <button type="submit" class="btn btn-danger">Revoke ALL</button>
+  <button type="submit" class="btn btn-danger">{{_('Revoke all')}}</button>
 </form>
 </div>
 {%- endblock %}
\ No newline at end of file
index c53e4b51307bb67b66d5b7079662fa89bc8c8c29..4c853eebbd022e296030f1deb95336f38f5aa077 100644 (file)
@@ -1,17 +1,17 @@
 {% extends "base.html" %}
 {% block title -%}
-Motion: {{motion.name}}
+{{_('Motion')}}: {{motion.name}}
 {%- endblock %}
 {% block body %}
 {%- include 'motion.html' %}
 {%- if votes %}
 <div class="motion card" id="votes">
   <div class="card-heading text-white bg-info">
-    Motion Votes
+    {{_('Motion Votes')}}
   </div>
   <div class="card-body">
     {%- for row in votes %}
-    <div>{{row.email}}: {{row.result}}{%- if row.proxyemail %} : given by {{row.proxyemail}}{%- endif %}</div>
+    <div>{{row.email}}: {{row.result}}{%- if row.proxyemail %} : {_('given by')}} {{row.proxyemail}}{%- endif %}</div>
     {%- endfor %}
   </div>
 </div>
@@ -22,20 +22,20 @@ Motion: {{motion.name}}
 {%- if may_vote %}
 <div class="panel panel-info" id="votes">
   <div class="panel-body">
-<h3>My vote</h3>
+<h3>{{_('My vote')}}</h3>
 {%- if proxyname %}
-Given by {{proxyname[0][0]}}
+{{_('Given by')}} {{proxyname[0][0]}}
 {%- endif %}
 <form action="/motion/{{motion.identifier}}/vote/{{g.voter}}" method="POST">
-{%- for vote in ['yes','no','abstain'] %}
-<button type="submit" class="btn btn-{{ 'success' if vote == motion.result else 'primary' }}" name="vote" value="{{vote}}" id="vote-{{vote}}">{{vote}}</button>
+{%- for vote in ['yes', 'no', 'abstain'] %}
+<button type="submit" class="btn btn-{{ 'success' if vote == motion.result else 'primary' }}" name="vote" value="{{vote}}" id="vote-{{vote}}">{{_(vote)}}</button>
 {%- endfor %}
 </form>
 
 {%- for p in proxyvote %}
-<h3>Vote for {{p.email}}</h3>
+<h3>{{_('Vote for')}} {{p.email}}</h3>
 {%- if p.owneremail and p.result%}
-Voted by {{p.owneremail}}
+{{_('Voted by')}} {{p.owneremail}}
 {%- endif %}
 <form action="/motion/{{motion.identifier}}/vote/{{p.voter_id}}" method="POST">
 {%- for vote in ['yes','no','abstain'] %}
@@ -47,18 +47,17 @@ Voted by {{p.owneremail}}
 
 {%- if may_cancel %}
 <form action="/motion/{{motion.identifier}}/cancel" method="POST" class="form-inline">
-<input type="text" placeholder="cancelation reason" name="reason" class="form-control" required="yes">
-<button type="submit" class="btn btn-danger" name="cancel" value="cancel" id="cancel">Cancel</button></br>
+<input type="text" placeholder="{{_('Cancelation reason')}}" name="reason" class="form-control" required="yes">
+<button type="submit" class="btn btn-danger" name="cancel" value="cancel" id="cancel">{{_('Cancel')}}</button></br>
 </form>
 {%- endif %}
 {%- if may_finish %}
 <form action="/motion/{{motion.identifier}}/finish" method="POST" class="form-inline">
-<button type="submit" class="btn btn-danger" name="finish" value="finish" id="finish">Finish</button></br>
+<button type="submit" class="btn btn-danger" name="finish" value="finish" id="finish">{{_('Finish')}}</button></br>
 </form>
 {%- endif %}
-
 {%- endif %}
-<a href="/?start={{motion.id}}#motion-{{motion.id}}" class="btn btn-primary">Back</a>
+<a href="/?start={{motion.id}}#motion-{{motion.id}}" class="btn btn-primary">{{_('Back')}}</a>
 {%- endblock %}
   </div>
 </div>
index 0bee29420e123a31029dcc6922b3d1c9103a71d7..e500639bc3a006d52fec1fbcb001ce3c2dc6b80e 100644 (file)
@@ -107,41 +107,39 @@ class GeneralTests(BasicTest):
     def test_basic_results_data(self):
         result = self.app.get('/', environ_base={'USER_ROLES': user}, follow_redirects=True)
         testtext= '<div class="motion card" id="motion-3">\n  <div class="motion-title card-heading alert-warning">'\
-            + '\n    <span class=\"title-text\">Motion C</span> (Canceled)\n    <span class=\"motion-type\">group1</span>'\
+            + '\n    <span class="title-text">Motion C</span> (Canceled)\n    <span class="motion-type">group1</span>'\
             + '\n    <div># g1.20200402.003'\
             + '\n    <a class="btn btn-primary" href="/motion/g1.20200402.003" role="button">Result</a>'\
-            + '\n    </div>'\
-            + '\n    <div class=\"date\">\n      <div>Proposed: 2020-04-02 21:47:24 (UTC) by User A</div>'\
-            + '\n      <div>Canceled: 2020-04-03 21:48:24 (UTC) by User A</div></div>\n  </div>'\
-            + '\n  <div class=\"card-body\">\n    <p><p>A third motion</p></p>'\
-            + '\n    <p>\nYes <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
-            + '\nNo <span class=\"badge badge-pill badge-secondary\">0</span><br>'\
-            + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n    </p>'\
-            + '\n    <p>Cancelation reason: Entered with wrong text</p>\n  </div>\n</div>'
+            + '\n    </div>\n    <div class="date">'\
+            + '\n      <div>Proposed: 2020-04-02 21:47:24 (UTC) by User A</div>'\
+            + '\n      <div>Canceled: 2020-04-03 21:48:24 (UTC) by User A</div>\n     </div>\n  </div>'\
+            + '\n  <div class="card-body">\n    <p><p>A third motion</p></p>'\
+            + '\n    <p>\nYes <span class="badge badge-pill badge-secondary">1</span><br>'\
+            + '\nNo <span class="badge badge-pill badge-secondary">0</span><br>'\
+            + '\nAbstain <span class="badge badge-pill badge-secondary">0</span><br>\n    </p>'\
+            + '\n    <p>Cancelation reason: Entered with wrong text</p>\n  </div>\n</div>\n'
         self.assertIn(str.encode(testtext), result.data)
         testtext= '<div class="motion card" id="motion-2">\n  <div class="motion-title card-heading alert-danger">'\
-            + '\n    <span class=\"title-text\">Motion B</span> (Finished)\n    <span class=\"motion-type\">group1</span>'\
+            + '\n    <span class="title-text">Motion B</span> (Finished)\n    <span class="motion-type">group1</span>'\
             + '\n    <div># g1.20200402.002'\
             + '\n    <a class="btn btn-primary" href="/motion/g1.20200402.002" role="button">Result</a>'\
-            + '\n    </div>'\
-            + '\n    <div class=\"date\">\n      <div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>'\
-            + '\n      <div>Votes until: 2020-04-04 21:41:26 (UTC)</div></div>\n  </div>'\
-            + '\n  <div class=\"card-body\">\n    <p><p>A second motion</p></p>'\
-            + '\n    <p>\nYes <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
-            + '\nNo <span class=\"badge badge-pill badge-secondary\">2</span><br>'\
-            + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n    </p>\n  </div>\n</div>\n'
-        self.assertIn(str.encode(testtext), result.data)
-        testtext= '<div class=\"motion card\" id=\"motion-1\">\n  <div class=\"motion-title card-heading alert-success\">'\
-            + '\n    <span class=\"title-text\">Motion A</span> (Finished)\n    <span class=\"motion-type\">group1</span>'\
+            + '\n    </div>\n    <div class="date">\n      <div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>'\
+            + '\n      <div>Votes until: 2020-04-04 21:41:26 (UTC)</div>\n     </div>\n  </div>'\
+            + '\n  <div class="card-body">\n    <p><p>A second motion</p></p>\n    <p>'\
+            + '\nYes <span class="badge badge-pill badge-secondary">1</span><br>'\
+            + '\nNo <span class="badge badge-pill badge-secondary">2</span><br>'\
+            + '\nAbstain <span class="badge badge-pill badge-secondary">0</span><br>\n    </p>\n  </div>\n</div>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<div class="motion card" id="motion-1">\n  <div class="motion-title card-heading alert-success">'\
+            + '\n    <span class="title-text">Motion A</span> (Finished)\n    <span class="motion-type">group1</span>'\
             + '\n    <div># g1.20200402.001'\
             + '\n    <a class="btn btn-primary" href="/motion/g1.20200402.001" role="button">Result</a>'\
-            + '\n    </div>'\
-            + '\n    <div class=\"date">\n      <div>Proposed: 2020-04-02 21:40:33 (UTC) by User A</div>'\
-            + '\n      <div>Votes until: 2020-04-02 21:40:33 (UTC)</div></div>\n  </div>'\
-            + '\n  <div class=\"card-body\">\n    <p><p>My special motion</p></p>'\
-            + '\n    <p>\nYes <span class=\"badge badge-pill badge-secondary\">2</span><br>'\
-            + '\nNo <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
-            + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n    </p>\n  </div>\n</div>\n</div>'
+            + '\n    </div>\n    <div class="date">\n      <div>Proposed: 2020-04-02 21:40:33 (UTC) by User A</div>'\
+            + '\n      <div>Votes until: 2020-04-02 21:40:33 (UTC)</div>\n     </div>\n  </div>'\
+            + '\n  <div class="card-body">\n    <p><p>My special motion</p></p>\n    <p>'\
+            + '\nYes <span class="badge badge-pill badge-secondary">2</span><br>'\
+            + '\nNo <span class="badge badge-pill badge-secondary">1</span><br>'\
+            + '\nAbstain <span class="badge badge-pill badge-secondary">0</span><br>\n    </p>\n  </div>\n</div>\n'
         self.assertIn(str.encode(testtext), result.data)
         testtext= 'Proxy management'
         self.assertNotIn(str.encode(testtext), result.data)
@@ -347,8 +345,8 @@ class VoterTests(BasicTest):
     def test_see_old_vote(self):
         motion='g1.20200402.002'
         result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
-        testtext= '<div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>\n      <div>Votes until: 2020-04-04 21:41:26 (UTC)</div></div>'\
-            + '\n  </div>\n  <div class="card-body">\n    <p><p>A second motion</p></p>\n  </div>\n</div>'\
+        testtext= '<div>Proposed: 2020-04-02 21:41:26 (UTC) by User A</div>\n      <div>Votes until: 2020-04-04 21:41:26 (UTC)</div>'\
+            + '\n     </div>\n  </div>\n  <div class="card-body">\n    <p><p>A second motion</p></p>\n  </div>\n</div>'\
             + '\n<a href="/?start=2#motion-2" class="btn btn-primary">Back</a>'
         self.assertIn(str.encode(testtext), result.data)
 
diff --git a/translations/babel.cfg b/translations/babel.cfg
new file mode 100644 (file)
index 0000000..cd2157f
--- /dev/null
@@ -0,0 +1,4 @@
+[python: **.py]
+[jinja2: **/templates/**.html]
+encoding = utf-8
+extensions=jinja2.ext.autoescape,jinja2.ext.with_