diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b2c1060..dbaea9a 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -50,6 +50,29 @@ jobs: run: | python -m flake8 snikket_web + translation-check: + runs-on: ubuntu-latest + + name: 'lint: i18n' + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install + run: | + set -euo pipefail + pip install flask-babel + - name: Linting + run: | + sed -ri '/^"POT-Creation-Date: /d' snikket_web/translations/messages.pot + git add snikket_web/translations/messages.pot + make extract_translations + sed -ri '/^"POT-Creation-Date: /d' snikket_web/translations/messages.pot + git diff --exit-code --color -- snikket_web/translations/messages.pot + + build: runs-on: ubuntu-latest diff --git a/babel.cfg b/babel.cfg index d82fb6e..4a6f19b 100644 --- a/babel.cfg +++ b/babel.cfg @@ -1,4 +1,3 @@ [python: snikket_web/**.py] [jinja2: snikket_web/templates/**.html] [jinja2: snikket_web/templates/**.j2] -extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 5aeb33d..8fcae91 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -5,4 +5,4 @@ export SNIKKET_WEB_DOMAIN="$SNIKKET_DOMAIN" export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE-127.0.0.1}" export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT-5765}" -exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" 'snikket_web:create_app()' +exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" --access-logfile=- --log-file=- 'snikket_web:create_app()' diff --git a/requirements.txt b/requirements.txt index 1098981..c1dfa3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ hsluv~=5.0 flask-babel~=1.0 email-validator~=1.1 environ-config~=20.0 -wtforms~=2.3 +wtforms~=3.0 typing-extensions diff --git a/snikket_web/__init__.py b/snikket_web/__init__.py index b352cc1..493fca3 100644 --- a/snikket_web/__init__.py +++ b/snikket_web/__init__.py @@ -147,9 +147,13 @@ class AppConfig: site_name = environ.var("") avatar_cache_ttl = environ.var(1800, converter=int) languages = environ.var([ + # Keep `en` as the first language, because it is used as a fallback + # if the language negotiation cannot find another match. It is more + # likely that users are able to read english (or find a suitable + # online translator) than, for instance, danish. + "en", "da", "de", - "en", "fr", "id", "it", diff --git a/snikket_web/admin.py b/snikket_web/admin.py index afcb9b7..05fe166 100644 --- a/snikket_web/admin.py +++ b/snikket_web/admin.py @@ -12,7 +12,6 @@ import werkzeug.exceptions import quart.flask_patch import wtforms -import wtforms.fields.html5 from quart import ( Blueprint, diff --git a/snikket_web/infra.py b/snikket_web/infra.py index c5e9ef0..12ce581 100644 --- a/snikket_web/infra.py +++ b/snikket_web/infra.py @@ -8,6 +8,7 @@ import quart.flask_patch # noqa:F401 from quart import ( current_app, request, + g, ) import flask_babel @@ -34,6 +35,7 @@ BYTE_UNIT_SCALE_MAP = [ @babel.localeselector # type:ignore def selected_locale() -> str: + g.language_header_accessed = True selected = request.accept_languages.best_match( current_app.config['LANGUAGES'] ) or current_app.config['LANGUAGES'][0] @@ -68,6 +70,12 @@ def format_bytes(n: float) -> str: return "{} {}".format(n, unit) +def add_vary_language_header(resp: quart.Response) -> quart.Response: + if getattr(g, "language_header_accessed", False): + resp.vary.add("Accept-Language") + return resp + + def init_templating(app: quart.Quart) -> None: app.template_filter("repr")(repr) app.template_filter("format_datetime")(flask_babel.format_datetime) @@ -78,6 +86,7 @@ def init_templating(app: quart.Quart) -> None: app.template_filter("format_bytes")(format_bytes) app.template_filter("flatten")(flatten) app.template_filter("circle_name")(circle_name) + app.after_request(add_vary_language_header) def generate_error_id() -> str: diff --git a/snikket_web/main.py b/snikket_web/main.py index 73b3ff2..286ee1d 100644 --- a/snikket_web/main.py +++ b/snikket_web/main.py @@ -34,7 +34,7 @@ bp = quart.Blueprint("main", __name__) class LoginForm(BaseForm): - address = wtforms.TextField( + address = wtforms.StringField( _l("Address"), validators=[wtforms.validators.InputRequired()], ) @@ -93,10 +93,16 @@ async def login() -> typing.Union[str, werkzeug.Response]: @bp.route("/meta/about.html") async def about() -> str: version = None + core_versions = {} extra_versions = {} - if current_app.debug or client.is_admin_session: version = _version.version + try: + core_versions["Prosody"] = await client.get_server_version() + except werkzeug.exceptions.Unauthorized: + core_versions["Prosody"] = "unknown" + + if current_app.debug: extra_versions["aiohttp"] = aiohttp.__version__ extra_versions["babel"] = babel.__version__ extra_versions["wtforms"] = wtforms.__version__ @@ -110,6 +116,7 @@ async def about() -> str: "about.html", version=version, extra_versions=extra_versions, + core_versions=core_versions, ) diff --git a/snikket_web/templates/about.html b/snikket_web/templates/about.html index 001eb50..b6e4b05 100644 --- a/snikket_web/templates/about.html +++ b/snikket_web/templates/about.html @@ -17,9 +17,12 @@

{% trans %}Trademarks{% endtrans %}

{% trans trademarks_url="https://snikket.org/about/trademarks/" %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company. For more information about the trademarks, visit the Snikket Trademarks information page.{% endtrans %}

{% trans %}Software Versions{% endtrans %}

-
Snikket Server
-Domain: {{ config["SNIKKET_DOMAIN"] }}
-Snikket Web Portal{% if version %} ({{ version }}){% endif %}
+		
Domain: {{ config["SNIKKET_DOMAIN"] }}
+Web Portal{% if version %} ({{ version }}){% endif %}
+{%- if core_versions -%}
+{% for name, version in core_versions.items() %}
+{{ name }} ({{ version }}){% endfor %}
+{%- endif -%}
 {%- if extra_versions -%}
 {% for name, version in extra_versions.items() %}
 {{ name }} ({{ version }}){% endfor %}
diff --git a/snikket_web/templates/user_manage_data.html b/snikket_web/templates/user_manage_data.html
index 0f958e8..8fcb9e6 100644
--- a/snikket_web/templates/user_manage_data.html
+++ b/snikket_web/templates/user_manage_data.html
@@ -11,6 +11,8 @@
 			{% call render_errors(form) %}{% endcall %}
 
 			
+ {%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%} +
{{ form.csrf_token }} {%- call form_button("download", form.action_export, class="primary") %}{% endcall -%} diff --git a/snikket_web/translations/messages.pot b/snikket_web/translations/messages.pot index 3589ae5..d8ff848 100644 --- a/snikket_web/translations/messages.pot +++ b/snikket_web/translations/messages.pot @@ -8,287 +8,287 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-01-17 17:27+0100\n" +"POT-Creation-Date: 2022-06-06 19:52+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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" +"Generated-By: Babel 2.10.1\n" -#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:10 +#: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10 #: snikket_web/templates/admin_edit_circle.html:59 #: snikket_web/templates/admin_users.html:8 msgid "Login name" msgstr "" -#: snikket_web/admin.py:72 snikket_web/templates/admin_delete_user.html:12 +#: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12 #: snikket_web/templates/admin_edit_circle.html:60 #: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63 msgid "Display name" msgstr "" -#: snikket_web/admin.py:76 snikket_web/templates/admin_edit_user.html:32 +#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:32 msgid "Access Level" msgstr "" -#: snikket_web/admin.py:78 +#: snikket_web/admin.py:79 msgid "Limited" msgstr "" -#: snikket_web/admin.py:79 +#: snikket_web/admin.py:80 msgid "Normal user" msgstr "" -#: snikket_web/admin.py:80 +#: snikket_web/admin.py:81 msgid "Administrator" msgstr "" -#: snikket_web/admin.py:85 +#: snikket_web/admin.py:86 msgid "Update user" msgstr "" -#: snikket_web/admin.py:89 +#: snikket_web/admin.py:90 msgid "Create password reset link" msgstr "" -#: snikket_web/admin.py:107 +#: snikket_web/admin.py:108 msgid "Password reset link created" msgstr "" -#: snikket_web/admin.py:122 +#: snikket_web/admin.py:123 msgid "User information updated." msgstr "" -#: snikket_web/admin.py:144 +#: snikket_web/admin.py:145 msgid "Delete user permanently" msgstr "" -#: snikket_web/admin.py:157 +#: snikket_web/admin.py:158 msgid "User deleted" msgstr "" -#: snikket_web/admin.py:195 +#: snikket_web/admin.py:196 msgid "Password reset link not found" msgstr "" -#: snikket_web/admin.py:207 +#: snikket_web/admin.py:208 msgid "Password reset link deleted" msgstr "" -#: snikket_web/admin.py:227 +#: snikket_web/admin.py:228 msgid "Invite to circle" msgstr "" -#: snikket_web/admin.py:233 +#: snikket_web/admin.py:234 msgid "At least one circle must be selected" msgstr "" -#: snikket_web/admin.py:238 +#: snikket_web/admin.py:239 msgid "Valid for" msgstr "" -#: snikket_web/admin.py:240 +#: snikket_web/admin.py:241 msgid "One hour" msgstr "" -#: snikket_web/admin.py:241 +#: snikket_web/admin.py:242 msgid "Twelve hours" msgstr "" -#: snikket_web/admin.py:242 +#: snikket_web/admin.py:243 msgid "One day" msgstr "" -#: snikket_web/admin.py:243 +#: snikket_web/admin.py:244 msgid "One week" msgstr "" -#: snikket_web/admin.py:244 +#: snikket_web/admin.py:245 msgid "Four weeks" msgstr "" -#: snikket_web/admin.py:250 snikket_web/templates/admin_edit_invite.html:17 +#: snikket_web/admin.py:251 snikket_web/templates/admin_edit_invite.html:17 msgid "Invitation type" msgstr "" -#: snikket_web/admin.py:252 snikket_web/templates/library.j2:116 +#: snikket_web/admin.py:253 snikket_web/templates/library.j2:116 msgid "Individual" msgstr "" -#: snikket_web/admin.py:253 snikket_web/templates/library.j2:114 +#: snikket_web/admin.py:254 snikket_web/templates/library.j2:114 msgid "Group" msgstr "" -#: snikket_web/admin.py:259 +#: snikket_web/admin.py:260 msgid "New invitation link" msgstr "" -#: snikket_web/admin.py:321 +#: snikket_web/admin.py:322 msgid "Revoke" msgstr "" -#: snikket_web/admin.py:345 +#: snikket_web/admin.py:346 msgid "Invitation created" msgstr "" -#: snikket_web/admin.py:361 +#: snikket_web/admin.py:362 msgid "No such invitation exists" msgstr "" -#: snikket_web/admin.py:376 +#: snikket_web/admin.py:377 msgid "Invitation revoked" msgstr "" -#: snikket_web/admin.py:393 snikket_web/admin.py:441 +#: snikket_web/admin.py:394 snikket_web/admin.py:442 msgid "Name" msgstr "" -#: snikket_web/admin.py:398 snikket_web/templates/admin_circles.html:47 +#: snikket_web/admin.py:399 snikket_web/templates/admin_circles.html:47 msgid "Create circle" msgstr "" -#: snikket_web/admin.py:428 +#: snikket_web/admin.py:429 msgid "Circle created" msgstr "" -#: snikket_web/admin.py:446 +#: snikket_web/admin.py:447 msgid "Select user" msgstr "" -#: snikket_web/admin.py:451 +#: snikket_web/admin.py:452 msgid "Update circle" msgstr "" -#: snikket_web/admin.py:455 +#: snikket_web/admin.py:456 msgid "Delete circle permanently" msgstr "" -#: snikket_web/admin.py:461 +#: snikket_web/admin.py:462 msgid "Add user" msgstr "" -#: snikket_web/admin.py:477 +#: snikket_web/admin.py:478 msgid "No such circle exists" msgstr "" -#: snikket_web/admin.py:514 +#: snikket_web/admin.py:515 msgid "Circle data updated" msgstr "" -#: snikket_web/admin.py:520 +#: snikket_web/admin.py:521 msgid "Circle deleted" msgstr "" -#: snikket_web/admin.py:531 +#: snikket_web/admin.py:532 msgid "User added to circle" msgstr "" -#: snikket_web/admin.py:540 +#: snikket_web/admin.py:541 msgid "User removed from circle" msgstr "" -#: snikket_web/admin.py:609 +#: snikket_web/admin.py:610 msgid "Message contents" msgstr "" -#: snikket_web/admin.py:615 +#: snikket_web/admin.py:616 msgid "Only send to online users" msgstr "" -#: snikket_web/admin.py:619 +#: snikket_web/admin.py:620 msgid "Post to all users" msgstr "" -#: snikket_web/admin.py:623 +#: snikket_web/admin.py:624 msgid "Send preview to yourself" msgstr "" -#: snikket_web/admin.py:645 +#: snikket_web/admin.py:646 msgid "Announcement sent!" msgstr "" -#: snikket_web/infra.py:51 +#: snikket_web/infra.py:53 msgid "Main" msgstr "" -#: snikket_web/invite.py:33 +#: snikket_web/invite.py:35 msgid "" "The account data you tried to import is too large to upload. Please " "contact your Snikket operator." msgstr "" -#: snikket_web/invite.py:112 +#: snikket_web/invite.py:114 msgid "Username" msgstr "" -#: snikket_web/invite.py:116 snikket_web/invite.py:184 snikket_web/main.py:41 +#: snikket_web/invite.py:118 snikket_web/invite.py:186 snikket_web/main.py:43 msgid "Password" msgstr "" -#: snikket_web/invite.py:120 snikket_web/invite.py:188 +#: snikket_web/invite.py:122 snikket_web/invite.py:190 msgid "Confirm password" msgstr "" -#: snikket_web/invite.py:124 snikket_web/invite.py:192 +#: snikket_web/invite.py:126 snikket_web/invite.py:194 msgid "The passwords must match." msgstr "" -#: snikket_web/invite.py:129 +#: snikket_web/invite.py:131 msgid "Create account" msgstr "" -#: snikket_web/invite.py:156 +#: snikket_web/invite.py:158 msgid "That username is already taken." msgstr "" -#: snikket_web/invite.py:160 snikket_web/invite.py:225 +#: snikket_web/invite.py:162 snikket_web/invite.py:227 msgid "Registration was declined for unknown reasons." msgstr "" -#: snikket_web/invite.py:164 +#: snikket_web/invite.py:166 msgid "The username is not valid." msgstr "" -#: snikket_web/invite.py:197 snikket_web/templates/user_home.html:32 +#: snikket_web/invite.py:199 snikket_web/templates/user_home.html:32 #: snikket_web/templates/user_passwd.html:29 msgid "Change password" msgstr "" -#: snikket_web/invite.py:244 +#: snikket_web/invite.py:246 msgid "Account data file" msgstr "" -#: snikket_web/invite.py:248 +#: snikket_web/invite.py:250 msgid "Import data" msgstr "" -#: snikket_web/invite.py:269 +#: snikket_web/invite.py:271 #, python-format msgid "" "The account data you tried to import is in an unknown format. Please " "upload an XML file in XEP-0227 format (provided format: %(mimetype)s)." msgstr "" -#: snikket_web/invite.py:289 snikket_web/templates/unauth.html:18 +#: snikket_web/invite.py:291 snikket_web/templates/unauth.html:18 #: snikket_web/user.py:178 msgid "Error" msgstr "" -#: snikket_web/main.py:36 +#: snikket_web/main.py:38 msgid "Address" msgstr "" -#: snikket_web/main.py:46 +#: snikket_web/main.py:48 msgid "Sign in" msgstr "" -#: snikket_web/main.py:55 +#: snikket_web/main.py:57 msgid "Invalid username or password." msgstr "" -#: snikket_web/main.py:83 +#: snikket_web/main.py:85 msgid "Login successful!" msgstr "" @@ -445,7 +445,7 @@ msgstr "" msgid "Software Versions" msgstr "" -#: snikket_web/templates/about.html:29 +#: snikket_web/templates/about.html:32 msgid "Back to the main page" msgstr "" @@ -580,6 +580,7 @@ msgstr "" #: snikket_web/templates/admin_delete_user.html:19 #: snikket_web/templates/admin_reset_user_password.html:25 #: snikket_web/templates/user_logout.html:10 +#: snikket_web/templates/user_manage_data.html:14 #: snikket_web/templates/user_passwd.html:27 #: snikket_web/templates/user_profile.html:32 msgid "Back" diff --git a/snikket_web/user.py b/snikket_web/user.py index beb2539..885f1b7 100644 --- a/snikket_web/user.py +++ b/snikket_web/user.py @@ -59,7 +59,7 @@ _ACCESS_MODEL_CHOICES = [ class ProfileForm(BaseForm): - nickname = wtforms.TextField( + nickname = wtforms.StringField( _l("Display name"), )