# emacs
\#*\#
*~
+messages.pot
+messages.mo
+messages.po
+++ /dev/null
-[python: **.py]
-[jinja2: **/templates/**.html]
-encoding = utf-8
-extensions=jinja2.ext.autoescape,jinja2.ext.with_
from flaskext.markdown import Markdown
from markdown.extensions import Extension
from datetime import date, time, datetime
+from flask_language import Language, current_language
import gettext
def get_db():
app = Flask(__name__)
app.register_blueprint(filters.blueprint)
babel = Babel(app)
+lang = Language(app)
gettext.install('motion')
class EscapeHtml(Extension):
@babel.localeselector
def get_locale():
- return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
+ return str(current_language)
+
+@lang.allowed_languages
+def get_allowed_languages():
+ return app.config['LANGUAGES'].keys()
+
+@lang.default_language
+def get_default_language():
+ return 'en'
+
+def get_languages():
+ return app.config['LANGUAGES']
+
+# Manually add vote options to the translation strings. They are used as keys in loops.
+TRANSLATION_STRINGS={_('yes'), _('no'), _('abstain')}
@app.before_request
def lookup_user():
roles = env.get("ROLES")
if user is None:
- return "Server misconfigured", 500
+ return _('Server misconfigured'), 500
roles = roles.split(" ")
if user == "<invalid>":
- return "Access denied", 403;
+ return _('Access denied'), 403;
db = get_db()
with db.xact():
else:
prev = -1
return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
- categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"))
+ categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
def rel_redirect(loc):
r = redirect(loc)
if may("audit", resultmotion[0].get("type")) and not resultmotion[0].get("running") and not resultmotion[0].get("canceled"):
votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
votes = get_db().prepare("SELECT vote.result, voter.email, CASE voter.email WHEN proxy.email THEN NULL ELSE proxy.email END as proxyemail FROM vote INNER JOIN voter ON voter.id = vote.voter_id INNER JOIN voter as proxy ON proxy.id = vote.proxy_id WHERE vote.motion_id=$1")(resultmotion[0].get("id"));
- return render_template('single_motion.html', motion=resultmotion[0], may_vote=may("vote", resultmotion[0].get("type")), may_cancel=may("cancel", resultmotion[0].get("type")), votes=votes, proxyvote=resultproxyvote, proxyname=resultproxyname)
+ return render_template('single_motion.html', motion=resultmotion[0], may_vote=may("vote", resultmotion[0].get("type")), may_cancel=may("cancel", resultmotion[0].get("type")), votes=votes, proxyvote=resultproxyvote, proxyname=resultproxyname, languages=get_languages())
@app.route("/motion/<string:motion>/vote/<string:voter>", methods=['POST'])
@validate_motion_access_vote('vote')
def proxy():
if not may_admin("proxyadmin"):
return _('Forbidden'), 403
- return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"))
+ return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"), languages=get_languages())
@app.route("/proxy/add", methods=['POST'])
def add_proxy():
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 '%s' reached." % (proxy), 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")
rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
return rel_redirect("/proxy")
+@app.route("/language/<string:language>")
+def set_language(language):
+ lang.change_language(language)
+ return rel_redirect("/")
<head>
<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">
+<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
+<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<style type="text/css">
.form-inline .motion {
width: 100%;
{%- endif %}
</a>
</li>
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{_('Language')}}
+ </a>
+ <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
+ {%- for k, v in languages.items() %}
+ <a class="dropdown-item" href="/language/{{k}}">{{v}}</a>
+ {%- endfor %}
+ </div>
+ </li>
</ul>
</div>
</nav>
{%- endif %}
</div>
<div class="date">
- <div>{{_('Proposed')}}: {{motion.posed|timestamp}} (UTC) {{_('by')}} {{motion.poser}}</div>
+ <div>{{_('Proposed')}}: {{_('%(dt)s (UTC) by %(user)s', dt=motion.posed|timestamp, user=motion.poser)}}</div>
{%- if motion.canceled != None %}
- <div>{{_('Canceled')}}: {{motion.canceled|timestamp}} (UTC) {{_('by')}} {{motion.canceler}}</div>
+ <div>{{_('Canceled')}}: {{_('%(dt)s (UTC) by %(user)s', dt=motion.canceled|timestamp, user=motion.poser)}}</div>
{%- else %}
- <div>{{_('Votes until')}}: {{motion.deadline|timestamp}} (UTC)</div>
+ <div>{{_('Votes until')}}: {{_('%(dt)s (UTC)', dt=motion.deadline|timestamp)}}</div>
{%- endif %}
</div>
</div>
<div class="panel-body">
<h3>{{_('My vote')}}</h3>
{%- if proxyname %}
-{{_('Given by')}} {{proxyname[0][0]}}
+{{_('Given by %(pn)s', pn=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>
+<button type="submit" class="btn btn-{{ 'success' if vote == motion.result else 'primary' }}" name="vote" value="{{vote}}" id="vote-{{vote}}">{{_(vote)|capitalize}}</button>
{%- endfor %}
</form>
{%- for p in proxyvote %}
-<h3>{{_('Vote for')}} {{p.email}}</h3>
+<h3>{{_('Vote for %(email)s', email=p.email)}}</h3>
{%- if p.owneremail and p.result%}
-{{_('Voted by')}} {{p.owneremail}}
+{{_('Voted by %(email)s', email=p.owneremail)}}
{%- endif %}
<form action="/motion/{{motion.identifier}}/vote/{{p.voter_id}}" method="POST">
-{%- for vote in ['yes','no','abstain'] %}
-<button type="submit" class="btn btn-{{ 'success' if vote == p.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 == p.result else 'primary' }}" name="vote" value="{{vote}}" id="vote-{{vote}}">{{_(vote)|capitalize}}</button>
{%- endfor %}
</form>
{%- endfor %}
result = self.app.get('/', environ_base={'USER_ROLES': user})
resulttext=self.buildResultText('A fourth motion', 1, 0, 0)
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= 'class=\"btn btn-success\" name=\"vote\" value="yes" id="vote-yes">yes</button>'
+ testtext= 'class=\"btn btn-success\" name=\"vote\" value="yes" id="vote-yes">Yes</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">no</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">No</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">abstain</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">Abstain</button>'
self.assertIn(str.encode(testtext), result.data)
def test_vote_no(self):
resulttext=self.buildResultText('A fourth motion', 0, 1, 0)
self.assertIn(str.encode(resulttext), result.data)
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= 'class="btn btn-primary" name="vote\" value=\"yes\" id=\"vote-yes\">yes</button>'
+ testtext= 'class="btn btn-primary" name="vote\" value=\"yes\" id=\"vote-yes\">Yes</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"no\" id=\"vote-no\">no</button>'
+ testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"no\" id=\"vote-no\">No</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">abstain</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">Abstain</button>'
self.assertIn(str.encode(testtext), result.data)
def test_vote_abstain(self):
resulttext=self.buildResultText('A fourth motion', 0, 0, 1)
self.assertIn(str.encode(resulttext), result.data)
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"yes\" id=\"vote-yes\">yes</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"yes\" id=\"vote-yes\">Yes</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">no</button>'
+ testtext= 'class=\"btn btn-primary\" name=\"vote\" value=\"no\" id=\"vote-no\">No</button>'
self.assertIn(str.encode(testtext), result.data)
- testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">abstain</button>'
+ testtext= 'class=\"btn btn-success\" name=\"vote\" value=\"abstain\" id=\"vote-abstain\">Abstain</button>'
self.assertIn(str.encode(testtext), result.data)
def test_vote_change(self):
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
# own vote without change
testtext= '<form action="/motion/g1.20200402.004/vote/4" method="POST">\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">yes</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">no</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>'
+ + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>'
self.assertIn(str.encode(testtext), result.data)
# proxy vote with change
testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
- + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">yes</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">no</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>\n'
+ + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>\n'
self.assertIn(str.encode(testtext), result.data)
# User B view
# own vote without change
testtext= '<h3>My vote</h3>\nGiven by testuser\n'\
+ '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
- + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">yes</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">no</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>'
+ + '<button type="submit" class="btn btn-success" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="no" id="vote-no">No</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>'
self.assertIn(str.encode(testtext), result.data)
# change vote
result = self.app.get('/motion/' + motion, environ_base={'USER_ROLES': user}, follow_redirects=True)
testtext= '<form action="/motion/g1.20200402.004/vote/2" method="POST">\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">yes</button>\n'\
- + '<button type="submit" class="btn btn-success" name="vote" value="no" id="vote-no">no</button>\n'\
- + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">abstain</button>\n</form>\n'
+ + '<button type="submit" class="btn btn-primary" name="vote" value="yes" id="vote-yes">Yes</button>\n'\
+ + '<button type="submit" class="btn btn-success" name="vote" value="no" id="vote-no">No</button>\n'\
+ + '<button type="submit" class="btn btn-primary" name="vote" value="abstain" id="vote-abstain">Abstain</button>\n</form>\n'
self.assertIn(str.encode(testtext), result.data)
def test_proxy_vote_no_proxy(self):
# Translation files for WPIA motion tool
+Add the needed translation files here.
+
The software is translated via Transifex: https://www.transifex.com/wpia/motion/.
The current files are also available in the motion-translations repository on our gitlab system https://git.ccs-baumann.de/wpia/motion-translations.
+
+To extract the translation files use:
+
+```
+pybabel extract -F ./translations/babel.cfg -k _l -o ./translations/messages.pot --input-dirs=.
+```
+
+To create a language file use e.g. de
+
+```
+pybabel init -i ./translations/messages.pot -d translations -l de
+```
+
+The translation files are maintained in a repo and translated on Transifex [https://www.transifex.com/wpia/landingpage-1/dashboard/](https://www.transifex.com/wpia/landingpage-1/dashboard/).
+
+The translation file are stored in this files structure translations/XX/LC_MESSAGES/messages.po with XX as language code.
+
+To compile the translated text from *.po to *.mo use:
+
+```
+pybabel compile -f -d translations
+```