Compare commits

..

22 Commits

Author SHA1 Message Date
Jonas Schäfer
f2c79044e0 Clean up post-merge lint
I am a *bit* sorry for this commit, because ideally this would've been
folded into 6d50b1c2c7 and whatever the
source of the other "conflict" was.

However, as the things have been merged in a batch, I can't do much more
than this.
2022-06-06 19:52:24 +02:00
Jonas Schäfer
13bc4bb227 Merge branch 'fix/babel-extraction' into premerge 2022-06-06 19:46:59 +02:00
Jonas Schäfer
f1351eb5cc Merge branch 'fix/use-english-default' into premerge 2022-06-06 19:46:49 +02:00
Jonas Schäfer
41573569af Merge branch 'feature/export-back-button' into premerge 2022-06-06 19:46:43 +02:00
Jonas Schäfer
b1f3026b8a Merge branch 'feature/wtforms-3' into premerge 2022-06-06 19:46:32 +02:00
Jonas Schäfer
6794314a59 Merge branch 'feature/vary-accept-language' into premerge 2022-06-06 19:46:25 +02:00
Jonas Schäfer
077e957a00 Merge branch 'feature/ci-extract-translations' into premerge 2022-06-06 19:46:19 +02:00
Jonas Schäfer
4902941145 Merge branch 'feature/strip-versions' into premerge 2022-06-06 19:46:09 +02:00
Jonas Schäfer
5222c8eafe Merge branch 'feature/hypercorn-stdout' into premerge 2022-06-06 19:44:39 +02:00
Jonas Schäfer
03ca7ac5bb Unbreak translation text extraction
It was broken because of the same jinja2 update (presumably) which
prompted 68f72743c5.
2022-05-30 20:51:37 +02:00
Matthew Wild
56cee8bab6 Merge pull request #135 from snikket-im/feature/update-dependencies
Update dependencies
2022-05-30 16:59:22 +01:00
Jonas Schäfer
b36fc0d5ac Bump hsluv to 5.x
Fixes #134.
2022-05-30 17:38:18 +02:00
Jonas Schäfer
68f72743c5 Bump quart to version 0.17
This is needed because jinja2 had an update which caused the portal to
not work at all:

```
ImportError: cannot import name 'escape' from 'jinja2'
```

Quart needed updating for that.

This update required a lot of typefixes. Apparently, the "canned"
responses (like redirect) are now plain werkzeug responses, while
quart.Response does not inherit from werkzeug.Response (otherwise, we
could've changed the type annotations to werkzeug.Response everywhere,
but that doesn't work because a quart.Response is not a
werkzeug.Response).

P.S.: This time, I *did* check that avatar uploads don't break (see
b007afc).
2022-05-30 17:37:54 +02:00
Daniel Holmgaard
8741efb2c4 Translated using Weblate (Danish)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/da/
2022-05-30 14:01:08 +00:00
Daniel Holmgaard
a0e8933b64 Translated using Weblate (Danish)
Currently translated at 95.6% (307 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/da/
2022-05-26 23:01:53 +00:00
Zack Zhou
edb3154127 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/zh_Hans/
2022-05-20 00:01:45 +00:00
Kim Alvefur
73fda3d623 Add a Back button from export panel for consistency
The other user related sections all have a Back button so it makes sense
that this one ought to have one as well.
2022-02-19 16:28:38 +01:00
Jonas Schäfer
a998348804 Make hypercorn log to stdout in Docker
This may help with debugging issues, because we now actually see
incoming requests also on the hypercorn layer.

Fixes #97.
2022-01-22 15:20:36 +01:00
Jonas Schäfer
20abe4b903 Add Vary: Accept-Language to all pages using that information
It was found during testing that some user agents cache aggressively
even between switches of the UI language. To properly indicate that the
pages actually depend on that information, we add the correct Vary
header.

Fixes #106.
2022-01-22 15:19:29 +01:00
Jonas Schäfer
a1ecb4ce80 Port to WTForms 3.x
Fixes #103.
2022-01-22 15:17:48 +01:00
Jonas Schäfer
b84b84b394 Add check for a missing make extract_translations
Forgetting to run that causes weblate (or other translation tools) to
show outdated strings and not import new strings, which is bad for the
collaboration.

Fixes #118.
2022-01-22 14:57:59 +01:00
Jonas Schäfer
6d50b1c2c7 Do not show dependency versions even to admins by default
Dependency versions are generally not useful, unless you are developing
or otherwise outside of a normal release situation: If you are on a
normal release, we can figure out the dep versions by looking at the
docker image.

To reduce the amount of information displayed and the amount of
information which needs to be conveyed in case of problems, we only show
the web portal and prosody versions to admins, unless debug mode is
enabled.

The behaviour that versions are only shown to logged in admins (unless
debug mode is enabled) remains unchanged.

Fixes #115.
2022-01-20 18:11:47 +01:00
19 changed files with 268 additions and 204 deletions

View File

@@ -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

View File

@@ -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_

View File

@@ -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()'

View File

@@ -1,9 +1,9 @@
aiohttp~=3.6
quart~=0.11,<0.15
quart~=0.17
flask-wtf~=0.14
hsluv~=0.0.2
hsluv~=5.0
flask-babel~=1.0
email-validator~=1.1
environ-config~=20.0
wtforms~=2.3
wtforms~=3.0
typing-extensions

View File

@@ -18,6 +18,8 @@ from quart import (
jsonify,
)
import werkzeug.exceptions
import environ
from . import colour, infra
@@ -40,7 +42,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
try:
user_info = await infra.client.get_user_info()
except (aiohttp.ClientError, quart.exceptions.HTTPException):
except (aiohttp.ClientError, werkzeug.exceptions.HTTPException):
user_info = {}
return {
@@ -105,16 +107,16 @@ async def backend_error_handler(exc: Exception) -> quart.Response:
async def generic_http_error(
exc: quart.exceptions.HTTPException,
exc: werkzeug.exceptions.HTTPException,
) -> quart.Response:
return quart.Response(
await render_template(
"generic_http_error.html",
status=exc.status_code,
status=exc.code,
description=exc.description,
name=exc.name,
),
status=exc.status_code,
status=exc.code,
)
@@ -204,19 +206,19 @@ def create_app() -> quart.Quart:
app.context_processor(proc)
app.register_error_handler(
aiohttp.ClientConnectorError,
backend_error_handler, # type:ignore
backend_error_handler,
)
app.register_error_handler(
quart.exceptions.HTTPException,
werkzeug.exceptions.HTTPException,
generic_http_error, # type:ignore
)
app.register_error_handler(
Exception,
generic_error_handler, # type:ignore
generic_error_handler,
)
@app.route("/")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
if infra.client.has_session:
return redirect(url_for('user.index'))

View File

@@ -7,10 +7,11 @@ from datetime import datetime
import aiohttp
import werkzeug.exceptions
import quart.flask_patch
import wtforms
import wtforms.fields.html5
from quart import (
Blueprint,
@@ -92,7 +93,7 @@ class EditUserForm(BaseForm):
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
target_user_info = await client.get_user_by_localpart(localpart)
form = EditUserForm()
@@ -147,7 +148,7 @@ class DeleteUserForm(BaseForm):
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"])
@client.require_admin_session()
async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
async def delete_user(localpart: str) -> typing.Union[str, werkzeug.Response]:
target_user_info = await client.get_user_by_localpart(localpart)
form = DeleteUserForm()
if form.validate_on_submit():
@@ -186,7 +187,7 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
@client.require_admin_session()
async def user_password_reset_link(
id_: str,
) -> typing.Union[str, quart.Response]:
) -> typing.Union[str, werkzeug.Response]:
invite_info = await client.get_invite_by_id(
id_,
)
@@ -278,7 +279,7 @@ class InvitePost(BaseForm):
@bp.route("/invitations", methods=["GET", "POST"])
@client.require_admin_session()
async def invitations() -> typing.Union[str, quart.Response]:
async def invitations() -> typing.Union[str, werkzeug.Response]:
invites = sorted(
(
invite
@@ -324,7 +325,7 @@ class InviteForm(BaseForm):
@bp.route("/invitation/-/new", methods=["POST"])
@client.require_admin_session()
async def create_invite() -> typing.Union[str, quart.Response]:
async def create_invite() -> typing.Union[str, werkzeug.Response]:
form = InvitePost()
circles = await client.list_groups()
form.circles.choices = [
@@ -352,7 +353,7 @@ async def create_invite() -> typing.Union[str, quart.Response]:
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
async def edit_invite(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite_info = await client.get_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -418,7 +419,7 @@ async def circles() -> str:
@bp.route("/circle/-/new", methods=["POST"])
@client.require_admin_session()
async def create_circle() -> typing.Union[str, quart.Response]:
async def create_circle() -> typing.Union[str, werkzeug.Response]:
create_form = CirclePost()
if create_form.validate_on_submit():
circle = await client.create_group(
@@ -464,7 +465,7 @@ class EditCircleForm(BaseForm):
@bp.route("/circle/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
async with client.authenticated_session() as session:
try:
circle = await client.get_group_by_id(
@@ -626,7 +627,7 @@ class AnnouncementForm(BaseForm):
@bp.route("/system/", methods=["GET", "POST"])
@client.require_admin_session()
async def system() -> typing.Union[str, quart.Response]:
async def system() -> typing.Union[str, werkzeug.Response]:
form = AnnouncementForm()
if form.validate_on_submit():
@@ -657,7 +658,7 @@ async def system() -> typing.Union[str, quart.Response]:
now = time.time()
try:
prosody_metrics = await client.get_system_metrics()
except quart.exceptions.NotFound:
except werkzeug.exceptions.NotFound:
# server does not offer the endpoint for whatever reason -- ignore
prosody_metrics = {}

View File

@@ -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:

View File

@@ -15,6 +15,8 @@ from quart import (
session as http_session,
)
import werkzeug
import wtforms
from flask_babel import lazy_gettext as _l, gettext
@@ -46,14 +48,14 @@ def apple_store_badge() -> str:
@bp.context_processor
def context() -> typing.Mapping[str, typing.Any]:
def context() -> typing.Dict[str, typing.Any]:
return {
"apple_store_badge": apple_store_badge,
}
@bp.route("/<id_>")
async def view_old(id_: str) -> quart.Response:
async def view_old(id_: str) -> werkzeug.Response:
return redirect(url_for(".view", id_=id_))
@@ -131,7 +133,7 @@ class RegisterForm(BaseForm):
@bp.route("/<id_>/register", methods=["GET", "POST"])
async def register(id_: str) -> typing.Union[str, quart.Response]:
async def register(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -199,7 +201,7 @@ class ResetForm(BaseForm):
@bp.route("/<id_>/reset", methods=["GET", "POST"])
async def reset(id_: str) -> typing.Union[str, quart.Response]:
async def reset(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -300,5 +302,5 @@ async def reset_success() -> str:
@bp.route("/-")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
return redirect(url_for("index"))

View File

@@ -18,6 +18,8 @@ from quart import (
flash,
)
import werkzeug.exceptions
import babel
import wtforms
@@ -32,7 +34,7 @@ bp = quart.Blueprint("main", __name__)
class LoginForm(BaseForm):
address = wtforms.TextField(
address = wtforms.StringField(
_l("Address"),
validators=[wtforms.validators.InputRequired()],
)
@@ -48,7 +50,7 @@ class LoginForm(BaseForm):
@bp.route("/-")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
return redirect(url_for("index"))
@@ -56,7 +58,7 @@ ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
@bp.route("/login", methods=["GET", "POST"])
async def login() -> typing.Union[str, quart.Response]:
async def login() -> typing.Union[str, werkzeug.Response]:
if client.has_session and (await client.test_session()):
return redirect(url_for('user.index'))
@@ -76,7 +78,7 @@ async def login() -> typing.Union[str, quart.Response]:
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
except werkzeug.exceptions.Unauthorized:
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
await flash(
@@ -91,24 +93,30 @@ async def login() -> typing.Union[str, quart.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
extra_versions["Quart"] = quart.__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__
extra_versions["flask-wtf"] = flask_wtf.__version__
try:
extra_versions["Prosody"] = await client.get_server_version()
except quart.exceptions.Unauthorized:
except werkzeug.exceptions.Unauthorized:
extra_versions["Prosody"] = "unknown"
return await render_template(
"about.html",
version=version,
extra_versions=extra_versions,
core_versions=core_versions,
)

View File

@@ -19,7 +19,9 @@ from quart import (
current_app, _app_ctx_stack, session as http_session, abort, redirect,
url_for,
)
import quart.exceptions
import quart
import werkzeug.exceptions
from . import xmpputil
from .xmpputil import split_jid
@@ -386,16 +388,16 @@ class ProsodyClient:
) -> typing.Callable[
[typing.Callable[..., typing.Awaitable[T]]],
typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]]:
def decorator(
f: typing.Callable[..., typing.Awaitable[T]],
) -> typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]:
@functools.wraps(f)
async def wrapped(
*args: typing.Any,
**kwargs: typing.Any,
) -> typing.Union[T, quart.Response]:
) -> typing.Union[T, quart.Response, werkzeug.Response]:
if not self.has_session or not (await self.test_session()):
redirect_to_value = redirect_to
if redirect_to_value is not False:
@@ -415,17 +417,17 @@ class ProsodyClient:
) -> typing.Callable[
[typing.Callable[..., typing.Awaitable[T]]],
typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]]:
def decorator(
f: typing.Callable[..., typing.Awaitable[T]],
) -> typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]:
@functools.wraps(f)
@self.require_session(redirect_to=redirect_to)
async def wrapped(
*args: typing.Any,
**kwargs: typing.Any,
) -> typing.Union[T, quart.Response]:
) -> typing.Union[T, quart.Response, werkzeug.Response]:
if not self.is_admin_session:
raise abort(403, "This is not for you.")
@@ -492,7 +494,7 @@ class ProsodyClient:
session=session,
)
avatar_hash = avatar_info["sha1"]
except quart.exceptions.HTTPException:
except werkzeug.exceptions.HTTPException:
avatar_hash = None
return {
@@ -644,7 +646,7 @@ class ProsodyClient:
new_access_model,
)
))
except quart.exceptions.NotFound:
except werkzeug.exceptions.NotFound:
if ignore_not_found:
return
raise
@@ -774,7 +776,7 @@ class ProsodyClient:
session: aiohttp.ClientSession,
) -> str:
access_models = filter(
lambda x: not isinstance(x, quart.exceptions.NotFound),
lambda x: not isinstance(x, werkzeug.exceptions.NotFound),
await asyncio.gather(
self.get_avatar_access_model(session=session),
self.get_nickname_access_model(session=session),

View File

@@ -17,9 +17,12 @@
<h3>{% trans %}Trademarks{% endtrans %}</h3>
<p>{% 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 <a href="{{ trademarks_url }}">Snikket Trademarks information page</a>.{% endtrans %}
<h3>{% trans %}Software Versions{% endtrans %}</h3>
<pre>Snikket Server
Domain: {{ config["SNIKKET_DOMAIN"] }}
Snikket Web Portal{% if version %} ({{ version }}){% endif %}
<pre>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 %}

View File

@@ -11,6 +11,8 @@
{% call render_errors(form) %}{% endcall %}
<div class="f-bbox">
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
<form method="POST">
{{ form.csrf_token }}
{%- call form_button("download", form.action_export, class="primary") %}{% endcall -%}

View File

@@ -6,18 +6,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: translations@snikket.org\n"
"POT-Creation-Date: 2022-01-17 17:27+0100\n"
"PO-Revision-Date: 2021-04-02 19:01+0000\n"
"Last-Translator: Daniel Holmgaard <annoncer@protonmail.com>\n"
"Language-Team: Danish <https://i18n.sotecware.net/projects/snikket/web-"
"portal/da/>\n"
"PO-Revision-Date: 2022-05-30 14:01+0000\n"
"Last-Translator: Daniel Holmgaard <fovatis@tutanota.com>\n"
"Language-Team: Danish <http://i18n.sotecware.net/projects/snikket/web-portal/"
"da/>\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.1\n"
"X-Generator: Weblate 4.8.1\n"
"Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:10
@@ -194,23 +194,23 @@ msgstr "Bruger fjernet fra cirkel"
#: snikket_web/admin.py:609
msgid "Message contents"
msgstr ""
msgstr "Meddelelsens indhold"
#: snikket_web/admin.py:615
msgid "Only send to online users"
msgstr ""
msgstr "Send kun til online brugere"
#: snikket_web/admin.py:619
msgid "Post to all users"
msgstr ""
msgstr "Send til alle brugere"
#: snikket_web/admin.py:623
msgid "Send preview to yourself"
msgstr ""
msgstr "Send forhåndsvisning til dig selv"
#: snikket_web/admin.py:645
msgid "Announcement sent!"
msgstr ""
msgstr "Bekendgørelse sendt!"
#: snikket_web/infra.py:51
msgid "Main"
@@ -221,6 +221,8 @@ msgid ""
"The account data you tried to import is too large to upload. Please contact "
"your Snikket operator."
msgstr ""
"De kontodata, du forsøgte at importere, er for store til at uploade. Kontakt "
"din Snikket-operatør."
#: snikket_web/invite.py:112
msgid "Username"
@@ -261,11 +263,11 @@ msgstr "Ændr adgangskode"
#: snikket_web/invite.py:244
msgid "Account data file"
msgstr ""
msgstr "Kontodatafil"
#: snikket_web/invite.py:248
msgid "Import data"
msgstr ""
msgstr "Importer data"
#: snikket_web/invite.py:269
#, python-format
@@ -273,6 +275,8 @@ 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 ""
"De kontodata, du forsøgte at importere, er i et ukendt format. Upload en XML-"
"fil i XEP-0227-format (forudsat format: %(mimetype)s)."
#: snikket_web/invite.py:289 snikket_web/templates/unauth.html:18
#: snikket_web/user.py:178
@@ -341,11 +345,11 @@ msgstr "Opdater profil"
#: snikket_web/user.py:82
msgid "Account data"
msgstr ""
msgstr "Kontodata"
#: snikket_web/user.py:86
msgid "Upload"
msgstr ""
msgstr "Upload"
#: snikket_web/user.py:111
msgid "Incorrect password."
@@ -369,11 +373,11 @@ msgstr "Profil opdateret"
#: snikket_web/user.py:184
msgid "Export"
msgstr ""
msgstr "Exporter"
#: snikket_web/user.py:202
msgid "You currently have no account data to export."
msgstr ""
msgstr "Du har i øjeblikket ingen kontodata at eksportere."
#: snikket_web/templates/_footer.html:4
#, python-format
@@ -662,7 +666,7 @@ msgstr "Cirkel medlemmer"
#: snikket_web/templates/admin_edit_circle.html:71
msgid "The user has been deleted from the server."
msgstr ""
msgstr "Brugeren er blevet slettet fra serveren."
#: snikket_web/templates/admin_edit_circle.html:71
#: snikket_web/templates/library.j2:108
@@ -876,22 +880,20 @@ msgstr "Håndter invitationer"
#: snikket_web/templates/admin_home.html:35
msgid "System health"
msgstr ""
msgstr "Systemets sundhed"
#: snikket_web/templates/admin_home.html:38
msgid "View the server status or send a broadcast message to all users."
msgstr ""
msgstr "Vis serverstatus, eller send en udsendelsesmeddelelse til alle brugere."
#: snikket_web/templates/admin_home.html:40
msgid "Send a broadcast message to all users."
msgstr ""
msgstr "Send en udsendelsesmeddelelse til alle brugere."
#: snikket_web/templates/admin_home.html:43
#: snikket_web/templates/admin_system.html:4
#, fuzzy
#| msgid "Manage users"
msgid "Manage system"
msgstr "Håndter brugere"
msgstr "Håndter system"
#: snikket_web/templates/admin_home.html:48
msgid "Go back to your user's web portal page."
@@ -952,11 +954,11 @@ msgstr "Ødelæg link"
#: snikket_web/templates/admin_system.html:6
msgid "Overall system status"
msgstr ""
msgstr "Samlet systemstatus"
#: snikket_web/templates/admin_system.html:9
msgid "System load (5 minute average)"
msgstr ""
msgstr "Systembelastning (5 minutters gennemsnit)"
#: snikket_web/templates/admin_system.html:14
#: snikket_web/templates/admin_system.html:22
@@ -967,11 +969,11 @@ msgstr ""
#: snikket_web/templates/admin_system.html:76
#: snikket_web/templates/admin_system.html:84
msgid "unknown"
msgstr ""
msgstr "ukendt"
#: snikket_web/templates/admin_system.html:17
msgid "Memory use"
msgstr ""
msgstr "Forbrug af hukommelse"
#: snikket_web/templates/admin_system.html:20
#, python-format
@@ -979,54 +981,56 @@ msgid ""
"%(percentage_global)s of %(mem_available)s. Of that, Snikket uses "
"%(percentage_snikket)s."
msgstr ""
"%(percentage_global)s af %(mem_available)s. Der af bruger Snikket "
"%(percentage_snikket)s."
#: snikket_web/templates/admin_system.html:27
msgid "Web portal status"
msgstr ""
msgstr "Webportalens status"
#: snikket_web/templates/admin_system.html:30
#: snikket_web/templates/admin_system.html:53
msgid "Version"
msgstr ""
msgstr "Version"
#: snikket_web/templates/admin_system.html:31
#: snikket_web/templates/admin_system.html:54
msgid "View all versions"
msgstr ""
msgstr "Vis alle versioner"
#: snikket_web/templates/admin_system.html:32
#: snikket_web/templates/admin_system.html:55
msgid "Average CPU use"
msgstr ""
msgstr "Gennemsnitlig CPU-forbrug"
#: snikket_web/templates/admin_system.html:40
#: snikket_web/templates/admin_system.html:63
msgid "Current memory use"
msgstr ""
msgstr "Nuværende hukommelsesbrug"
#: snikket_web/templates/admin_system.html:50
#, fuzzy
#| msgid "Snikket Web Portal"
msgid "Snikket server status"
msgstr "Snikket Webportal"
msgstr "Snikket server status"
#: snikket_web/templates/admin_system.html:71
msgid "Storage used by shared files"
msgstr ""
msgstr "Lagerplads, der bruges af delte filer"
#: snikket_web/templates/admin_system.html:79
msgid "Connected devices"
msgstr ""
msgstr "Forbundet enheder"
#: snikket_web/templates/admin_system.html:90
msgid "Broadcast message"
msgstr ""
msgstr "Send besked"
#: snikket_web/templates/admin_system.html:92
msgid ""
"This form allows you to send a message to all users currently online on your "
"Snikket server. Use it wisely."
msgstr ""
"Denne formular giver dig mulighed for at sende en besked til alle brugere, "
"der i øjeblikket er online på din Snikket-server. Brug den med omtanke."
#: snikket_web/templates/admin_users.html:19
msgid "The user is an administrator."
@@ -1300,22 +1304,20 @@ msgid ""
"You can now safely close this page, or log in to the web portal to <a href="
"\"%(login_url)s\">manage your account</a>."
msgstr ""
"Du kan nu trygt lukke denne side eller logge ind på webportalen for at <a "
"href=\"%(login_url)s\">administrere din konto</a>."
#: snikket_web/templates/invite_success.html:21
#, fuzzy
#| msgid "Operation successful"
msgid "Import successful"
msgstr "Operation lykkes"
msgstr "Importering lykkes"
#: snikket_web/templates/invite_success.html:22
msgid "Congratulations! Your account data has been successfully imported."
msgstr ""
msgstr "Tillykke! Dine kontodata er blevet importeret."
#: snikket_web/templates/invite_success.html:26
#, fuzzy
#| msgid "Using the Snikket app"
msgid "Moving to Snikket?"
msgstr "Bruger Snikket appen"
msgstr "Flytte til Snikket?"
#: snikket_web/templates/invite_success.html:27
msgid ""
@@ -1324,10 +1326,14 @@ msgid ""
"information, etc.) from your previous account. When you have exported the "
"data from your previous account, upload it using the form below."
msgstr ""
"Hvis du flytter fra en anden Snikket-platform eller en anden XMPP-kompatibel "
"tjeneste, kan du eventuelt importere dataene (kontakter, profiloplysninger "
"osv.) fra din tidligere konto. Når du har eksporteret dataene fra din "
"tidligere konto, skal du uploade dem ved hjælp af nedenstående formular."
#: snikket_web/templates/invite_success.html:30
msgid "Upload account data"
msgstr ""
msgstr "Upload kontodata"
#: snikket_web/templates/invite_view.html:6
#, python-format
@@ -1558,10 +1564,8 @@ msgstr "Rediger profil"
#: snikket_web/templates/user_home.html:33
#: snikket_web/templates/user_manage_data.html:4
#, fuzzy
#| msgid "Manage users"
msgid "Manage your data"
msgstr "Håndter brugere"
msgstr "Håndter dine data"
#: snikket_web/templates/user_home.html:39
msgid "Your Snikket"
@@ -1588,16 +1592,16 @@ msgstr ""
"af de forbundne enheder."
#: snikket_web/templates/user_manage_data.html:8
#, fuzzy
#| msgid "Your account"
msgid "Export account"
msgstr "Din konto"
msgstr "Eksporter konto"
#: snikket_web/templates/user_manage_data.html:9
msgid ""
"Download your account data as a file for backup purposes or to move your "
"account to another service."
msgstr ""
"Download dine kontodata som en fil til sikkerhedskopieringsformål eller for "
"at flytte din konto til en anden tjeneste."
#: snikket_web/templates/user_passwd.html:5
msgid "Change your password"

View File

@@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View File

@@ -6,18 +6,19 @@
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: translations@snikket.org\n"
"POT-Creation-Date: 2022-01-17 17:27+0100\n"
"PO-Revision-Date: 2022-05-14 14:42+0800\n"
"Last-Translator: Raka <me@bihu.im>\n"
"Language-Team: zh_Hans_CN <LL@li.org>\n"
"Language: zh_CN\n"
"PO-Revision-Date: 2022-05-20 00:01+0000\n"
"Last-Translator: Zack Zhou <lnx@outlook.com>\n"
"Language-Team: Chinese (Simplified) <http://i18n.sotecware.net/projects/"
"snikket/web-portal/zh_Hans/>\n"
"Language: zh_Hans_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.8.1\n"
"Generated-By: Babel 2.10.1\n"
"X-Generator: Poedit 3.0.1\n"
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_edit_circle.html:59
@@ -371,7 +372,7 @@ msgstr "导出"
#: snikket_web/user.py:202
msgid "You currently have no account data to export."
msgstr "目前没有需要导出的数据"
msgstr "目前没有需要导出的数据"
#: snikket_web/templates/_footer.html:4
#, python-format
@@ -415,13 +416,15 @@ msgid ""
"\"%(agpl_url)s\">Affero GNU General Public License, version 3.0 or later</a>. "
"The full terms of the license can be reviewed using the aforementioned link."
msgstr ""
"此Web门户软件许可遵循 <a href=\"%(agpl_url)s\">GNU Affero通用公共许可证版本3."
"0及之后版本</a>中的条款。此许可证的完整条款请使用前文中的链接查看。"
#: snikket_web/templates/about.html:15
#, python-format
msgid ""
"The source code of the web portal can be downloaded and viewed in <a href="
"\"%(source_url)s\">its GitHub repository</a>."
msgstr ""
msgstr "此Web门户的源代码可在<a href=\"%(source_url)s\">其GitHub repository</a>查看并下载。"
#: snikket_web/templates/about.html:16
#, python-format
@@ -430,6 +433,8 @@ msgid ""
"Material Icons</a>, made available by Google under the terms of the <a href="
"\"%(apache20_url)s\">Apache 2.0 License</a>."
msgstr ""
"Web门户中使用的图标为<a href=\"%(source_url)s\">Googles Material Icons</a>"
"由Google制作分发并遵循<a href=\"%(apache20_url)s\">Apache 2.0 许可证</a>中的条款。"
#: snikket_web/templates/about.html:17
msgid "Trademarks"
@@ -442,6 +447,8 @@ msgid ""
"Company. For more information about the trademarks, visit the <a href="
"\"%(trademarks_url)s\">Snikket Trademarks information page</a>."
msgstr ""
"“Snikket”和鹦鹉logo是Snikket Community Interest Company的商标。关于商标的更多信息请访问<a href="
"\"%(trademarks_url)s\">Snikket商标信息页面</a>。"
#: snikket_web/templates/about.html:19
msgid "Software Versions"
@@ -544,12 +551,11 @@ msgstr "警告"
#: snikket_web/templates/admin_debug_user.html:12
msgid "The below dump may contain sensitive information."
msgstr "Dump可能包括敏感信息"
msgstr "调试信息中可能包括敏感信息"
#: snikket_web/templates/admin_debug_user.html:15
#, fuzzy
msgid "Raw debug dump"
msgstr "Debug信息"
msgstr "原始调试信息"
#: snikket_web/templates/admin_debug_user.html:17
msgid "Copy complete output"
@@ -596,7 +602,7 @@ msgstr "这是你的主圈子"
#: snikket_web/templates/admin_edit_circle.html:15
msgid "This circle is managed automatically and cannot be removed or renamed."
msgstr "此圈子由系统自动生成,无法被修改或删除"
msgstr "此圈子由系统自动生成,无法被修改或删除"
#: snikket_web/templates/admin_edit_circle.html:17
#: snikket_web/templates/admin_edit_circle.html:33
@@ -636,7 +642,7 @@ msgstr "圈子成员"
#: snikket_web/templates/admin_edit_circle.html:71
msgid "The user has been deleted from the server."
msgstr "用户已从实例删除"
msgstr "用户已从实例删除"
#: snikket_web/templates/admin_edit_circle.html:71
#: snikket_web/templates/library.j2:108
@@ -698,7 +704,7 @@ msgstr "圈子"
#: snikket_web/templates/admin_edit_invite.html:35
msgid "The user will not be added to any circle and will have no contacts."
msgstr "此用户将不会有圈子和联系人"
msgstr "此用户将不会有任何圈子和联系人"
#: snikket_web/templates/admin_edit_invite.html:40
msgid "Contact"
@@ -744,7 +750,7 @@ msgstr "编辑用户"
#: snikket_web/templates/admin_edit_user.html:26
msgid "The login name cannot be changed."
msgstr "账号无法修改"
msgstr "登录账号无法修改"
#: snikket_web/templates/admin_edit_user.html:33
msgid ""
@@ -755,7 +761,7 @@ msgstr "权限级别控制此Sknikket实例的用户能发起交流的范围。"
#: snikket_web/templates/admin_edit_user.html:40
#, python-format
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
msgstr ""
msgstr "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
#: snikket_web/templates/admin_edit_user.html:50
msgid "Return to user list"
@@ -807,7 +813,7 @@ msgstr "用户"
#: snikket_web/templates/admin_home.html:11
msgid "Create password reset links or delete users."
msgstr "创建密码重置链接或删除用户"
msgstr "创建密码重置链接或删除用户"
#: snikket_web/templates/admin_home.html:15
#: snikket_web/templates/admin_users.html:4
@@ -837,7 +843,7 @@ msgstr "系统健康状况"
#: snikket_web/templates/admin_home.html:38
msgid "View the server status or send a broadcast message to all users."
msgstr "查看服务器状态或向所有用户发布广播消息"
msgstr "查看服务器状态或向所有用户发布广播消息"
#: snikket_web/templates/admin_home.html:40
msgid "Send a broadcast message to all users."
@@ -850,7 +856,7 @@ msgstr "系统管理"
#: snikket_web/templates/admin_home.html:48
msgid "Go back to your user's web portal page."
msgstr "返回用户门户页面"
msgstr "返回用户门户页面"
#: snikket_web/templates/admin_home.html:50
msgid "Exit admin panel"
@@ -882,7 +888,7 @@ msgstr "删除邀请"
#: snikket_web/templates/admin_invites.html:57
msgid "Currently, there are no pending invitations."
msgstr "当前没有待定邀请"
msgstr "当前没有待定邀请"
#: snikket_web/templates/admin_reset_user_password.html:8
msgid "Password reset"
@@ -987,7 +993,7 @@ msgstr "此用户是管理员。"
#: snikket_web/templates/admin_users.html:19
msgid " (Administrator)"
msgstr "(管理员)"
msgstr " (管理员)"
#: snikket_web/templates/admin_users.html:22
msgid "The user is restricted."
@@ -995,7 +1001,7 @@ msgstr "此用户受到限制。"
#: snikket_web/templates/admin_users.html:22
msgid " (Restricted)"
msgstr "(受限)"
msgstr " (受限)"
#: snikket_web/templates/app.html:4
msgid "Snikket Web Portal"
@@ -1040,7 +1046,7 @@ msgstr "邀请到 %(site_name)s"
#: snikket_web/templates/invite_view.html:14
#, python-format
msgid "Powered by <img src=\"%(logo_url)s\" alt=\"Snikket\">"
msgstr ""
msgstr "由<img alt=\"Snikket\" src=\"%(logo_url)s\">强力驱动"
#: snikket_web/templates/invite_invalid.html:8
msgid "Invite expired"
@@ -1131,7 +1137,7 @@ msgstr "在线重置密码"
msgid ""
"To reset your password online, fill out the fields below and confirm using the "
"button."
msgstr "填写信息并提交来重置密码"
msgstr "填写信息并使用下面的按钮提交来在线重置密码"
#: snikket_web/templates/invite_reset_success.html:5
msgid "Password reset successful | Snikket"
@@ -1300,6 +1306,8 @@ msgid ""
"Install the Snikket App on your Android device (<a href=\"%(ios_info_url)s\" "
"rel=\"noopener noreferrer\" target=\"_blank\">iOS coming soon!</a>)."
msgstr ""
"在你的Android设备上安装Snikket App<a href=\"%(ios_info_url)s\" rel=\"noopener "
"noreferrer\" target=\"_blank\">iOS 版本即将到来!</a>)。"
#: snikket_web/templates/invite_view.html:28
msgid "Get it on Google Play"

View File

@@ -13,7 +13,7 @@ from quart import (
flash,
current_app,
)
import quart.exceptions
import werkzeug.exceptions
import wtforms
@@ -59,7 +59,7 @@ _ACCESS_MODEL_CHOICES = [
class ProfileForm(BaseForm):
nickname = wtforms.TextField(
nickname = wtforms.StringField(
_l("Display name"),
)
@@ -96,7 +96,7 @@ async def index() -> str:
@bp.route('/passwd', methods=["GET", "POST"])
@client.require_session()
async def change_pw() -> typing.Union[str, quart.Response]:
async def change_pw() -> typing.Union[str, werkzeug.Response]:
form = ChangePasswordForm()
if form.validate_on_submit():
try:
@@ -104,8 +104,8 @@ async def change_pw() -> typing.Union[str, quart.Response]:
form.current_password.data,
form.new_password.data,
)
except (quart.exceptions.Unauthorized,
quart.exceptions.Forbidden):
except (werkzeug.exceptions.Unauthorized,
werkzeug.exceptions.Forbidden):
# server refused current password, set an appropriate error
form.current_password.errors.append(
_("Incorrect password."),
@@ -128,7 +128,7 @@ EAVATARTOOBIG = _l(
@bp.route("/profile", methods=["GET", "POST"])
@client.require_session()
async def profile() -> typing.Union[str, quart.Response]:
async def profile() -> typing.Union[str, werkzeug.Response]:
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
form = ProfileForm()
@@ -221,7 +221,7 @@ async def manage_data() -> typing.Union[str, quart.Response]:
@bp.route("/logout", methods=["GET", "POST"])
@client.require_session()
async def logout() -> typing.Union[quart.Response, str]:
async def logout() -> typing.Union[werkzeug.Response, str]:
form = LogoutForm()
if form.validate_on_submit():
await client.logout()

View File

@@ -4,7 +4,7 @@ import typing
import xml.etree.ElementTree as ET
from quart import abort
import quart.exceptions
import werkzeug.exceptions
TAG_XMPP_ERROR = "error"
@@ -239,7 +239,7 @@ def extract_pubsub_item_get_reply(
) -> typing.Optional[ET.Element]:
try:
pubsub = extract_iq_reply(iq_tree, TAG_PUBSUB)
except quart.exceptions.NotFound:
except werkzeug.exceptions.NotFound:
return None
if pubsub is None: