diff --git a/.gitignore b/.gitignore
index 0bd1163..8eb4757 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
/.local
__pycache__
/snikket_web/static/css/*.css
+/snikket_web/translations/messages.pot
+/snikket_web/translations/*/LC_MESSAGES/*.mo
diff --git a/Makefile b/Makefile
index ca09a10..2c37075 100644
--- a/Makefile
+++ b/Makefile
@@ -2,9 +2,14 @@ scss_files = $(filter-out snikket_web/scss/_%.scss,$(wildcard snikket_web/scss/*
scss_includes = $(filter snikket_web/scss/_%.scss,$(wildcard snikket_web/scss/*.scss))
generated_css_files = $(patsubst snikket_web/scss/%.scss,snikket_web/static/css/%.css,$(scss_files))
+translation_basepath = snikket_web/translations
+pot_file = $(translation_basepath)/messages.pot
+
PYTHON3 ?= python3
SCSSC ?= $(PYTHON3) -m scss --load-path snikket_web/scss/
+all: build_css compile_translations
+
build_css: $(generated_css_files)
$(generated_css_files): snikket_web/static/css/%.css: snikket_web/scss/%.scss $(scss_includes)
@@ -13,4 +18,12 @@ $(generated_css_files): snikket_web/static/css/%.css: snikket_web/scss/%.scss $(
clean:
rm -f $(generated_css_files)
-.PHONY: build_css clean
+update_translations:
+ pybabel extract -F babel.cfg -k _l -o $(pot_file) .
+ pybabel update -i $(pot_file) -d $(translation_basepath)
+
+compile_translations:
+ pybabel compile -d $(translation_basepath)
+
+
+.PHONY: build_css clean update_translations compile_translations
diff --git a/babel.cfg b/babel.cfg
new file mode 100644
index 0000000..9ba026f
--- /dev/null
+++ b/babel.cfg
@@ -0,0 +1,3 @@
+[python: snikket_web/**.py]
+[jinja2: snikket_web/templates/**.html]
+extensions=jinja2.ext.autoescape,jinja2.ext.with_
diff --git a/requirements.txt b/requirements.txt
index 76e75ef..9328045 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@ aiohttp~=3.6
quart~=0.11
flask-wtf~=0.14
hsluv~=0.0.2
+flask-babel~=1.0
diff --git a/snikket_web/__init__.py b/snikket_web/__init__.py
index 0102895..958c29c 100644
--- a/snikket_web/__init__.py
+++ b/snikket_web/__init__.py
@@ -10,17 +10,29 @@ from quart import (
current_app,
)
+from flask_babel import Babel
+
from . import colour
from .prosodyclient import client
from ._version import version, version_info
app = Quart(__name__)
+app.config.setdefault("LANGUAGES", ["de", "en"])
app.config.from_envvar("SNIKKET_WEB_CONFIG")
client.init_app(app)
client.default_login_redirect = "login"
+babel = Babel(app)
+
+
+@babel.localeselector
+def select_locale():
+ return request.accept_languages.best_match(
+ current_app.config['LANGUAGES']
+ )
+
@app.route("/login", methods=["GET", "POST"])
async def login():
diff --git a/snikket_web/templates/app.html b/snikket_web/templates/app.html
index 3015e5c..237a6dc 100644
--- a/snikket_web/templates/app.html
+++ b/snikket_web/templates/app.html
@@ -15,6 +15,6 @@
{% block content %}{% endblock %}
{% endblock %}
diff --git a/snikket_web/templates/login.html b/snikket_web/templates/login.html
index fdff05a..f94878b 100644
--- a/snikket_web/templates/login.html
+++ b/snikket_web/templates/login.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% set body_id = "login" %}
{% block head_lead %}
-
Snikket Web Portal
+{{ _("Snikket Login") }}
{% endblock %}
{% block style %}
@@ -10,22 +10,22 @@
{% block body %}
{% endblock %}
diff --git a/snikket_web/templates/user_home.html b/snikket_web/templates/user_home.html
index 3c7795b..2723a7a 100644
--- a/snikket_web/templates/user_home.html
+++ b/snikket_web/templates/user_home.html
@@ -1,19 +1,18 @@
{% extends "app.html" %}
{% block content %}
-Welcome!
-Welcome home, {{ user_info.display_name }}.
+{% trans %}Welcome!{% endtrans %}
+{% trans user_name=user_info.display_name %}Welcome home, {{ user_name }}.{% endtrans %}
{% endblock %}
diff --git a/snikket_web/templates/user_passwd.html b/snikket_web/templates/user_passwd.html
index d13e472..f36f552 100644
--- a/snikket_web/templates/user_passwd.html
+++ b/snikket_web/templates/user_passwd.html
@@ -4,12 +4,12 @@
{% endblock %}
{% block content %}
{% endblock %}
diff --git a/snikket_web/templates/user_profile.html b/snikket_web/templates/user_profile.html
index d790f2e..3c079cf 100644
--- a/snikket_web/templates/user_profile.html
+++ b/snikket_web/templates/user_profile.html
@@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
{% endblock %}
diff --git a/snikket_web/translations/de/LC_MESSAGES/messages.po b/snikket_web/translations/de/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..26721b8
--- /dev/null
+++ b/snikket_web/translations/de/LC_MESSAGES/messages.po
@@ -0,0 +1,140 @@
+# German translations for SnikketWeb.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the SnikketWeb project.
+# FIRST AUTHOR , 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: SnikketWeb 0.1.0\n"
+"Report-Msgid-Bugs-To: jonas@zombofant.net\n"
+"POT-Creation-Date: 2020-03-07 16:53+0100\n"
+"PO-Revision-Date: 2020-03-07 16:32+0100\n"
+"Last-Translator: Jonas Schäfer \n"
+"Language: de\n"
+"Language-Team: de \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.8.0\n"
+
+#: snikket_web/templates/app.html:18 snikket_web/templates/login.html:29
+#, python-format
+msgid "A Snikket server"
+msgstr "Ein Snikket-Server"
+
+#: snikket_web/templates/login.html:4
+msgid "Snikket Login"
+msgstr "Snikket Anmeldung"
+
+#: snikket_web/templates/login.html:13
+msgid "Enter your Snikket address and password to manage your account."
+msgstr "Gib deine Snikket-Adresse und -Passwort ein um dein Konto zu verwalten."
+
+#: snikket_web/templates/login.html:16 snikket_web/templates/login.html:17
+msgid "Address"
+msgstr "Adresse"
+
+#: snikket_web/templates/login.html:20 snikket_web/templates/login.html:21
+msgid "Password"
+msgstr "Passwort"
+
+#: snikket_web/templates/login.html:24
+msgid "Log in"
+msgstr "Anmelden"
+
+#: snikket_web/templates/user_home.html:3
+msgid "Welcome!"
+msgstr "Willkommen!"
+
+#: snikket_web/templates/user_home.html:4
+#, python-format
+msgid "Welcome home, %(user_name)s."
+msgstr "Willkommen zu Hause, %(user_name)s."
+
+#: snikket_web/templates/user_home.html:7
+msgid "Update profile"
+msgstr "Profil bearbeiten"
+
+#: snikket_web/templates/user_home.html:8
+msgid ""
+"Change display name, set avatar and configure visibility of your personal"
+" data to others."
+msgstr ""
+"Ändere deinen Namen oder dein Bild und wer deine persönlichen Daten "
+"einsehen kann."
+
+#: snikket_web/templates/user_home.html:11
+#: snikket_web/templates/user_passwd.html:36
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: snikket_web/templates/user_home.html:14
+msgid "Log out"
+msgstr "Abmelden"
+
+#: snikket_web/templates/user_home.html:15
+msgid "Exit the Snikket Web Portal, without logging out your other devices."
+msgstr ""
+"Verlasse das Snikket Web-Portal, ohne dass deine anderen Geräte "
+"beeinträchtigt werden."
+
+#: snikket_web/templates/user_passwd.html:7
+msgid "Change your password"
+msgstr "Ändere dein Passwort"
+
+#: snikket_web/templates/user_passwd.html:8
+msgid ""
+"To change your password, you need to provide the current password as well"
+" as the new one. To reduce the chance of typos, we ask for your new "
+"password twice."
+msgstr ""
+"Um dein Passwort zu ändern musst du sowohl dein aktuelles Passwort "
+"alsauch dein neues Passwort angeben. Damit Tippfehler dich nicht "
+"aussperren bitten wir dich, dein neues Passwort zweimal einzutippen."
+
+#: snikket_web/templates/user_passwd.html:12
+msgid "Password change failed"
+msgstr "Passwortänderung fehlgeschlagen"
+
+#: snikket_web/templates/user_passwd.html:35
+#: snikket_web/templates/user_profile.html:18
+msgid "Back"
+msgstr "Zurück"
+
+#: snikket_web/templates/user_profile.html:7
+msgid "Profile"
+msgstr "Profil"
+
+#: snikket_web/templates/user_profile.html:18
+msgid "Apply"
+msgstr "Übernehmen"
+
+#: snikket_web/user/__init__.py:21
+msgid "Current password"
+msgstr "Aktuelles Passwort"
+
+#: snikket_web/user/__init__.py:26
+msgid "New password"
+msgstr "Neues Passwort"
+
+#: snikket_web/user/__init__.py:31
+msgid "Confirm new password"
+msgstr "Neues Passwort (Bestätigung)"
+
+#: snikket_web/user/__init__.py:35
+msgid "The new passwords must match."
+msgstr "Die neuen Passwörter müssen übereinstimmen."
+
+#: snikket_web/user/__init__.py:46
+msgid "Display name"
+msgstr "Anzeigename"
+
+#: snikket_web/user/__init__.py:50
+msgid "Avatar"
+msgstr "Bild"
+
+#: snikket_web/user/__init__.py:74
+msgid "Incorrect password"
+msgstr "Ungültiges Passwort"
+
diff --git a/snikket_web/translations/en/LC_MESSAGES/messages.po b/snikket_web/translations/en/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..c6a9556
--- /dev/null
+++ b/snikket_web/translations/en/LC_MESSAGES/messages.po
@@ -0,0 +1,138 @@
+# English translations for PROJECT.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2020-03-07 16:53+0100\n"
+"PO-Revision-Date: 2020-03-07 16:50+0100\n"
+"Last-Translator: FULL NAME \n"
+"Language: en\n"
+"Language-Team: en \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.8.0\n"
+
+#: snikket_web/templates/app.html:18 snikket_web/templates/login.html:29
+#, python-format
+msgid "A Snikket server"
+msgstr "A Snikket server"
+
+#: snikket_web/templates/login.html:4
+msgid "Snikket Login"
+msgstr "Snikket Login"
+
+#: snikket_web/templates/login.html:13
+msgid "Enter your Snikket address and password to manage your account."
+msgstr "Enter your Snikket address and password to manage your account."
+
+#: snikket_web/templates/login.html:16 snikket_web/templates/login.html:17
+msgid "Address"
+msgstr "Address"
+
+#: snikket_web/templates/login.html:20 snikket_web/templates/login.html:21
+msgid "Password"
+msgstr "Password"
+
+#: snikket_web/templates/login.html:24
+msgid "Log in"
+msgstr "Log in"
+
+#: snikket_web/templates/user_home.html:3
+msgid "Welcome!"
+msgstr "Welcome!"
+
+#: snikket_web/templates/user_home.html:4
+#, python-format
+msgid "Welcome home, %(user_name)s."
+msgstr "Welcome home, %(user_name)s."
+
+#: snikket_web/templates/user_home.html:7
+msgid "Update profile"
+msgstr "Update profile"
+
+#: snikket_web/templates/user_home.html:8
+msgid ""
+"Change display name, set avatar and configure visibility of your personal"
+" data to others."
+msgstr ""
+"Change display name, set avatar and configure visibility of your personal"
+" data to others."
+
+#: snikket_web/templates/user_home.html:11
+#: snikket_web/templates/user_passwd.html:36
+msgid "Change password"
+msgstr "Change password"
+
+#: snikket_web/templates/user_home.html:14
+msgid "Log out"
+msgstr "Log out"
+
+#: snikket_web/templates/user_home.html:15
+msgid "Exit the Snikket Web Portal, without logging out your other devices."
+msgstr "Exit the Snikket Web Portal, without logging out your other devices."
+
+#: snikket_web/templates/user_passwd.html:7
+msgid "Change your password"
+msgstr "Change your password"
+
+#: snikket_web/templates/user_passwd.html:8
+msgid ""
+"To change your password, you need to provide the current password as well"
+" as the new one. To reduce the chance of typos, we ask for your new "
+"password twice."
+msgstr ""
+"To change your password, you need to provide the current password as well"
+" as the new one. To reduce the chance of typos, we ask for your new "
+"password twice."
+
+#: snikket_web/templates/user_passwd.html:12
+msgid "Password change failed"
+msgstr "Password change failed"
+
+#: snikket_web/templates/user_passwd.html:35
+#: snikket_web/templates/user_profile.html:18
+msgid "Back"
+msgstr "Back"
+
+#: snikket_web/templates/user_profile.html:7
+msgid "Profile"
+msgstr "Profile"
+
+#: snikket_web/templates/user_profile.html:18
+msgid "Apply"
+msgstr "Apply"
+
+#: snikket_web/user/__init__.py:21
+msgid "Current password"
+msgstr "Current password"
+
+#: snikket_web/user/__init__.py:26
+msgid "New password"
+msgstr "New password"
+
+#: snikket_web/user/__init__.py:31
+msgid "Confirm new password"
+msgstr "Confirm new password"
+
+#: snikket_web/user/__init__.py:35
+msgid "The new passwords must match."
+msgstr "The new passwords must match."
+
+#: snikket_web/user/__init__.py:46
+msgid "Display name"
+msgstr "Display name"
+
+#: snikket_web/user/__init__.py:50
+msgid "Avatar"
+msgstr "Avatar"
+
+#: snikket_web/user/__init__.py:74
+msgid "Incorrect password"
+msgstr "Incorrect password"
+
diff --git a/snikket_web/user/__init__.py b/snikket_web/user/__init__.py
index 88d80aa..a575ba0 100644
--- a/snikket_web/user/__init__.py
+++ b/snikket_web/user/__init__.py
@@ -3,6 +3,7 @@ import quart.flask_patch
from quart import Blueprint, render_template, request, redirect, url_for
import quart.exceptions
from flask_wtf import FlaskForm
+from flask_babel import lazy_gettext as _l, _
import wtforms
from snikket_web.prosodyclient import client
@@ -17,25 +18,21 @@ async def proc():
class ChangePasswordForm(FlaskForm):
current_password = wtforms.PasswordField(
- # TODO(i18n)
- "Current password",
+ _l("Current password"),
validators=[wtforms.validators.InputRequired()]
)
new_password = wtforms.PasswordField(
- # TODO(i18n)
- "New password",
+ _l("New password"),
validators=[wtforms.validators.InputRequired()]
)
new_password_confirm = wtforms.PasswordField(
- # TODO(i18n)
- "Confirm new password",
+ _l("Confirm new password"),
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"new_password",
- # TODO(i18n)
- "The new passwords must match."
+ _l("The new passwords must match.")
)]
)
@@ -46,11 +43,11 @@ class LogoutForm(FlaskForm):
class ProfileForm(FlaskForm):
nickname = wtforms.TextField(
- "Display name",
+ _l("Display name"),
)
avatar = wtforms.FileField(
- "Avatar"
+ _l("Avatar")
)
@@ -74,8 +71,7 @@ async def change_pw():
except quart.exceptions.Unauthorized:
# server refused current password, set an appropriate error
form.errors.setdefault(form.current_password.name, []).append(
- # TODO(i18n)
- "Incorrect password",
+ _("Incorrect password"),
)
else:
return redirect(url_for("user.change_pw"))