From d0566bb03ec3b61ec1a160919e9f18b5e99ddc3a Mon Sep 17 00:00:00 2001
From: sistason <c3infra@sistason.de>
Date: Sun, 25 Jul 2021 07:32:37 +0000
Subject: [PATCH] Multi-language support und German translations

---
 .gitignore                                    |   1 -
 requirements.txt                              |   1 +
 uffd/__init__.py                              |  20 +
 uffd/babel.cfg                                |   3 +
 uffd/default_config.cfg                       |   5 +
 uffd/mfa/templates/mfa/auth.html              |  16 +-
 uffd/mfa/templates/mfa/disable.html           |   8 +-
 uffd/mfa/templates/mfa/setup.html             |  96 ++-
 uffd/mfa/templates/mfa/setup_recovery.html    |  18 +-
 uffd/mfa/templates/mfa/setup_totp.html        |  27 +-
 uffd/mfa/views.py                             |  17 +-
 uffd/role/templates/role/list.html            |   8 +-
 uffd/role/templates/role/show.html            |  50 +-
 uffd/role/views.py                            |   7 +-
 .../selfservice/forgot_password.html          |   8 +-
 .../templates/selfservice/self.html           |  30 +-
 .../templates/selfservice/set_password.html   |  10 +-
 uffd/selfservice/views.py                     |  15 +-
 .../services/templates/services/overview.html |   6 +-
 uffd/services/views.py                        |  25 +-
 uffd/session/templates/session/login.html     |  10 +-
 uffd/session/views.py                         |  15 +-
 uffd/templates/base.html                      |  29 +-
 uffd/translations/README.md                   |  21 +
 uffd/translations/de/LC_MESSAGES/messages.mo  | Bin 0 -> 14883 bytes
 uffd/translations/de/LC_MESSAGES/messages.po  | 761 ++++++++++++++++++
 uffd/user/templates/group/list.html           |   6 +-
 uffd/user/templates/group/show.html           |   6 +-
 uffd/user/templates/user/list.html            |  22 +-
 uffd/user/templates/user/show.html            |  44 +-
 uffd/user/views_group.py                      |   5 +-
 uffd/user/views_user.py                       |  19 +-
 32 files changed, 1097 insertions(+), 212 deletions(-)
 create mode 100644 uffd/babel.cfg
 create mode 100644 uffd/translations/README.md
 create mode 100644 uffd/translations/de/LC_MESSAGES/messages.mo
 create mode 100644 uffd/translations/de/LC_MESSAGES/messages.po

diff --git a/.gitignore b/.gitignore
index aebed19b..04263972 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,7 +52,6 @@ coverage.xml
 .pytest_cache/
 
 # Translations
-*.mo
 *.pot
 
 # Django stuff:
diff --git a/requirements.txt b/requirements.txt
index ff3e37c1..f5d04323 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ qrcode==6.1
 fido2==0.5.0
 Flask-OAuthlib==0.9.5
 Flask-Migrate==2.1.1
+Flask-Babel==0.11.2
 alembic==1.0.0
 
 # The main dependencies on their own lead to version collisions and pip is
diff --git a/uffd/__init__.py b/uffd/__init__.py
index 3a7472c2..4348fd20 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -3,6 +3,7 @@ import secrets
 import sys
 
 from flask import Flask, redirect, url_for, request
+from flask_babel import Babel
 from werkzeug.routing import IntegerConverter
 from werkzeug.serving import make_ssl_devcert
 from werkzeug.contrib.profiler import ProfilerMiddleware
@@ -93,6 +94,13 @@ def create_app(test_config=None): # pylint: disable=too-many-locals
 	def index(): #pylint: disable=unused-variable
 		return redirect(url_for('selfservice.index'))
 
+	@app.route('/lang', methods=['POST'])
+	def setlang(): #pylint: disable=unused-variable
+		resp = redirect(request.values.get('ref', '/'))
+		if 'lang' in request.values:
+			resp.set_cookie('language', request.values['lang'])
+		return resp
+
 	@app.teardown_request
 	def close_connection(exception): #pylint: disable=unused-variable,unused-argument
 		if hasattr(request, "ldap_connection"):
@@ -116,4 +124,16 @@ def create_app(test_config=None): # pylint: disable=too-many-locals
 		app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
 		app.run(debug=True)
 
+	babel = Babel(app)
+
+	@babel.localeselector
+	def get_locale(): #pylint: disable=unused-variable
+		language_cookie = request.cookies.get('language')
+		if language_cookie is not None:
+			return language_cookie
+		languages = list(map(lambda x: x.get('value'), app.config['LANGUAGES']))
+		return request.accept_languages.best_match(languages)
+
+	app.add_template_global(get_locale)
+
 	return app
diff --git a/uffd/babel.cfg b/uffd/babel.cfg
new file mode 100644
index 00000000..f0234b32
--- /dev/null
+++ b/uffd/babel.cfg
@@ -0,0 +1,3 @@
+[python: **.py]
+[jinja2: **/templates/**.html]
+extensions=jinja2.ext.autoescape,jinja2.ext.with_
diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index 61a93cd1..ad90801e 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -53,6 +53,11 @@ SESSION_COOKIE_SECURE=True
 SESSION_COOKIE_HTTPONLY=True
 SESSION_COOKIE_SAMESITE='Strict'
 
+LANGUAGES=[
+    { "value": "de", "display": "DE" },
+    { "value": "en", "display": "EN" }
+]
+
 ACL_ADMIN_GROUP="uffd_admin"
 ACL_SELFSERVICE_GROUP="uffd_access"
 # Members can create invite links for signup
diff --git a/uffd/mfa/templates/mfa/auth.html b/uffd/mfa/templates/mfa/auth.html
index 6b8afbfb..c17e99f6 100644
--- a/uffd/mfa/templates/mfa/auth.html
+++ b/uffd/mfa/templates/mfa/auth.html
@@ -9,34 +9,34 @@
 			<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
 		</div>
 		<div class="col-12 mb-3">
-			<h2 class="text-center">Two-Factor Authentication</h2>
+			<h2 class="text-center">{{_("Two-Factor Authentication")}}</h2>
 		</div>
 		{% if request.user_pre_mfa.mfa_webauthn_methods %}
 		<noscript>
 			<div class="form-group col-12">
-				<div id="webauthn-nojs" class="alert alert-warning" role="alert">Enable javascript for authentication with U2F/FIDO2 devices</div>
+				<div id="webauthn-nojs" class="alert alert-warning" role="alert">{{_("Enable javascript for authentication with U2F/FIDO2 devices")}}</div>
 			</div>
 		</noscript>
 		<div id="webauthn-unsupported" class="form-group col-12 d-none">
-			<div class="alert alert-warning" role="alert">Authentication with U2F/FIDO2 devices is not supported by your browser</div>
+			<div class="alert alert-warning" role="alert">{{_("Authentication with U2F/FIDO2 devices is not supported by your browser")}}</div>
 		</div>
 		<div class="form-group col-12 d-none webauthn-group">
 			<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
 			<button type="button" id="webauthn-btn" class="btn btn-primary btn-block">
 				<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
-				<span id="webauthn-btn-text">Authenticate with U2F/FIDO2 device</span>
+				<span id="webauthn-btn-text">{{_("Authenticate with U2F/FIDO2 device")}}</span>
 			</button>
 		</div>
-		<div class="text-center text-muted d-none webauthn-group mb-3">- or -</div>
+		<div class="text-center text-muted d-none webauthn-group mb-3">- {{_("or")}} -</div>
 		{% endif %}
 		<div class="form-group col-12 mb-2">
-			<input type="text" class="form-control" id="mfa-code" name="code" required="required" placeholder="Code from your authenticator app or recovery code" autocomplete="off" autofocus>
+			<input type="text" class="form-control" id="mfa-code" name="code" required="required" placeholder="{{_("Code from your authenticator app or recovery code")}}" autocomplete="off" autofocus>
 		</div>
 		<div class="form-group col-12">
-			<button type="submit" class="btn btn-primary btn-block">Verify</button>
+			<button type="submit" class="btn btn-primary btn-block">{{_("Verify")}}</button>
 		</div>
 		<div class="form-group col-12">
-			<a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">Cancel</a>
+			<a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">{{_("Cancel")}}</a>
 		</div>
 	</div>
 </div>
diff --git a/uffd/mfa/templates/mfa/disable.html b/uffd/mfa/templates/mfa/disable.html
index 679ff1ba..d577b73b 100644
--- a/uffd/mfa/templates/mfa/disable.html
+++ b/uffd/mfa/templates/mfa/disable.html
@@ -2,11 +2,13 @@
 
 {% block body %}
 
-<p>When you proceed, all recovery codes, registered authenticator applications and devices will be invalidated. 
-You can later generate new recovery codes and setup your applications and devices again.</p>
+<p>
+	{{_("When you proceed, all recovery codes, registered authenticator applications and devices will be invalidated.
+	You can later generate new recovery codes and setup your applications and devices again.")}}
+</p>
 
 <form class="form" action="{{ url_for('mfa.disable_confirm') }}" method="POST">
-	<button type="submit" class="btn btn-danger btn-block">Disable two-factor authentication</button>
+	<button type="submit" class="btn btn-danger btn-block">{{_("Disable two-factor authentication")}}</button>
 </form>
 
 {% endblock %}
diff --git a/uffd/mfa/templates/mfa/setup.html b/uffd/mfa/templates/mfa/setup.html
index 707ebb5a..04b90a4a 100644
--- a/uffd/mfa/templates/mfa/setup.html
+++ b/uffd/mfa/templates/mfa/setup.html
@@ -13,22 +13,27 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
 {% set mfa_setup = request.user.mfa_recovery_codes and not mfa_enabled %}
 
 {% block body %}
-<p>Two-factor authentication is currently <strong>{{ 'enabled' if mfa_enabled else 'disabled' }}</strong>.
+<p>
+	{% if mfa_enabled %}
+	{{ _("Two-factor authentication is currently <strong>enabled</strong>.")|safe }}
+	{% else %}
+	{{ _("Two-factor authentication is currently <strong>disabled</strong>.")|safe }}
+	{% endif %}
 {% if mfa_init %}
-You need to generate recovery codes and setup at least one authentication method to enable two-factor authentication.
+	{{_("You need to generate recovery codes and setup at least one authentication method to enable two-factor authentication.")}}
 {% elif mfa_setup %}
-You need to setup at least one authentication method to enable two-factor authentication.
+	{{_("You need to setup at least one authentication method to enable two-factor authentication.")}}
 {% endif %}
 </p>
 {% if mfa_setup or mfa_enabled %}
 <div class="clearfix">
 	{% if mfa_enabled %}
 	<form class="form float-right" action="{{ url_for('mfa.disable') }}">
-		<button type="submit" class="btn btn-danger mb-2">Disable two-factor authentication</button>
+		<button type="submit" class="btn btn-danger mb-2">{{_("Disable two-factor authentication")}}</button>
 	</form>
 	{% else %}
 	<form class="form float-right" action="{{ url_for('mfa.disable_confirm') }}" method="POST">
-		<button type="submit" class="btn btn-light mb-2">Reset two-factor configuration</button>
+		<button type="submit" class="btn btn-light mb-2">{{_("Reset two-factor configuration")}}</button>
 	</form>
 	{% endif %}
 </div>
@@ -38,29 +43,37 @@ You need to setup at least one authentication method to enable two-factor authen
 
 <div class="row mt-3">
 	<div class="col-12 col-md-5">
-		<h4>Recovery Codes</h4>
-		<p>Recovery codes allow you to login and setup new two-factor methods when you lost your registered second factor.</p>
+		<h4>{{_("Recovery Codes")}}</h4>
+		<p>
+			{{_("Recovery codes allow you to login and setup new two-factor methods when you lost your registered second factor.")}}
+		</p>
 		<p>
 			{% if mfa_init %}<strong>{% endif %}
-			You need to setup recovery codes before you can setup up authenticator apps or U2F/FIDO2 devices.
+			{{_("You need to setup recovery codes before you can setup up authenticator apps or U2F/FIDO2 devices.")}}
 			{% if mfa_init %}</strong>{% endif %}
-			Each code can only be used once.
+			{{_("Each code can only be used once.")}}
 		</p>
 	</div>
 
 	<div class="col-12 col-md-7">
 		<form class="form" action="{{ url_for('mfa.setup_recovery') }}" method="POST">
 			{% if mfa_init %}
-			<button type="submit" class="btn btn-primary mb-2 col">Generate recovery codes to enable two-factor authentication</button>
+			<button type="submit" class="btn btn-primary mb-2 col">
+				{{_("Generate recovery codes to enable two-factor authentication")}}
+			</button>
 			{% else %}
-			<button type="submit" class="btn btn-primary mb-2 col">Generate new recovery codes</button>
+			<button type="submit" class="btn btn-primary mb-2 col">
+				{{_("Generate new recovery codes")}}
+			</button>
 			{% endif %}
 		</form>
 
 		{% if request.user.mfa_recovery_codes %}
 		<p>{{ request.user.mfa_recovery_codes|length }} recovery codes remain</p>
 		{% elif not request.user.mfa_recovery_codes and mfa_enabled %}
-		<p><strong>You have no remaining recovery codes.</strong></p>
+		<p>
+			<strong>{{_("You have no remaining recovery codes.")}}</strong>
+		</p>
 		{% endif %}
 	</div>
 </div>
@@ -69,26 +82,30 @@ You need to setup at least one authentication method to enable two-factor authen
 
 <div class="row mt-3">
 	<div class="col-12 col-md-5">
-		<h4>Authenticator Apps (TOTP)</h4>
-		<p>Use an authenticator application on your mobile device as a second factor.</p>
-		<p>The authenticator app generates a 6-digit one-time code each time you login.
-		Compatible apps are freely available for most phones.</p>
+		<h4>{{_("Authenticator Apps (TOTP)")}}</h4>
+		<p>
+			{{_("Use an authenticator application on your mobile device as a second factor.")}}
+		</p>
+		<p>
+			{{_("The authenticator app generates a 6-digit one-time code each time you login.
+			Compatible apps are freely available for most phones.")}}
+		</p>
 	</div>
 
 	<div class="col-12 col-md-7">
 		<form class="form mb-2" action="{{ url_for('mfa.setup_totp') }}">
 			<div class="row m-0">
-				<label class="sr-only" for="totp-name">Name</label>
-				<input type="text" name="name" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="totp-name" placeholder="Name" required {{ 'disabled' if mfa_init }}>
-				<button type="submit" id="totp-submit" class="btn btn-primary mb-2 col" {{ 'disabled' if mfa_init }}>Setup new app</button>
+				<label class="sr-only" for="totp-name">{{_("Name")}}</label>
+				<input type="text" name="name" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="totp-name" placeholder="{{_("Name")}}" required {{ 'disabled' if mfa_init }}>
+				<button type="submit" id="totp-submit" class="btn btn-primary mb-2 col" {{ 'disabled' if mfa_init }}>{{_("Setup new app")}}</button>
 			</div>
 		</form>
 
 		<table class="table">
 			<thead>
 				<tr>
-					<th scope="col">Name</th>
-					<th scope="col">Registered On</th>
+					<th scope="col">{{_("Name")}}</th>
+					<th scope="col">{{_("Registered On")}}</th>
 					<th scope="col"></th>
 				</tr>
 			</thead>
@@ -97,12 +114,12 @@ You need to setup at least one authentication method to enable two-factor authen
 				<tr>
 					<td>{{ method.name }}</td>
 					<td>{{ method.created.strftime('%b %d, %Y') }}</td>
-					<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td>
+					<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">{{_("Delete")}}</a></td>
 				</tr>
 				{% endfor %}
 				{% if not request.user.mfa_totp_methods %}
 				<tr class="table-secondary">
-					<td colspan=3 class="text-center">No authenticator apps registered yet</td>
+					<td colspan=3 class="text-center">{{_("No authenticator apps registered yet")}}</td>
 				</tr>
 				{% endif %}
 			</tbody>
@@ -114,27 +131,34 @@ You need to setup at least one authentication method to enable two-factor authen
 
 <div class="row">
 	<div class="col-12 col-md-5">
-		<h4>U2F and FIDO2 Devices</h4>
-		<p>Use an U2F or FIDO2 compatible hardware security key as a second factor.</p>
-		<p>U2F and FIDO2 devices are not supported by all browsers and can be particularly difficult to use on mobile devices.
-		<strong>It is strongly recommended to also setup an authenticator app</strong> to be able to login on all browsers.</p>
+		<h4>{{_("U2F and FIDO2 Devices")}}</h4>
+		<p>
+			{{_("Use an U2F or FIDO2 compatible hardware security key as a second factor.")}}
+		</p>
+		<p>
+			{{_("U2F and FIDO2 devices are not supported by all browsers and can be particularly difficult to use on mobile
+			devices. <strong>It is strongly recommended to also setup an authenticator app</strong> to be able to login on all
+			browsers.")}}
+		</p>
 	</div>
 
 	<div class="col-12 col-md-7">
 		{% if not webauthn_supported %}
-		<div class="alert alert-warning" role="alert">U2F/FIDO2 support not enabled</div>
+		<div class="alert alert-warning" role="alert">{{_("U2F/FIDO2 support not enabled")}}</div>
 		{% endif %}
 		<noscript>
-			<div class="alert alert-warning" role="alert">Enable javascript in your browser to use U2F and FIDO2 devices!</div>
+			<div class="alert alert-warning" role="alert">
+				{{_("Enable javascript in your browser to use U2F and FIDO2 devices!")}}
+			</div>
 		</noscript>
 		<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
 		<form id="webauthn-form" class="form mb-2">
 			<div class="row m-0">
-				<label class="sr-only" for="webauthn-name">Name</label>
-				<input type="text" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="webauthn-name" placeholder="Name" required disabled>
+				<label class="sr-only" for="webauthn-name">{{_("Name")}}</label>
+				<input type="text" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="webauthn-name" placeholder="{{_("Name")}}" required disabled>
 				<button type="submit" id="webauthn-btn" class="btn btn-primary mb-2 col" disabled>
 					<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
-					<span id="webauthn-btn-text">Setup new device</span>
+					<span id="webauthn-btn-text">{{_("Setup new device")}}</span>
 				</button>
 			</div>
 		</form>
@@ -142,8 +166,8 @@ You need to setup at least one authentication method to enable two-factor authen
 		<table class="table">
 			<thead>
 				<tr>
-					<th scope="col">Name</th>
-					<th scope="col">Registered On</th>
+					<th scope="col">{{_("Name")}}</th>
+					<th scope="col">{{_("Registered On")}}</th>
 					<th scope="col"></th>
 				</tr>
 			</thead>
@@ -152,12 +176,12 @@ You need to setup at least one authentication method to enable two-factor authen
 				<tr>
 					<td>{{ method.name }}</td>
 					<td>{{ method.created.strftime('%b %d, %Y') }}</td>
-					<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">Delete</a></td>
+					<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">{{_("Delete")}}</a></td>
 				</tr>
 				{% endfor %}
 				{% if not request.user.mfa_webauthn_methods %}
 				<tr class="table-secondary">
-					<td colspan=3 class="text-center">No U2F/FIDO2 devices registered yet</td>
+					<td colspan=3 class="text-center">{{_("No U2F/FIDO2 devices registered yet")}}</td>
 				</tr>
 				{% endif %}
 			</tbody>
diff --git a/uffd/mfa/templates/mfa/setup_recovery.html b/uffd/mfa/templates/mfa/setup_recovery.html
index b5e00467..a4dbf9cf 100644
--- a/uffd/mfa/templates/mfa/setup_recovery.html
+++ b/uffd/mfa/templates/mfa/setup_recovery.html
@@ -2,9 +2,12 @@
 
 {% block body %}
 
-<h1 class="d-none d-print-block">Recovery Codes</h1>
+<h1 class="d-none d-print-block">{{_("Recovery Codes")}}</h1>
 
-<p>Recovery codes allow you to login when you lose access to your authenticator app or U2F/FIDO device. Each code can only be used once.</p>
+<p>
+	{{_("Recovery codes allow you to login when you lose access to your authenticator app or U2F/FIDO device. Each code can
+	only be used once.")}}
+</p>
 
 <div class="text-monospace">
 	<ul>
@@ -14,12 +17,17 @@
 	</ul>
 </div>
 
-<p>These are your new recovery codes. Make sure to store them in a safe place or you risk losing access to your account. All previous recovery codes are now invalid.</p>
+<p>
+	{{_("These are your new recovery codes. Make sure to store them in a safe place or you risk losing access to your
+	account. All previous recovery codes are now invalid.")}}
+</p>
 
 <div class="btn-toolbar">
 	<a class="ml-auto mb-2 btn btn-primary d-print-none" href="{{ url_for('mfa.setup') }}">Continue</a>
-	<a class="ml-2 mb-2 btn btn-light d-print-none" href="{{ methods|map(attribute='code')|join('\n')|datauri }}" download="uffd-recovery-codes">Download codes</a>
-	<button class="ml-2 mb-2 btn btn-light d-print-none" type="button" onClick="window.print()">Print codes</button>
+	<a class="ml-2 mb-2 btn btn-light d-print-none" href="{{ methods|map(attribute='code')|join('\n')|datauri }}" download="uffd-recovery-codes">
+		{{_("Download codes")}}
+	</a>
+	<button class="ml-2 mb-2 btn btn-light d-print-none" type="button" onClick="window.print()">{{_("Print codes")}}</button>
 </div>
 
 {% endblock %}
diff --git a/uffd/mfa/templates/mfa/setup_totp.html b/uffd/mfa/templates/mfa/setup_totp.html
index f0850914..d4d44702 100644
--- a/uffd/mfa/templates/mfa/setup_totp.html
+++ b/uffd/mfa/templates/mfa/setup_totp.html
@@ -2,7 +2,10 @@
 
 {% block body %}
 
-<p>Install an authenticator application on your mobile device like FreeOTP or Google Authenticator and scan this QR code. On Apple devices you can use an app called "Authenticator".</p>
+<p>
+	{{_("Install an authenticator application on your mobile device like FreeOTP or Google Authenticator and scan this QR
+	code. On Apple devices you can use an app called \"Authenticator\".")}}
+</p>
 
 <div class="row">
 	<div class="mx-auto col-9 col-md-4 mb-3">
@@ -11,15 +14,19 @@
 		</a>
 	</div>
 	<div class="col-12 col-md-8">
-		<p>If you are on your mobile device and cannot scan the code, you can click on it to open it with your authenticator app. If that does not work, enter the following details manually into your authenticator app:</p>
 		<p>
-		Issuer: {{ method.issuer }}<br>
-		Account: {{ method.accountname }}<br>
-		Secret: {{ method.key }}<br>
-		Type: TOTP (time-based)<br>
-		Digits: 6<br>
-		Hash algorithm: SHA1<br>
-		Interval/period: 30 seconds
+			{{_("If you are on your mobile device and cannot scan the code, you can click on it to open it with your
+			authenticator app. If that does not work, enter the following details manually into your authenticator
+			app:")}}
+		</p>
+		<p>
+			{{_("Issuer")}}: {{ method.issuer }}<br>
+			{{_("Account")}}: {{ method.accountname }}<br>
+			{{_("Secret")}}: {{ method.key }}<br>
+			{{_("Type")}}: TOTP (time-based)<br>
+			{{_("Digits")}}: 6<br>
+			{{_("Hash algorithm")}}: SHA1<br>
+			{{_("Interval/period")}}: 30 {{_("seconds")}}
 		</p>
 
 	</div>
@@ -28,7 +35,7 @@
 <form action="{{ url_for('mfa.setup_totp_finish', name=name) }}" method="POST" class="form">
 	<div class="row m-0">
 		<input type="text" name="code" class="form-control mb-2 mr-2 col-auto col-md" id="code" placeholder="Code" required autofocus>
-		<button type="submit" class="btn btn-primary mb-2 col col-md-auto">Verify and complete setup</button>
+		<button type="submit" class="btn btn-primary mb-2 col col-md-auto">{{_("Verify and complete setup")}}</button>
 	</div>
 </form>
 
diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py
index 406566ff..133df968 100644
--- a/uffd/mfa/views.py
+++ b/uffd/mfa/views.py
@@ -2,6 +2,7 @@ from warnings import warn
 import urllib.parse
 
 from flask import Blueprint, render_template, session, request, redirect, url_for, flash, current_app, abort
+from flask_babel import gettext as _
 
 from uffd.database import db
 from uffd.ldap import ldap
@@ -49,7 +50,7 @@ def admin_disable(uid):
 	db.session.commit()
 	user.update_groups()
 	ldap.session.commit()
-	flash('Two-factor authentication was reset')
+	flash(_('Two-factor authentication was reset'))
 	return redirect(url_for('user.show', uid=uid))
 
 @bp.route('/setup/recovery', methods=['POST'])
@@ -78,7 +79,7 @@ def setup_totp():
 @csrf_protect(blueprint=bp)
 def setup_totp_finish():
 	if not RecoveryCodeMethod.query.filter_by(dn=request.user.dn).all():
-		flash('Generate recovery codes first!')
+		flash(_('Generate recovery codes first!'))
 		return redirect(url_for('mfa.setup'))
 	method = TOTPMethod(request.user, name=request.values['name'], key=session.pop('mfa_totp_key'))
 	if method.verify(request.form['code']):
@@ -87,7 +88,7 @@ def setup_totp_finish():
 		request.user.update_groups()
 		ldap.session.commit()
 		return redirect(url_for('mfa.setup'))
-	flash('Code is invalid')
+	flash(_('Code is invalid'))
 	return redirect(url_for('mfa.setup_totp', name=request.values['name']))
 
 @bp.route('/setup/totp/<int:id>/delete')
@@ -111,7 +112,7 @@ try:
 	from fido2 import cbor
 	WEBAUTHN_SUPPORTED = True
 except ImportError as err:
-	warn('2FA WebAuthn support disabled because import of the fido2 module failed (%s)'%err)
+	warn(_('2FA WebAuthn support disabled because import of the fido2 module failed (%s)')%err)
 	WEBAUTHN_SUPPORTED = False
 
 bp.add_app_template_global(WEBAUTHN_SUPPORTED, name='webauthn_supported')
@@ -220,7 +221,7 @@ def auth():
 def auth_finish():
 	delay = mfa_ratelimit.get_delay(request.user_pre_mfa.dn)
 	if delay:
-		flash('We received too many invalid attempts! Please wait at least %s.'%format_delay(delay))
+		flash(_('We received too many invalid attempts! Please wait at least %s.')%format_delay(delay))
 		return redirect(url_for('mfa.auth', ref=request.values.get('ref')))
 	for method in request.user_pre_mfa.mfa_totp_methods:
 		if method.verify(request.form['code']):
@@ -234,12 +235,12 @@ def auth_finish():
 			session['user_mfa'] = True
 			set_request_user()
 			if len(request.user_pre_mfa.mfa_recovery_codes) <= 1:
-				flash('You have exhausted your recovery codes. Please generate new ones now!')
+				flash(_('You have exhausted your recovery codes. Please generate new ones now!'))
 				return redirect(url_for('mfa.setup'))
 			if len(request.user_pre_mfa.mfa_recovery_codes) <= 5:
-				flash('You only have a few recovery codes remaining. Make sure to generate new ones before they run out.')
+				flash(_('You only have a few recovery codes remaining. Make sure to generate new ones before they run out.'))
 				return redirect(url_for('mfa.setup'))
 			return redirect(request.values.get('ref', url_for('index')))
 	mfa_ratelimit.log(request.user_pre_mfa.dn)
-	flash('Two-factor authentication failed')
+	flash(_('Two-factor authentication failed'))
 	return redirect(url_for('mfa.auth', ref=request.values.get('ref')))
diff --git a/uffd/role/templates/role/list.html b/uffd/role/templates/role/list.html
index d5b66624..76e84301 100644
--- a/uffd/role/templates/role/list.html
+++ b/uffd/role/templates/role/list.html
@@ -5,15 +5,15 @@
 	<div class="col">
 		<p class="text-right">
 			<a class="btn btn-primary" href="{{ url_for("role.new") }}">
-				<i class="fa fa-plus" aria-hidden="true"></i> New
+				<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
 			</a>
 		</p>
 		<table class="table table-striped table-sm">
 			<thead>
 				<tr>
-					<th scope="col">roleid</th>
-					<th scope="col">name</th>
-					<th scope="col">description</th>
+					<th scope="col">{{_("roleid")}}</th>
+					<th scope="col">{{_("name")}}</th>
+					<th scope="col">{{_("description")}}</th>
 				</tr>
 			</thead>
 			<tbody>
diff --git a/uffd/role/templates/role/show.html b/uffd/role/templates/role/show.html
index beb6c051..d5ae275f 100644
--- a/uffd/role/templates/role/show.html
+++ b/uffd/role/templates/role/show.html
@@ -3,64 +3,64 @@
 {% block body %}
 {% if role.locked %}
 <div class="alert alert-warning" role="alert">
-Name, moderator group, included roles and groups of this role are managed externally. <a href="{{ url_for("role.unlock", roleid=role.id) }}" class="alert-link">Unlock this role</a> to edit them at the risk of having your changes overwritten.
+{{_("Name, moderator group, included roles and groups of this role are managed externally.")}} <a href="{{ url_for("role.unlock", roleid=role.id) }}" class="alert-link">Unlock this role</a> to edit them at the risk of having your changes overwritten.
 </div>
 {% endif %}
 
 <form action="{{ url_for("role.update", roleid=role.id) }}" method="POST">
 <div class="align-self-center">
 	<div class="float-sm-right pb-2">
-		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
-		<a href="{{ url_for("role.index") }}" class="btn btn-secondary">Cancel</a>
+		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
+		<a href="{{ url_for("role.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
 		{% if role.id %}
 			{% if not role.is_default %}
-			<a href="{{ url_for("role.set_default", roleid=role.id) }}" onClick="return confirm('All non-service users will be removed as members from this role and get its permissions implicitly. Are you sure?');" class="btn btn-secondary">Set as default</a>
+			<a href="{{ url_for("role.set_default", roleid=role.id) }}" onClick="return confirm('All non-service users will be removed as members from this role and get its permissions implicitly. Are you sure?');" class="btn btn-secondary">{{_("Set as default")}}</a>
 			{% else %}
-			<a href="{{ url_for("role.unset_default", roleid=role.id) }}" onClick="return confirm('Are you sure?');" class="btn btn-secondary">Unset as default</a>
+			<a href="{{ url_for("role.unset_default", roleid=role.id) }}" onClick="return confirm('Are you sure?');" class="btn btn-secondary">{{_("Unset as default")}}</a>
 			{% endif %}
-			<a href="{{ url_for("role.delete", roleid=role.id) }}"  onClick="return confirm('Are you sure?');" class="btn btn-danger {{ 'disabled' if role.locked }}"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+			<a href="{{ url_for("role.delete", roleid=role.id) }}"  onClick="return confirm('Are you sure?');" class="btn btn-danger {{ 'disabled' if role.locked }}"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% else %}
-			<a href="#" class="btn btn-secondary disabled">Set as default</a>
-			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+			<a href="#" class="btn btn-secondary disabled">{{_("Set as default")}}</a>
+			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% endif %}
 	</div>
 	<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
 		<li class="nav-item">
-			<a class="nav-link active" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="true">Settings</a>
+			<a class="nav-link active" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="true">{{_("Settings")}}</a>
 		</li>
 		<li class="nav-item">
-			<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">Included roles <span class="badge badge-pill badge-secondary">{{ role.included_roles|length }}</span></a>
+			<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">{{_("Included roles")}} <span class="badge badge-pill badge-secondary">{{ role.included_roles|length }}</span></a>
 		</li>
 		<li class="nav-item">
-			<a class="nav-link" id="groups-tab" data-toggle="tab" href="#groups" role="tab" aria-controls="groups" aria-selected="false">Included groups <span class="badge badge-pill badge-secondary">{{ role.groups|length }}</span></a>
+			<a class="nav-link" id="groups-tab" data-toggle="tab" href="#groups" role="tab" aria-controls="groups" aria-selected="false">{{_("Included groups")}} <span class="badge badge-pill badge-secondary">{{ role.groups|length }}</span></a>
 		</li>
 	</ul>
 
 	<div class="tab-content border mb-2 pt-2" id="tabcontent">
 		<div class="tab-pane fade show active" id="settings" role="tabpanel" aria-labelledby="settings-tab">
 			<div class="form-group col">
-				<label for="role-name">Role Name</label>
+				<label for="role-name">{{_("Role Name")}}</label>
 				<input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}" {{ 'disabled' if role.locked }}>
 				<small class="form-text text-muted">
 				</small>
 			</div>
 			<div class="form-group col">
-				<label for="role-description">Description</label>
+				<label for="role-description">{{_("Description")}}</label>
 				<textarea class="form-control" id="role-description" name="description" rows="5">{{ role.description or '' }}</textarea>
 				<small class="form-text text-muted">
 				</small>
 			</div>
 			<div class="form-group col">
-				<label for="moderator-group">Moderator Group</label>
+				<label for="moderator-group">{{_("Moderator Group")}}</label>
 				<select class="form-control" id="moderator-group" name="moderator-group" {{ 'disabled' if role.locked }}>
-					<option value="" class="text-muted">No Moderator Group</option>
+					<option value="" class="text-muted">{{_("No Moderator Group")}}</option>
 					{% for group in groups %}
 					<option value="{{ group.dn }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option>
 					{% endfor %}
 				</select>
 			</div>
 			<div class="form-group col">
-				<span>Moderators:</span>
+				<span>{{_("Moderators")}}:</span>
 				<ul class="row">
 					{% for moderator in role.moderator_group.members %}
 					<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", uid=moderator.uid) }}">{{ moderator.loginname }}</a></li>
@@ -68,7 +68,7 @@ Name, moderator group, included roles and groups of this role are managed extern
 				</ul>
 			</div>
 			<div class="form-group col">
-				<span>Members:</span>
+				<span>{{_("Members")}}:</span>
 				<ul class="row">
 				{% for member in role.members|sort(attribute='loginname') %}
 					<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", uid=member.uid) }}">{{ member.loginname }}</a></li>
@@ -78,14 +78,14 @@ Name, moderator group, included roles and groups of this role are managed extern
 		</div>
 		<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
 			<div class="form-group col">
-				<span>Roles to include groups from recursively</span>
+				<span>{{_("Roles to include groups from recursively")}}</span>
 				<table class="table table-striped table-sm">
 					<thead>
 						<tr>
 							<th scope="col"></th>
-							<th scope="col">name</th>
-							<th scope="col">description</th>
-							<th scope="col">currently includes groups</th>
+							<th scope="col">{{_("name")}}</th>
+							<th scope="col">{{_("description")}}</th>
+							<th scope="col">{{_("currently includes groups")}}</th>
 						</tr>
 					</thead>
 					<tbody>
@@ -119,14 +119,14 @@ Name, moderator group, included roles and groups of this role are managed extern
 		</div>
 		<div class="tab-pane fade" id="groups" role="tabpanel" aria-labelledby="groups-tab">
 			<div class="form-group col">
-				<span>Included groups</span>
+				<span>{{_("Included groups")}}</span>
 				<table class="table table-striped table-sm">
 					<thead>
 						<tr>
 							<th scope="col"></th>
-							<th scope="col">name</th>
-							<th scope="col">description</th>
-							<th scope="col">2FA required</th>
+							<th scope="col">{{_("name")}}</th>
+							<th scope="col">{{_("description")}}</th>
+							<th scope="col">{{_("2FA required")}}</th>
 						</tr>
 					</thead>
 					<tbody>
diff --git a/uffd/role/views.py b/uffd/role/views.py
index 32d0593a..2e8f9d9f 100644
--- a/uffd/role/views.py
+++ b/uffd/role/views.py
@@ -1,6 +1,7 @@
 import sys
 
 from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
+from flask_babel import gettext as _, lazy_gettext
 import click
 
 from uffd.navbar import register_navbar
@@ -40,14 +41,14 @@ def add_cli_commands(state):
 @login_required()
 def role_acl(): #pylint: disable=inconsistent-return-statements
 	if not role_acl_check():
-		flash('Access denied')
+		flash(_('Access denied'))
 		return redirect(url_for('index'))
 
 def role_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
 @bp.route("/")
-@register_navbar('Roles', icon='key', blueprint=bp, visible=role_acl_check)
+@register_navbar(lazy_gettext('Roles'), icon='key', blueprint=bp, visible=role_acl_check)
 def index():
 	return render_template('role/list.html', roles=Role.query.all())
 
@@ -97,7 +98,7 @@ def update(roleid=None):
 def delete(roleid):
 	role = Role.query.filter_by(id=roleid).one()
 	if role.locked:
-		flash('Locked roles cannot be deleted')
+		flash(_('Locked roles cannot be deleted'))
 		return redirect(url_for('role.show', roleid=role.id))
 	old_members = set(role.members_effective)
 	role.members.clear()
diff --git a/uffd/selfservice/templates/selfservice/forgot_password.html b/uffd/selfservice/templates/selfservice/forgot_password.html
index 1a40f868..6997f4a7 100644
--- a/uffd/selfservice/templates/selfservice/forgot_password.html
+++ b/uffd/selfservice/templates/selfservice/forgot_password.html
@@ -8,18 +8,18 @@
 			<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
 		</div>
 		<div class="col-12">
-			<h2 class="text-center">Forgot password</h2>
+			<h2 class="text-center">{{_("Forgot password")}}</h2>
 		</div>
 		<div class="form-group col-12">
-			<label for="user-loginname">Login Name</label>
+			<label for="user-loginname">{{_("Login Name")}}</label>
 			<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1">
 		</div>
 		<div class="form-group col-12">
-			<label for="user-mail">Mail Address</label>
+			<label for="user-mail">{{_("Mail Address")}}</label>
 			<input type="text" class="form-control" id="user-mail" name="mail" required="required" tabindex = "2">
 		</div>
 		<div class="form-group col-12">
-			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Send password reset mail</button>
+			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">{{_("Send password reset mail")}}</button>
 		</div>
 	</div>
 </div>
diff --git a/uffd/selfservice/templates/selfservice/self.html b/uffd/selfservice/templates/selfservice/self.html
index 96bdaafd..e872748a 100644
--- a/uffd/selfservice/templates/selfservice/self.html
+++ b/uffd/selfservice/templates/selfservice/self.html
@@ -4,13 +4,13 @@
 
 {% if not user.mfa_enabled and user.compute_groups() != user.compute_groups(ignore_mfa=True) %}
 <div class="alert alert-warning" role="alert">
-	Some permissions require you to setup two-factor authentication.
-	These permissions are not in effect until you do that.
+	{{_("Some permissions require you to setup two-factor authentication.
+	These permissions are not in effect until you do that.")}}
 </div>
 {% endif %}
 
 <div class="btn-toolbar">
-	<a class="ml-auto mb-3 btn btn-primary" href="{{ url_for('mfa.setup') }}">Manage two-factor authentication</a>
+	<a class="ml-auto mb-3 btn btn-primary" href="{{ url_for('mfa.setup') }}">{{_("Manage two-factor authentication")}}</a>
 </div>
 
 <form action="{{ url_for("selfservice.update") }}" method="POST" onInput="
@@ -18,41 +18,41 @@
 	password1.setCustomValidity((password1.value.length < 8 && password1.value.length > 0) ? 'Password is too short' : '') ">
 <div class="align-self-center row">
 	<div class="form-group col-md-6">
-		<label for="user-uid">Uid</label>
+		<label for="user-uid">{{_("Uid")}}</label>
 		<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid }}" readonly>
 	</div>
 	<div class="form-group col-md-6">
-		<label for="user-loginname">Login Name</label>
+		<label for="user-loginname">{{_("Login Name")}}</label>
 		<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname }}" readonly>
 	</div>
 	<div class="form-group col-md-6">
-		<label for="user-displayname">Display Name</label>
+		<label for="user-displayname">{{_("Display Name")}}</label>
 		<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
 	</div>
 	<div class="form-group col-md-6">
-		<label for="user-mail">Mail</label>
+		<label for="user-mail">{{_("Mail")}}</label>
 		<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail }}">
 		<small class="form-text text-muted">
-			We will send you a confirmation mail to set a new mail address.
+			{{_("We will send you a confirmation mail to set a new mail address.")}}
 		</small>
 	</div>
 	<div class="form-group col-md-6">
-		<label for="user-password1">Password</label>
+		<label for="user-password1">{{_("Password")}}</label>
 		<input type="password" class="form-control" id="user-password1" name="password1" placeholder="●●●●●●●●">
 		<small class="form-text text-muted">
-			At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.
+			{{_("At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.")}}
 		</small>
 	</div>
 	<div class="form-group col-md-6">
-		<label for="user-password2">Password Repeat</label>
+		<label for="user-password2">{{_("Password Repeat")}}</label>
 		<input type="password" class="form-control" id="user-password2" name="password2" placeholder="●●●●●●●●">
 	</div>
 	<div class="form-group">
 		{% if user.roles|length %}
 			{% if user.roles|length == 1 %}
-				You have this role:
+				{{_("You have this role")}}:
 			{% else %}
-				You currently have these roles:
+				{{_("You currently have these roles")}}:
 			{% endif %}
 			<ul>
 				{% for role in user.roles|sort(attribute="name") %}
@@ -60,11 +60,11 @@
 				{% endfor %}
 			</ul>
 		{% else %}
-			You currently don't have any roles.
+			{{_("You currently don't have any roles.")}}
 		{% endif %}
 	</div>
 	<div class="form-group col-md-12">
-		<button type="submit" class="btn btn-primary float-right"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
+		<button type="submit" class="btn btn-primary float-right"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
 	</div>
 </div>
 </form>
diff --git a/uffd/selfservice/templates/selfservice/set_password.html b/uffd/selfservice/templates/selfservice/set_password.html
index 280c8cad..6288906b 100644
--- a/uffd/selfservice/templates/selfservice/set_password.html
+++ b/uffd/selfservice/templates/selfservice/set_password.html
@@ -8,21 +8,21 @@
 			<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
 		</div>
 		<div class="col-12">
-			<h2 class="text-center">Reset password</h2>
+			<h2 class="text-center">{{_("Reset password")}}</h2>
 		</div>
 		<div class="form-group col-12">
-			<label for="user-password1">New Password</label>
+			<label for="user-password1">{{_("New Password")}}</label>
 			<input type="password" class="form-control" id="user-password1" name="password1" required="required" tabindex = "2">
 			<small class="form-text text-muted">
-				At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.
+				{{_("At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.")}}
 			</small>
 		</div>
 		<div class="form-group col-12">
-			<label for="user-password2">Repeat Password</label>
+			<label for="user-password2">{{_("Repeat Password")}}</label>
 			<input type="password" class="form-control" id="user-password2" name="password2" required="required" tabindex = "2">
 		</div>
 		<div class="form-group col-12">
-			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Set password</button>
+			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">{{_("Set password")}}</button>
 		</div>
 	</div>
 </div>
diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py
index d469ef69..a3279127 100644
--- a/uffd/selfservice/views.py
+++ b/uffd/selfservice/views.py
@@ -5,6 +5,7 @@ from email.message import EmailMessage
 import email.utils
 
 from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session
+from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
 from uffd.csrf import csrf_protect
@@ -20,7 +21,7 @@ bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix=
 reset_ratelimit = Ratelimit('passwordreset', 1*60*60, 3)
 
 @bp.route("/")
-@register_navbar('Selfservice', icon='portrait', blueprint=bp, visible=lambda: bool(request.user))
+@register_navbar(lazy_gettext('Selfservice'), icon='portrait', blueprint=bp, visible=lambda: bool(request.user))
 @login_required()
 def index():
 	return render_template('selfservice/self.html', user=request.user)
@@ -33,21 +34,21 @@ def update():
 	user = request.user
 	if request.values['displayname'] != user.displayname:
 		if user.set_displayname(request.values['displayname']):
-			flash('Display name changed.')
+			flash(_('Display name changed.'))
 		else:
-			flash('Display name is not valid.')
+			flash(_('Display name is not valid.'))
 	if request.values['password1']:
 		if not request.values['password1'] == request.values['password2']:
-			flash('Passwords do not match')
+			flash(_('Passwords do not match'))
 		else:
 			if user.set_password(request.values['password1']):
-				flash('Password changed.')
+				flash(_('Password changed.'))
 				password_changed = True
 			else:
-				flash('Password could not be set.')
+				flash(_('Password could not be set.'))
 	if request.values['mail'] != user.mail:
 		send_mail_verification(user.loginname, request.values['mail'])
-		flash('We sent you an email, please verify your mail address.')
+		flash(_('We sent you an email, please verify your mail address.'))
 	ldap.session.commit()
 	# When using a user_connection, update the connection on password-change
 	if password_changed and current_app.config['LDAP_SERVICE_USER_BIND']:
diff --git a/uffd/services/templates/services/overview.html b/uffd/services/templates/services/overview.html
index 5b91772c..0ccdb6f2 100644
--- a/uffd/services/templates/services/overview.html
+++ b/uffd/services/templates/services/overview.html
@@ -5,7 +5,7 @@
 {% set iconstyle = 'style="width: 1.8em;"'|safe %}
 
 {% if not user %}
-<div class="alert alert-warning" role="alert">Some services may not be publicly listed! Log in to see all services you have access to.</div>
+<div class="alert alert-warning" role="alert">{{_("Some services may not be publicly listed! Log in to see all services you have access to.")}}</div>
 {% endif %}
 
 {% if banner %}
@@ -41,7 +41,7 @@
       </div>
 			<div class="list-group list-group-flush">
 				{% if not service.has_access %}
-					<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> No access</div>
+					<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> {{_("No access")}}</div>
 				{% elif service.permission %}
 					<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> {{ service.permission }}</div>
 				{% endif %}
@@ -75,7 +75,7 @@
 		<div class="modal-content">
 			<div class="modal-header">
 				<h5 class="modal-title">{{ info.title }}</h5>
-				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+				<button type="button" class="close" data-dismiss="modal" aria-label="{{_("Close")}}">
 					<span aria-hidden="true">&times;</span>
 				</button>
 			</div>
diff --git a/uffd/services/views.py b/uffd/services/views.py
index 585de42c..3423eba4 100644
--- a/uffd/services/views.py
+++ b/uffd/services/views.py
@@ -1,4 +1,5 @@
 from flask import Blueprint, render_template, current_app, abort, request
+from flask_babel import lazy_gettext, get_locale
 
 from uffd.navbar import register_navbar
 
@@ -10,12 +11,14 @@ def get_services(user=None):
 		return []
 	services = []
 	for service_data in current_app.config['SERVICES']:
-		if not service_data.get('title'):
+		service_title = get_language_specific(service_data, 'title')
+		if not service_title:
 			continue
+		service_description = get_language_specific(service_data, 'description')
 		service = {
-			'title': service_data['title'],
+			'title': service_title,
 			'subtitle': service_data.get('subtitle', ''),
-			'description': service_data.get('description', ''),
+			'description': service_description,
 			'url': service_data.get('url', ''),
 			'logo_url': service_data.get('logo_url', ''),
 			'has_access': True,
@@ -48,12 +51,15 @@ def get_services(user=None):
 			if info_data.get('required_group'):
 				if not user or not user.has_permission(info_data['required_group']):
 					continue
-			if not info_data.get('title') or not info_data.get('html'):
+			info_title = get_language_specific(info_data, 'title')
+			info_html = get_language_specific(info_data, 'html')
+			if not info_title or not info_html:
 				continue
+			info_button_text = get_language_specific(info_data, 'button_text', info_title)
 			info = {
-				'title': info_data['title'],
-				'button_text': info_data.get('button_text', info_data['title']),
-				'html': info_data['html'],
+				'title': info_title,
+				'button_text': info_button_text,
+				'html': info_html,
 				'id': '%d-%d'%(len(services), len(service['infos'])),
 			}
 			service['infos'].append(info)
@@ -67,11 +73,14 @@ def get_services(user=None):
 		services.append(service)
 	return services
 
+def get_language_specific(data, field_name, default =''):
+	return data.get(field_name + '_' + get_locale().language, data.get(field_name, default))
+
 def services_visible():
 	return len(get_services(request.user)) > 0
 
 @bp.route("/")
-@register_navbar('Services', icon='sitemap', blueprint=bp, visible=services_visible)
+@register_navbar(lazy_gettext('Services'), icon='sitemap', blueprint=bp, visible=services_visible)
 def index():
 	services = get_services(request.user)
 	if not current_app.config['SERVICES']:
diff --git a/uffd/session/templates/session/login.html b/uffd/session/templates/session/login.html
index 54ee477d..7f22b3db 100644
--- a/uffd/session/templates/session/login.html
+++ b/uffd/session/templates/session/login.html
@@ -11,15 +11,15 @@
 			<h2 class="text-center">Login</h2>
 		</div>
 		<div class="form-group col-12">
-			<label for="user-loginname">Login Name</label>
+			<label for="user-loginname">{{ _("Login Name") }}</label>
 			<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1" autofocus>
 		</div>
 		<div class="form-group col-12">
-			<label for="user-password1">Password</label>
+			<label for="user-password1">{{_("Password")}}</label>
 			<input type="password" class="form-control" id="user-password1" name="password" required="required" tabindex = "2">
 		</div>
 		<div class="form-group col-12">
-			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Login</button>
+			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">{{_("Login")}}</button>
 		</div>
 		{% if request.values.get('devicelogin') %}
 		<div class="text-center text-muted mb-3">- or -</div>
@@ -29,10 +29,10 @@
 		{% endif %}
 		<div class="clearfix col-12">
 			{% if config['SELF_SIGNUP'] %}
-			<a href="{{ url_for("signup.signup_start") }}" class="float-left">Register</a>
+			<a href="{{ url_for("signup.signup_start") }}" class="float-left">{{_("Register")}}</a>
 			{% endif %}
 			{% if config['ENABLE_PASSWORDRESET'] %}
-			<a href="{{ url_for("selfservice.forgot_password") }}" class="float-right">Forgot Password?</a>
+			<a href="{{ url_for("selfservice.forgot_password") }}" class="float-right">{{_("Forgot Password?")}}</a>
 			{% endif %}
 		</div>
 	</div>
diff --git a/uffd/session/views.py b/uffd/session/views.py
index 0c93ef72..964a885b 100644
--- a/uffd/session/views.py
+++ b/uffd/session/views.py
@@ -3,6 +3,7 @@ import secrets
 import functools
 
 from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session, abort
+from flask_babel import gettext as _
 
 from uffd.database import db
 from uffd.csrf import csrf_protect
@@ -86,18 +87,18 @@ def login():
 	host_delay = host_ratelimit.get_delay()
 	if login_delay or host_delay:
 		if login_delay > host_delay:
-			flash('We received too many invalid login attempts for this user! Please wait at least %s.'%format_delay(login_delay))
+			flash(_('We received too many invalid login attempts for this user! Please wait at least %s.')%format_delay(login_delay))
 		else:
-			flash('We received too many requests from your ip address/network! Please wait at least %s.'%format_delay(host_delay))
+			flash(_('We received too many requests from your ip address/network! Please wait at least %s.')%format_delay(host_delay))
 		return render_template('session/login.html', ref=request.values.get('ref'))
 	user = login_get_user(username, password)
 	if user is None:
 		login_ratelimit.log(username)
 		host_ratelimit.log()
-		flash('Login name or password is wrong')
+		flash(_('Login name or password is wrong'))
 		return render_template('session/login.html', ref=request.values.get('ref'))
 	if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']):
-		flash('You do not have access to this service')
+		flash(_('You do not have access to this service'))
 		return render_template('session/login.html', ref=request.values.get('ref'))
 	set_session(user, password=password)
 	return redirect(url_for('mfa.auth', ref=request.values.get('ref', url_for('index'))))
@@ -109,7 +110,7 @@ def login_required_pre_mfa(no_redirect=False):
 			if not request.user_pre_mfa:
 				if no_redirect:
 					abort(403)
-				flash('You need to login first')
+				flash(_('You need to login first'))
 				return redirect(url_for('session.login', ref=request.url))
 			return func(*args, **kwargs)
 		return decorator
@@ -120,12 +121,12 @@ def login_required(group=None):
 		@functools.wraps(func)
 		def decorator(*args, **kwargs):
 			if not request.user_pre_mfa:
-				flash('You need to login first')
+				flash(_('You need to login first'))
 				return redirect(url_for('session.login', ref=request.url))
 			if not request.user:
 				return redirect(url_for('mfa.auth', ref=request.url))
 			if not request.user.is_in_group(group):
-				flash('Access denied')
+				flash(_('Access denied'))
 				return redirect(url_for('index'))
 			return func(*args, **kwargs)
 		return decorator
diff --git a/uffd/templates/base.html b/uffd/templates/base.html
index 4fd7ce50..6695abe1 100644
--- a/uffd/templates/base.html
+++ b/uffd/templates/base.html
@@ -36,12 +36,12 @@
 
 		<nav class="navbar navbar-expand-md navbar-dark bg-dark static-top" >
 			<a class="navbar-brand" href="{{ url_for('index') }}">uffd</a>
-			{% if getnavbar() %}
+			{% if getnavbar() or request.user or config['LANGUAGES']|length > 1 %}
 			<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#baseNavbar" aria-controls="baseNavbar" aria-expanded="false" aria-label="Toggle navigation">
 				<span class="navbar-toggler-icon"></span>
 			</button>
-
 			<div class="collapse navbar-collapse" id="baseNavbar">
+				{% if getnavbar() %}
 				<ul class="navbar-nav mr-auto">
 					{% for n in getnavbar() if not n.group %}
 					{{ navbaricon(n) }}
@@ -67,8 +67,26 @@
 					</li>
 					{% endfor %}
 				</ul>
-				{% if request.user %}
+				{% endif %}
+
+				{% if request.user or config['LANGUAGES']|length > 1 %}
 				<ul class="navbar-nav ml-auto">
+					{% if config['LANGUAGES']|length > 1 %}
+					<li class="nav-item">
+						<form class="language-switch py-2 pr-1" method="POST" style="margin-left: -5px;" action="{{ url_for('setlang') }}">
+							<input type="hidden" name="ref" value="{{ request.uri }}">
+							<select name="lang" class="bg-dark" style="border: 0px; color: rgba(255, 255, 255, 0.5);" onchange="$('.language-switch').submit()">
+								{% for language in config['LANGUAGES'] %}
+									<option value="{{ language['value'] }}" {{ 'selected' if language['value'] == get_locale() }}>{{ language['display'] }}</option>
+								{% endfor %}
+							</select>
+							<noscript>
+								<button type="submit" class="bg-dark py-0 pl-0" style="border: 0px; color: rgba(255, 255, 255, 0.5);">Change</button>
+							</noscript>
+						</form>
+					</li>
+					{% endif %}
+					{% if request.user %}
 					<li class="nav-item">
 						<a class="nav-link" href="{{ url_for("session.deviceauth") }}">
 							<span aria-hidden="true" class="fas fa-mobile-alt" title="Authorize Device Login"></span>
@@ -78,9 +96,10 @@
 					<li class="nav-item">
 						<a class="nav-link" href="{{ url_for("session.logout") }}">
 							<span aria-hidden="true" class="fa fa-sign-out-alt"></span>
-							Logout
+							{{_("Logout")}}
 						</a>
 					</li>
+					{% endif %}
 				</ul>
 				{% endif %}
 			</div>
@@ -111,7 +130,7 @@
 					<li class="list-inline-item"><a href="{{ link.url }}">{{ link.title }}</a></li>
 					{% endfor %}
 					<li class="list-inline-item float-right">
-						<a href="https://git.cccv.de/uffd/uffd/">Sourcecode</a>
+						<a href="https://git.cccv.de/uffd/uffd/">{{_("Sourcecode")}}</a>
 						<a target="_blank" href="https://git.cccv.de/uffd/uffd/-/commit/{{ gitversion.longhash }}"><span title="{{ gitversion.branch }} {{ gitversion.hash }}: {{ gitversion.msg }}" data-toggle="tooltip">Version: {{ gitversion.hash }}</span></a>
 					</li>
 				</ul>
diff --git a/uffd/translations/README.md b/uffd/translations/README.md
new file mode 100644
index 00000000..c5b74111
--- /dev/null
+++ b/uffd/translations/README.md
@@ -0,0 +1,21 @@
+# How to add new translations
+
+Extract all translatable string from `.py` and `.html` files to a `.pot`.
+```bash
+pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .
+```
+Update the `messages.po` file to include the new / updated strings.
+```bash
+pybabel update -i messages.pot -d translations
+```
+Compile the `messages.po` file to a `messages.mo` file.
+```bash
+pybabel compile -d translations
+```
+Bonus:  
+Initialize a new language.
+```bash
+pybabel init -i messages.pot -d translations -l de
+```
+
+Complete Documentation of Flask-Babel: https://flask-babel.tkte.ch
\ No newline at end of file
diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000000000000000000000000000000000000..952c7b0b0f71194d8092cfe0910cde1f4d2dfdc7
GIT binary patch
literal 14883
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

literal 0
HcmV?d00001

diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..77a75923
--- /dev/null
+++ b/uffd/translations/de/LC_MESSAGES/messages.po
@@ -0,0 +1,761 @@
+# German translations for uffd.
+# Copyright (C) 2021 ORGANIZATION
+# This file is distributed under the same license as the uffd project.
+# Milan Höllner <milan.hoellner@posteo.de>, 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2021-07-15 22:28+0200\n"
+"PO-Revision-Date: 2021-05-25 21:18+0200\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: de\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.9.1\n"
+
+#: mfa/views.py:53
+msgid "Two-factor authentication was reset"
+msgstr "Zwei-Faktor-Authentifizierung wurde zurückgesetzt"
+
+#: mfa/views.py:82
+msgid "Generate recovery codes first!"
+msgstr "Generiere zuerst die Wiederherstellungscodes!"
+
+#: mfa/views.py:91
+msgid "Code is invalid"
+msgstr "Wiederherstellungscode ist ungültig"
+
+#: mfa/views.py:115
+#, python-format
+msgid ""
+"2FA WebAuthn support disabled because import of the fido2 module failed "
+"(%s)"
+msgstr ""
+"2FA WebAuthn Unterstützung deaktiviert, da das fido2 Modul nicht geladen "
+"werden konnte (%s)"
+
+#: mfa/views.py:224
+#, python-format
+msgid "We received too many invalid attempts! Please wait at least %s."
+msgstr "Wir haben zu viele fehlgeschlagene Versuche! Bitte warte mindestens %s."
+
+#: mfa/views.py:238
+msgid "You have exhausted your recovery codes. Please generate new ones now!"
+msgstr "Du hast keine Wiederherstellungscode mehr. Bitte generiere diese jetzt!"
+
+#: mfa/views.py:241
+msgid ""
+"You only have a few recovery codes remaining. Make sure to generate new "
+"ones before they run out."
+msgstr ""
+"Du hast nur noch wenige Wiederherstellungscodes übrig. Bitte generiere "
+"diese erneut bevor keine mehr übrig sind."
+
+#: mfa/views.py:245
+msgid "Two-factor authentication failed"
+msgstr "Zwei-Faktor-Authentifizierung fehlgeschlagen"
+
+#: mfa/templates/mfa/auth.html:12
+msgid "Two-Factor Authentication"
+msgstr "Zwei-Faktor-Authentifizierung"
+
+#: mfa/templates/mfa/auth.html:17
+msgid "Enable javascript for authentication with U2F/FIDO2 devices"
+msgstr "Aktiviere Javascript zur Authentifizierung mit U2F/FIDO2 Geräten"
+
+#: mfa/templates/mfa/auth.html:21
+msgid "Authentication with U2F/FIDO2 devices is not supported by your browser"
+msgstr ""
+"Authentifizierung mit U2F/FIDO2 Geräten wird von deinem Browser nicht "
+"unterstützt"
+
+#: mfa/templates/mfa/auth.html:27
+msgid "Authenticate with U2F/FIDO2 device"
+msgstr "Authentifiziere dich mit einem U2F/FIDO2 Gerät"
+
+#: mfa/templates/mfa/auth.html:30
+msgid "or"
+msgstr "oder"
+
+#: mfa/templates/mfa/auth.html:33
+msgid "Code from your authenticator app or recovery code"
+msgstr "Code aus deiner Authentifikator-App oder Wiederherstellungscode"
+
+#: mfa/templates/mfa/auth.html:36
+msgid "Verify"
+msgstr "Verifizieren"
+
+#: mfa/templates/mfa/auth.html:39 role/templates/role/show.html:14
+#: user/templates/user/show.html:8
+msgid "Cancel"
+msgstr "Abbrechen"
+
+#: mfa/templates/mfa/disable.html:6
+msgid ""
+"When you proceed, all recovery codes, registered authenticator "
+"applications and devices will be invalidated.\n"
+"\tYou can later generate new recovery codes and setup your applications "
+"and devices again."
+msgstr ""
+"Wenn du fortfährst werden alle Wiederherstellungscodes, registrierte "
+"Authentifikator-Apps und Geräte ungültig gemacht. Du kannst später neue "
+"Wiederherstellungscodes generieren und das Setup der Anwendungen und "
+"Geräte erneut durchführen."
+
+#: mfa/templates/mfa/disable.html:11 mfa/templates/mfa/setup.html:32
+msgid "Disable two-factor authentication"
+msgstr "Zwei-Faktor-Authentifizierung (2FA) deaktivieren"
+
+#: mfa/templates/mfa/setup.html:18
+msgid "Two-factor authentication is currently <strong>enabled</strong>."
+msgstr "Die Zwei-Faktor-Authentifizierung ist derzeit <strong>aktiviert</strong>."
+
+#: mfa/templates/mfa/setup.html:20
+msgid "Two-factor authentication is currently <strong>disabled</strong>."
+msgstr "Die Zwei-Faktor-Authentifizierung ist derzeit <strong>deaktiviert</strong>."
+
+#: mfa/templates/mfa/setup.html:23
+msgid ""
+"You need to generate recovery codes and setup at least one authentication"
+" method to enable two-factor authentication."
+msgstr ""
+"Du musst Wiederherstellungscodes generieren und mindestens eine "
+"Authentifizierungsmethode hinzufügen um Zwei-Faktor-Authentifizierung "
+"nutzen zu können."
+
+#: mfa/templates/mfa/setup.html:25
+msgid ""
+"You need to setup at least one authentication method to enable two-factor"
+" authentication."
+msgstr ""
+"Du musst mindestens eine Authentifizierungsmethode hinzufügen um Zwei-"
+"Faktor-Authentifizierung nutzen zu können."
+
+#: mfa/templates/mfa/setup.html:36
+msgid "Reset two-factor configuration"
+msgstr "Zwei-Faktor-Authentifizierung zurücksetzen"
+
+#: mfa/templates/mfa/setup.html:46 mfa/templates/mfa/setup_recovery.html:5
+msgid "Recovery Codes"
+msgstr "Wiederherstellungscodes"
+
+#: mfa/templates/mfa/setup.html:48
+msgid ""
+"Recovery codes allow you to login and setup new two-factor methods when "
+"you lost your registered second factor."
+msgstr ""
+"Wiederherstellungscodes erlauben die Anmeldung und das erneute Hinzufügen"
+" einer Zwei-Faktor-Methode, falls der Zweite Faktor verloren geht."
+
+#: mfa/templates/mfa/setup.html:52
+msgid ""
+"You need to setup recovery codes before you can setup up authenticator "
+"apps or U2F/FIDO2 devices."
+msgstr ""
+"Du musst Wiederherstellungscodes generieren bevor du einen "
+"Authentifikator-App oder ein U2F/FIDO2 Gerät hinzufen kannst."
+
+#: mfa/templates/mfa/setup.html:54
+msgid "Each code can only be used once."
+msgstr "Jeder Code kann nur einmal verwendet werden."
+
+#: mfa/templates/mfa/setup.html:62
+msgid "Generate recovery codes to enable two-factor authentication"
+msgstr ""
+"Generiere Wiederherstellungscodes um die Zwei-Faktor-Authentifizierung zu"
+" aktivieren"
+
+#: mfa/templates/mfa/setup.html:66
+msgid "Generate new recovery codes"
+msgstr "Generiere neue Wiederherstellungscodes"
+
+#: mfa/templates/mfa/setup.html:75
+msgid "You have no remaining recovery codes."
+msgstr "Du hast keine Wiederherstellungscodes übrig."
+
+#: mfa/templates/mfa/setup.html:85
+msgid "Authenticator Apps (TOTP)"
+msgstr "Authentifikator-Apps (TOTP)"
+
+#: mfa/templates/mfa/setup.html:87
+msgid "Use an authenticator application on your mobile device as a second factor."
+msgstr "Nutze eine Authentifikator-App auf deinem Mobilgerät als zweiten Faktor."
+
+#: mfa/templates/mfa/setup.html:90
+msgid ""
+"The authenticator app generates a 6-digit one-time code each time you "
+"login.\n"
+"\t\t\tCompatible apps are freely available for most phones."
+msgstr ""
+"Die Authentifikator-App generiert ein 6-stelliges Einmalpasswort für "
+"jeden Login.Passende Apps sind kostenlos verfügbar für die meisten "
+"Mobilgeräte."
+
+#: mfa/templates/mfa/setup.html:98 mfa/templates/mfa/setup.html:99
+#: mfa/templates/mfa/setup.html:107 mfa/templates/mfa/setup.html:157
+#: mfa/templates/mfa/setup.html:158 mfa/templates/mfa/setup.html:169
+msgid "Name"
+msgstr "Name"
+
+#: mfa/templates/mfa/setup.html:100
+msgid "Setup new app"
+msgstr "Neue App hinzufügen"
+
+#: mfa/templates/mfa/setup.html:108 mfa/templates/mfa/setup.html:170
+msgid "Registered On"
+msgstr "Registriert am"
+
+#: mfa/templates/mfa/setup.html:117 mfa/templates/mfa/setup.html:179
+#: role/templates/role/show.html:21 role/templates/role/show.html:24
+#: user/templates/user/show.html:11 user/templates/user/show.html:13
+msgid "Delete"
+msgstr "Löschen"
+
+#: mfa/templates/mfa/setup.html:122
+msgid "No authenticator apps registered yet"
+msgstr "Bisher keine Authentifikator-Apps registriert"
+
+#: mfa/templates/mfa/setup.html:134
+msgid "U2F and FIDO2 Devices"
+msgstr "U2F und FIDO2 Geräte"
+
+#: mfa/templates/mfa/setup.html:136
+msgid "Use an U2F or FIDO2 compatible hardware security key as a second factor."
+msgstr "Nutze einen U2F oder FIDO2 kompatiblen Key als zweiten Faktor."
+
+#: mfa/templates/mfa/setup.html:139
+msgid ""
+"U2F and FIDO2 devices are not supported by all browsers and can be "
+"particularly difficult to use on mobile\n"
+"\t\t\tdevices. <strong>It is strongly recommended to also setup an "
+"authenticator app</strong> to be able to login on all\n"
+"\t\t\tbrowsers."
+msgstr ""
+"U2F und FIDO2 Geräte werden nicht von allen Browsern unterstützt und "
+"können besonders auf mobilen Geräten schwer zu nutzen sein. <strong>Es "
+"wird dringend empfohlen ebenfalls eine Authentifikator-App "
+"hinzuzufügen</strong> um einen Login mit allen Browsern zu ermöglichen."
+
+#: mfa/templates/mfa/setup.html:147
+msgid "U2F/FIDO2 support not enabled"
+msgstr "U2F/FIDO2 Unterstützung nicht aktiviert"
+
+#: mfa/templates/mfa/setup.html:151
+msgid "Enable javascript in your browser to use U2F and FIDO2 devices!"
+msgstr ""
+"Aktiviere Javascript in deinem Browser, um U2F und FIDO2 Geräte nutzen zu"
+" können!"
+
+#: mfa/templates/mfa/setup.html:161
+msgid "Setup new device"
+msgstr "Neues Gerät hinzufügen"
+
+#: mfa/templates/mfa/setup.html:184
+msgid "No U2F/FIDO2 devices registered yet"
+msgstr "Bisher kein U2F/FIDO2 Gerät registriert"
+
+#: mfa/templates/mfa/setup_recovery.html:8
+msgid ""
+"Recovery codes allow you to login when you lose access to your "
+"authenticator app or U2F/FIDO device. Each code can\n"
+"\tonly be used once."
+msgstr ""
+"Wiederherstellungscodes erlauben den Login, wenn der Zugriff auf die "
+"Authentifikator-App oder das U2F/FIDO2 Gerät verloren geht. Jeder Code "
+"kann nur einmal verwendet werden."
+
+#: mfa/templates/mfa/setup_recovery.html:21
+msgid ""
+"These are your new recovery codes. Make sure to store them in a safe "
+"place or you risk losing access to your\n"
+"\taccount. All previous recovery codes are now invalid."
+msgstr ""
+"Dies sind deine Wiederherstellungscodes. Speichere sie an einem sicheren "
+"Ort, sonst könntest du den Zugriff auf dein Konto verlieren. Alle "
+"vorherigen Wiederherstellungscodes sind nun ungültig."
+
+#: mfa/templates/mfa/setup_recovery.html:28
+msgid "Download codes"
+msgstr "Codes herunterladen"
+
+#: mfa/templates/mfa/setup_recovery.html:30
+msgid "Print codes"
+msgstr "Codes ausdrucken"
+
+#: mfa/templates/mfa/setup_totp.html:6
+msgid ""
+"Install an authenticator application on your mobile device like FreeOTP "
+"or Google Authenticator and scan this QR\n"
+"\tcode. On Apple devices you can use an app called \"Authenticator\"."
+msgstr ""
+"Installiere eine Authentifikator-App auf deinem Mobilgerät wie FreeOTP "
+"oder Google Authenticator and scanne diesen QR Code. Auf Geräten von "
+"Apple kann die App \"Authenticator\" verwendet werden."
+
+#: mfa/templates/mfa/setup_totp.html:18
+msgid ""
+"If you are on your mobile device and cannot scan the code, you can click "
+"on it to open it with your\n"
+"\t\t\tauthenticator app. If that does not work, enter the following "
+"details manually into your authenticator\n"
+"\t\t\tapp:"
+msgstr ""
+"Falls du ein Mobilgerät verwendest und den Code nicht scannen kannst, "
+"kannst du drauf klick und direkt in der Authentifikator-App öffnen. Wenn "
+"das nicht funktioniert, gib die folgenden Angaben manuell in die "
+"Authentifikator-App ein:"
+
+#: mfa/templates/mfa/setup_totp.html:23
+msgid "Issuer"
+msgstr "Herausgeber"
+
+#: mfa/templates/mfa/setup_totp.html:24
+msgid "Account"
+msgstr "Konto"
+
+#: mfa/templates/mfa/setup_totp.html:25
+msgid "Secret"
+msgstr "Geheimnis"
+
+#: mfa/templates/mfa/setup_totp.html:26
+msgid "Type"
+msgstr "Typ"
+
+#: mfa/templates/mfa/setup_totp.html:27
+msgid "Digits"
+msgstr "Zeichen"
+
+#: mfa/templates/mfa/setup_totp.html:28
+msgid "Hash algorithm"
+msgstr "Hash-Algorithmus"
+
+#: mfa/templates/mfa/setup_totp.html:29
+msgid "Interval/period"
+msgstr "Intervall/Dauer"
+
+#: mfa/templates/mfa/setup_totp.html:29
+msgid "seconds"
+msgstr "Sekunden"
+
+#: mfa/templates/mfa/setup_totp.html:38
+msgid "Verify and complete setup"
+msgstr "Verifiziere und beende das Setup"
+
+#: role/views.py:44 session/views.py:126 user/views_group.py:14
+#: user/views_user.py:22
+msgid "Access denied"
+msgstr "Zugriff verweigert"
+
+#: role/views.py:51 user/templates/user/show.html:21
+#: user/templates/user/show.html:93
+msgid "Roles"
+msgstr "Rollen"
+
+#: role/views.py:101
+msgid "Locked roles cannot be deleted"
+msgstr "Gesperrte Rollen können nicht gelöscht werden"
+
+#: role/templates/role/list.html:8 user/templates/user/list.html:8
+msgid "New"
+msgstr "Neu"
+
+#: role/templates/role/list.html:14
+msgid "roleid"
+msgstr "Rollen ID"
+
+#: role/templates/role/list.html:15 role/templates/role/show.html:86
+#: role/templates/role/show.html:127 user/templates/group/list.html:10
+#: user/templates/group/show.html:11 user/templates/user/show.html:98
+#: user/templates/user/show.html:130
+msgid "name"
+msgstr "Name"
+
+#: role/templates/role/list.html:16 role/templates/role/show.html:87
+#: role/templates/role/show.html:128 user/templates/group/list.html:11
+#: user/templates/user/show.html:99 user/templates/user/show.html:131
+msgid "description"
+msgstr "Beschreibung"
+
+#: role/templates/role/show.html:6
+msgid ""
+"Name, moderator group, included roles and groups of this role are managed"
+" externally."
+msgstr ""
+"Name, Moderator:innengruppe, enthaltene Rollen und Gruppen dieser Rolle "
+"werden extern verwaltet."
+
+#: role/templates/role/show.html:13
+#: selfservice/templates/selfservice/self.html:67
+#: user/templates/user/show.html:7
+msgid "Save"
+msgstr "Speichern"
+
+#: role/templates/role/show.html:17 role/templates/role/show.html:23
+msgid "Set as default"
+msgstr "Als Default setzen"
+
+#: role/templates/role/show.html:19
+msgid "Unset as default"
+msgstr "Nicht mehr als Default setzen"
+
+#: role/templates/role/show.html:29
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: role/templates/role/show.html:32
+msgid "Included roles"
+msgstr "Enthaltene Rollen"
+
+#: role/templates/role/show.html:35 role/templates/role/show.html:122
+msgid "Included groups"
+msgstr "Enthaltene Gruppen"
+
+#: role/templates/role/show.html:42
+msgid "Role Name"
+msgstr "Rollenname"
+
+#: role/templates/role/show.html:48
+msgid "Description"
+msgstr "Beschreibung"
+
+#: role/templates/role/show.html:54
+msgid "Moderator Group"
+msgstr "Moderator:innengruppe"
+
+#: role/templates/role/show.html:56
+msgid "No Moderator Group"
+msgstr "Keine Moderator:innengruppe"
+
+#: role/templates/role/show.html:63
+msgid "Moderators"
+msgstr "Moderator:innen"
+
+#: role/templates/role/show.html:71 user/templates/group/show.html:15
+msgid "Members"
+msgstr "Mitglieder"
+
+#: role/templates/role/show.html:81
+msgid "Roles to include groups from recursively"
+msgstr "Rollen, deren Gruppen rekursiv enthalten sein sollen"
+
+#: role/templates/role/show.html:88
+msgid "currently includes groups"
+msgstr "derzeit enthaltene Gruppen"
+
+#: role/templates/role/show.html:129
+msgid "2FA required"
+msgstr "2FA erforderlich"
+
+#: selfservice/views.py:24
+msgid "Selfservice"
+msgstr ""
+
+#: selfservice/views.py:37
+msgid "Display name changed."
+msgstr "Anzeigename geändert."
+
+#: selfservice/views.py:39
+msgid "Display name is not valid."
+msgstr "Anzeigename ist nicht valide."
+
+#: selfservice/views.py:42
+msgid "Passwords do not match"
+msgstr "Die Passwörter stimmen nicht überein"
+
+#: selfservice/views.py:45
+msgid "Password changed."
+msgstr "Passwort geändert."
+
+#: selfservice/views.py:48
+msgid "Password could not be set."
+msgstr "Das Passwort konnte nicht gesetzt werden."
+
+#: selfservice/views.py:51
+msgid "We sent you an email, please verify your mail address."
+msgstr "Wir haben dir eine E-Mail gesendet, bitte prüfe deine E-Mail-Adresse."
+
+#: selfservice/templates/selfservice/forgot_password.html:11
+msgid "Forgot password"
+msgstr "Passwort vergessen"
+
+#: selfservice/templates/selfservice/forgot_password.html:14
+#: selfservice/templates/selfservice/self.html:25
+#: session/templates/session/login.html:14 user/templates/user/show.html:51
+msgid "Login Name"
+msgstr "Anmeldename"
+
+#: selfservice/templates/selfservice/forgot_password.html:18
+msgid "Mail Address"
+msgstr "Mail-Adresse"
+
+#: selfservice/templates/selfservice/forgot_password.html:22
+msgid "Send password reset mail"
+msgstr "Passwort-Zurücksetzen-Mail versenden"
+
+#: selfservice/templates/selfservice/self.html:7
+msgid ""
+"Some permissions require you to setup two-factor authentication.\n"
+"\tThese permissions are not in effect until you do that."
+msgstr ""
+
+#: selfservice/templates/selfservice/self.html:13
+msgid "Manage two-factor authentication"
+msgstr "Zwei-Faktor-Authentifizierung (2FA) bearbeiten"
+
+#: selfservice/templates/selfservice/self.html:21
+msgid "Uid"
+msgstr ""
+
+#: selfservice/templates/selfservice/self.html:29
+#: user/templates/user/show.html:66
+msgid "Display Name"
+msgstr "Anzeigename"
+
+#: selfservice/templates/selfservice/self.html:33
+#: user/templates/user/show.html:73
+msgid "Mail"
+msgstr "E-Mail-Adresse"
+
+#: selfservice/templates/selfservice/self.html:36
+msgid "We will send you a confirmation mail to set a new mail address."
+msgstr ""
+"Wir werden dir eine Bestätigungsmail zum Setzen der neuen E-Mail-Adresse "
+"senden."
+
+#: selfservice/templates/selfservice/self.html:40
+#: session/templates/session/login.html:18 user/templates/user/show.html:80
+msgid "Password"
+msgstr "Passwort"
+
+#: selfservice/templates/selfservice/self.html:43
+#: selfservice/templates/selfservice/set_password.html:17
+#: user/templates/user/show.html:87
+msgid ""
+"At least 8 and at most 256 characters, no other special requirements. But"
+" please don't be stupid, do use a password manager."
+msgstr ""
+"Mindestens 8 und maximal 256 Zeichen, keine weiteren Einschränkungen. "
+"Bitte sei nicht dumm und verwende einen Passwort-Manager."
+
+#: selfservice/templates/selfservice/self.html:47
+msgid "Password Repeat"
+msgstr "Passwort wiederholen"
+
+#: selfservice/templates/selfservice/self.html:53
+msgid "You have this role"
+msgstr "Du hast diese Rolle"
+
+#: selfservice/templates/selfservice/self.html:55
+msgid "You currently have these roles"
+msgstr "Du hast aktuell folgende Rollen"
+
+#: selfservice/templates/selfservice/self.html:63
+msgid "You currently don't have any roles."
+msgstr "Du hast aktuell keine Rollen."
+
+#: selfservice/templates/selfservice/set_password.html:11
+msgid "Reset password"
+msgstr "Passwort zurücksetzen"
+
+#: selfservice/templates/selfservice/set_password.html:14
+msgid "New Password"
+msgstr "Neues Passwort"
+
+#: selfservice/templates/selfservice/set_password.html:21
+msgid "Repeat Password"
+msgstr "Passwort wiederholen"
+
+#: selfservice/templates/selfservice/set_password.html:25
+msgid "Set password"
+msgstr "Passwort setzen"
+
+#: services/views.py:83
+msgid "Services"
+msgstr ""
+
+#: services/templates/services/overview.html:8
+msgid ""
+"Some services may not be publicly listed! Log in to see all services you "
+"have access to."
+msgstr ""
+"Einige Services sind eventuell nicht öffentlich aufgelistet! Melde dich "
+"an um alle deine Service zu sehen."
+
+#: services/templates/services/overview.html:44
+msgid "No access"
+msgstr "Kein Zugriff"
+
+#: services/templates/services/overview.html:78
+#: user/templates/user/list.html:58 user/templates/user/list.html:81
+msgid "Close"
+msgstr "Schließen"
+
+#: session/views.py:87
+#, python-format
+msgid ""
+"We received too many invalid login attempts for this user! Please wait at"
+" least %s."
+msgstr ""
+"Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account! "
+"Bitte warte mindestens %s."
+
+#: session/views.py:89
+#, python-format
+msgid ""
+"We received too many requests from your ip address/network! Please wait "
+"at least %s."
+msgstr ""
+"Wir haben zu viele Anfragen von der IP Adresses / aus deinem Netzwerk "
+"bekommen! Bitte warte mindestens %s."
+
+#: session/views.py:95
+msgid "Login name or password is wrong"
+msgstr "Der Anmeldename oder das Passwort ist falsch"
+
+#: session/views.py:98
+msgid "You do not have access to this service"
+msgstr "Du hast keinen Zugriff auf diesen Service"
+
+#: session/views.py:110 session/views.py:121
+msgid "You need to login first"
+msgstr "Du musst dich erst anmelden"
+
+#: session/templates/session/login.html:22
+msgid "Login"
+msgstr "Anmelden"
+
+#: session/templates/session/login.html:26
+msgid "Register"
+msgstr "Registrieren"
+
+#: session/templates/session/login.html:29
+msgid "Forgot Password?"
+msgstr "Passwort vergessen?"
+
+#: templates/base.html:75
+msgid "Logout"
+msgstr "Abmelden"
+
+#: templates/base.html:108
+msgid "Sourcecode"
+msgstr "Quellcode"
+
+#: user/views_group.py:21
+msgid "Groups"
+msgstr "Gruppen"
+
+#: user/views_user.py:29
+msgid "Users"
+msgstr "Nutzer:innen"
+
+#: user/views_user.py:49
+msgid "Login name does not meet requirements"
+msgstr "Anmeldename entspricht nicht den Anforderungen"
+
+#: user/views_user.py:54
+msgid "Mail is invalid"
+msgstr "E-Mail-Adresse nicht valide"
+
+#: user/views_user.py:58
+msgid "Display name does not meet requirements"
+msgstr "Anzeigename entspricht nicht den Anforderungen"
+
+#: user/views_user.py:75
+msgid "Service user created"
+msgstr "Service-Account erstellt"
+
+#: user/views_user.py:78
+msgid "User created. We sent the user a password reset link by mail"
+msgstr ""
+"Nutzer:in erstellt. Wir haben der/dem Nutzer:in eine E-Mail mit einem "
+"Passwort Zurücksetzen Link gesendet"
+
+#: user/views_user.py:80
+msgid "User updated"
+msgstr "Nutzer:in aktualisiert"
+
+#: user/views_user.py:91
+msgid "Deleted user"
+msgstr "Nutzer:in gelöscht"
+
+#: user/templates/group/list.html:9 user/templates/group/show.html:7
+msgid "gid"
+msgstr ""
+
+#: user/templates/user/list.html:11
+msgid "CSV import"
+msgstr "CSV Import"
+
+#: user/templates/user/list.html:17
+msgid "uid"
+msgstr ""
+
+#: user/templates/user/list.html:18
+msgid "login name"
+msgstr "Anmeldename"
+
+#: user/templates/user/list.html:19
+msgid "display name"
+msgstr "Anzeigename"
+
+#: user/templates/user/list.html:20
+msgid "roles"
+msgstr "Rollen"
+
+#: user/templates/user/list.html:57
+msgid "Import a csv formated list of users"
+msgstr "Importiere eine als CSV formatierte Liste von Nutzer:innen"
+
+#: user/templates/user/list.html:77
+msgid "Ignore login name blacklist"
+msgstr ""
+
+#: user/templates/user/list.html:82
+msgid "Import"
+msgstr "Importieren"
+
+#: user/templates/user/show.html:10
+msgid "Reset 2FA"
+msgstr "2FA zurücksetzen"
+
+#: user/templates/user/show.html:18
+msgid "Profile"
+msgstr "Profile"
+
+#: user/templates/user/show.html:54
+msgid ""
+"Only letters, numbers and underscore (\"_\") are allowed. At most 32, at "
+"least 2 characters. There is a word blacklist. Must be unique."
+msgstr ""
+"Nur Buchstaben, Zahlen und Unterstriche (\"_\") sind erlaubt. Maximal 32,"
+" mindestens 2 Zeichen. Muss einmalig sein."
+
+#: user/templates/user/show.html:69
+msgid ""
+"If you leave this empty it will be set to the login name. At most 128, at"
+" least 2 characters. No character restrictions."
+msgstr ""
+"Wenn das Feld leer bleibt, wird der Anmeldename verwendet. Maximal 128, "
+"midestens 2 Zeichen. Keine Zeichenbeschränkung."
+
+#: user/templates/user/show.html:76
+msgid ""
+"Do a sanity check here. A user can take over another account if both have"
+" the same mail address set."
+msgstr ""
+"Prüfe die Einmaligkeit. Ein:e Nutzer:in kann einen anderen Account "
+"übernehmen, wenn beide die selbe E-Mail-Adresse verwenden."
+
+#: user/templates/user/show.html:84
+msgid "mail to set it will be sent"
+msgstr "Mail zum Setzen wird versendet"
+
+#: user/templates/user/show.html:126
+msgid "Resulting groups (only updated after save)"
+msgstr "Resultierende Gruppen (wird nur aktualisiert beim Speichern)"
+
diff --git a/uffd/user/templates/group/list.html b/uffd/user/templates/group/list.html
index f8a35b10..89abf1ad 100644
--- a/uffd/user/templates/group/list.html
+++ b/uffd/user/templates/group/list.html
@@ -6,9 +6,9 @@
 		<table class="table table-striped table-sm">
 			<thead>
 				<tr>
-					<th scope="col">gid</th>
-					<th scope="col">name</th>
-					<th scope="col">description</th>
+					<th scope="col">{{_("gid")}}</th>
+					<th scope="col">{{_("name")}}</th>
+					<th scope="col">{{_("description")}}</th>
 				</tr>
 			</thead>
 			<tbody>
diff --git a/uffd/user/templates/group/show.html b/uffd/user/templates/group/show.html
index e98726d8..71ab805a 100644
--- a/uffd/user/templates/group/show.html
+++ b/uffd/user/templates/group/show.html
@@ -4,15 +4,15 @@
 <form action="{{ url_for("group.show", gid=group.gid) }}" method="POST">
 <div class="align-self-center">
 	<div class="form-group col">
-		<label for="group-gid">gid</label>
+		<label for="group-gid">{{_("gid")}}</label>
 		<input type="number" class="form-control" id="group-gid" name="gid" value="{{ group.gid }}" readonly>
 	</div>
 	<div class="form-group col">
-		<label for="group-loginname">name</label>
+		<label for="group-loginname">{{_("name")}}</label>
 		<input type="text" class="form-control" id="group-loginname" name="loginname" value="{{ group.name }}" readonly>
 	</div>
 	<div class="col"> 
-		<span>Members:</span>
+		<span>{{_("Members")}}:</span>
 		<ul class="row">
 		{% for member in group.members|sort(attribute='loginname') %}
 			<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", uid=member.uid) }}">{{ member.loginname }}</a></li>
diff --git a/uffd/user/templates/user/list.html b/uffd/user/templates/user/list.html
index a6854d9f..7f5edc7e 100644
--- a/uffd/user/templates/user/list.html
+++ b/uffd/user/templates/user/list.html
@@ -5,19 +5,19 @@
 	<div class="col">
 		<p class="text-right">
 			<a class="btn btn-primary" href="{{ url_for("user.show") }}">
-				<i class="fa fa-plus" aria-hidden="true"></i> New
+				<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
 			</a>
 			<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#csvimport">
-				<i class="fa fa-file-csv" aria-hidden="true"></i> CSV import
+				<i class="fa fa-file-csv" aria-hidden="true"></i> {{_("CSV import")}}
 			</button>
 		</p>
 		<table class="table table-striped table-sm">
 			<thead>
 				<tr>
-					<th scope="col">uid</th>
-					<th scope="col">login name</th>
-					<th scope="col">display name</th>
-					<th scope="col">roles</th>
+					<th scope="col">{{_("uid")}}</th>
+					<th scope="col">{{_("login name")}}</th>
+					<th scope="col">{{_("display name")}}</th>
+					<th scope="col">{{_("roles")}}</th>
 				</tr>
 			</thead>
 			<tbody>
@@ -54,8 +54,8 @@
 	<div class="modal-dialog" role="document">
 		<div class="modal-content">
 			<div class="modal-header">
-				<h5 class="modal-title" id="exampleModalLabel">Import a csv formated list of users</h5>
-				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+				<h5 class="modal-title" id="exampleModalLabel">{{_("Import a csv formated list of users")}}</h5>
+				<button type="button" class="close" data-dismiss="modal" aria-label="{{_('Close')}}">
 					<span aria-hidden="true">&times;</span>
 				</button>
 			</div>
@@ -74,12 +74,12 @@ testuser2,foobaadsfr@example.com,5;2
 				<textarea rows="10" class="form-control" name="csv"></textarea>
 				<div class="form-check mt-2">
 					<input class="form-check-input" type="checkbox" id="ignore-loginname-blacklist" name="ignore-loginname-blacklist" value="1" aria-label="enabled">
-					<label class="form-check-label" for="ignore-loginname-blacklist">Ignore login name blacklist</label>
+					<label class="form-check-label" for="ignore-loginname-blacklist">{{_("Ignore login name blacklist")}}</label>
 				</div>
 			</div>
 			<div class="modal-footer">
-				<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-				<button type="submit" class="btn btn-primary">Import</button>
+				<button type="button" class="btn btn-secondary" data-dismiss="modal">{{_("Close")}}</button>
+				<button type="submit" class="btn btn-primary">{{_("Import")}}</button>
 			</div>
 		</div>
 	</div>
diff --git a/uffd/user/templates/user/show.html b/uffd/user/templates/user/show.html
index 1511e74b..72d50653 100644
--- a/uffd/user/templates/user/show.html
+++ b/uffd/user/templates/user/show.html
@@ -4,21 +4,21 @@
 <form action="{{ url_for("user.update", uid=user.uid) }}" method="POST">
 <div class="align-self-center">
 	<div class="float-sm-right pb-2">
-		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
-		<a href="{{ url_for("user.index") }}" class="btn btn-secondary">Cancel</a>
+		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
+		<a href="{{ url_for("user.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
 		{% if user.uid %}
-			<a href="{{ url_for("mfa.admin_disable", uid=user.uid) }}" class="btn btn-secondary">Reset 2FA</a>
-			<a href="{{ url_for("user.delete", uid=user.uid) }}" onClick="return confirm('Are you sure?');" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+			<a href="{{ url_for("mfa.admin_disable", uid=user.uid) }}" class="btn btn-secondary">{{_("Reset 2FA")}}</a>
+			<a href="{{ url_for("user.delete", uid=user.uid) }}" onClick="return confirm('Are you sure?');" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% else %}
-			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
 		{% endif %}
 	</div>
 	<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
 		<li class="nav-item">
-			<a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">Profile</a>
+			<a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">{{_("Profile")}}</a>
 		</li>
 		<li class="nav-item">
-			<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">Roles</a>
+			<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">{{_("Roles")}}</a>
 		</li>
 		<li class="nav-item">
 			<a class="nav-link" id="ldif-tab" data-toggle="tab" href="#ldif" role="tab" aria-controls="ldif" aria-selected="false">LDIF</a>
@@ -48,10 +48,10 @@
 			</div>
 			{% endif %}
 			<div class="form-group col">
-				<label for="user-loginname">Login Name</label>
+				<label for="user-loginname">{{_("Login Name")}}</label>
 				<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname or '' }}" {% if user.uid %}readonly{% endif %}>
 				<small class="form-text text-muted">
-					Only letters, numbers and underscore ("_") are allowed. At most 32, at least 2 characters. There is a word blacklist. Musst be unique.
+					{{_("Only letters, numbers and underscore (\"_\") are allowed. At most 32, at least 2 characters. There is a word blacklist. Must be unique.")}}
 				</small>
 			</div>
 			{% if not user.uid %}
@@ -63,40 +63,40 @@
 			</div>
 			{% endif %}
 			<div class="form-group col">
-				<label for="user-loginname">Display Name</label>
+				<label for="user-loginname">{{_("Display Name")}}</label>
 				<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname or '' }}">
 				<small class="form-text text-muted">
-					If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.
+					{{_("If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.")}}
 				</small>
 			</div>
 			<div class="form-group col">
-				<label for="user-mail">Mail</label>
+				<label for="user-mail">{{_("Mail")}}</label>
 				<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail or '' }}">
 				<small class="form-text text-muted">
-					Do a sanity check here. A user can take over another account if both have the same mail address set.
+					{{_("Do a sanity check here. A user can take over another account if both have the same mail address set.")}}
 				</small>
 			</div>
 			<div class="form-group col">
-				<label for="user-loginname">Password</label>
+				<label for="user-loginname">{{_("Password")}}</label>
 				{% if user.uid %}
 				<input type="password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●">
 				{% else %}
-				<input type="password" class="form-control" id="user-password" name="password" placeholder="mail to set it will be sent" readonly>
+				<input type="password" class="form-control" id="user-password" name="password" placeholder="{{_("mail to set it will be sent")}}" readonly>
 				{% endif %}
 				<small class="form-text text-muted">
-					At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.
+					{{_("At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.")}}
 				</small>
 			</div>
 		</div>
 		<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
 			<div class="form-group col">
-				<span>Roles:</span>
+				<span>{{_("Roles")}}:</span>
 				<table class="table table-striped table-sm">
 					<thead>
 						<tr>
 							<th scope="col"></th>
-							<th scope="col">name</th>
-							<th scope="col">description</th>
+							<th scope="col">{{_("name")}}</th>
+							<th scope="col">{{_("description")}}</th>
 						</tr>
 					</thead>
 					<tbody>
@@ -123,12 +123,12 @@
 				</table>
 			</div>
 			<div class="form-group col">
-				<span>Resulting groups (only updated after save):</span>
+				<span>{{_("Resulting groups (only updated after save)")}}:</span>
 				<table class="table table-striped table-sm">
 					<thead>
 						<tr>
-							<th scope="col">name</th>
-							<th scope="col">description</th>
+							<th scope="col">{{_("name")}}</th>
+							<th scope="col">{{_("description")}}</th>
 						</tr>
 					</thead>
 					<tbody>
diff --git a/uffd/user/views_group.py b/uffd/user/views_group.py
index d4318b3c..c9c80fde 100644
--- a/uffd/user/views_group.py
+++ b/uffd/user/views_group.py
@@ -1,4 +1,5 @@
 from flask import Blueprint, render_template, url_for, redirect, flash, current_app, request
+from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
 from uffd.session import login_required
@@ -10,14 +11,14 @@ bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/grou
 @login_required()
 def group_acl(): #pylint: disable=inconsistent-return-statements
 	if not group_acl_check():
-		flash('Access denied')
+		flash(_('Access denied'))
 		return redirect(url_for('index'))
 
 def group_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
 @bp.route("/")
-@register_navbar('Groups', icon='layer-group', blueprint=bp, visible=group_acl_check)
+@register_navbar(lazy_gettext('Groups'), icon='layer-group', blueprint=bp, visible=group_acl_check)
 def index():
 	return render_template('group/list.html', groups=Group.query.all())
 
diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py
index 193a3507..969b71b4 100644
--- a/uffd/user/views_user.py
+++ b/uffd/user/views_user.py
@@ -2,6 +2,7 @@ import csv
 import io
 
 from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
+from flask_babel import gettext as _, lazy_gettext
 
 from uffd.navbar import register_navbar
 from uffd.csrf import csrf_protect
@@ -18,14 +19,14 @@ bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/
 @login_required()
 def user_acl(): #pylint: disable=inconsistent-return-statements
 	if not user_acl_check():
-		flash('Access denied')
+		flash(_('Access denied'))
 		return redirect(url_for('index'))
 
 def user_acl_check():
 	return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP'])
 
 @bp.route("/")
-@register_navbar('Users', icon='users', blueprint=bp, visible=user_acl_check)
+@register_navbar(lazy_gettext('Users'), icon='users', blueprint=bp, visible=user_acl_check)
 def index():
 	return render_template('user/list.html', users=User.query.all())
 
@@ -45,16 +46,16 @@ def update(uid=None):
 		if request.form.get('serviceaccount'):
 			user.is_service_user = True
 		if not user.set_loginname(request.form['loginname'], ignore_blacklist=ignore_blacklist):
-			flash('Login name does not meet requirements')
+			flash(_('Login name does not meet requirements'))
 			return redirect(url_for('user.show'))
 	else:
 		user = User.query.filter_by(uid=uid).first_or_404()
 	if user.mail != request.form['mail'] and not user.set_mail(request.form['mail']):
-		flash('Mail is invalid')
+		flash(_('Mail is invalid'))
 		return redirect(url_for('user.show', uid=uid))
 	new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname']
 	if user.displayname != new_displayname and not user.set_displayname(new_displayname):
-		flash('Display name does not meet requirements')
+		flash(_('Display name does not meet requirements'))
 		return redirect(url_for('user.show', uid=uid))
 	new_password = request.form.get('password')
 	if uid is not None and new_password:
@@ -71,12 +72,12 @@ def update(uid=None):
 	db.session.commit()
 	if uid is None:
 		if user.is_service_user:
-			flash('Service user created')
+			flash(_('Service user created'))
 		else:
 			send_passwordreset(user, new=True)
-			flash('User created. We sent the user a password reset link by mail')
+			flash(_('User created. We sent the user a password reset link by mail'))
 	else:
-		flash('User updated')
+		flash(_('User updated'))
 	return redirect(url_for('user.show', uid=user.uid))
 
 @bp.route("/<int:uid>/del")
@@ -87,7 +88,7 @@ def delete(uid):
 	ldap.session.delete(user)
 	ldap.session.commit()
 	db.session.commit()
-	flash('Deleted user')
+	flash(_('Deleted user'))
 	return redirect(url_for('user.index'))
 
 @bp.route("/csv", methods=['POST'])
-- 
GitLab