Add htmx version
This commit is contained in:
parent
343a8d3766
commit
1440316523
|
@ -0,0 +1,26 @@
|
||||||
|
This is a demo project for htmx, flask and babel, so this readme contains
|
||||||
|
mostly random notes about its construction and maintenance.
|
||||||
|
|
||||||
|
Localization
|
||||||
|
============
|
||||||
|
|
||||||
|
I followed the tutorial at https://phrase.com/blog/posts/python-localization-flask-applications/
|
||||||
|
|
||||||
|
To extract messages, use
|
||||||
|
|
||||||
|
```
|
||||||
|
pybabel extract -F babel.cfg -o messages.pot .
|
||||||
|
```
|
||||||
|
|
||||||
|
The following creates a new translation file and may be a bad idea if you
|
||||||
|
already have one:
|
||||||
|
|
||||||
|
```
|
||||||
|
pybabel init -i messages.pot -d translations -l de
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile the translation file(s):
|
||||||
|
|
||||||
|
```
|
||||||
|
pybabel compile -d translations
|
||||||
|
```
|
|
@ -0,0 +1,197 @@
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
from flask import Flask, request, render_template, render_template_string, session, g
|
||||||
|
from flask_babel import Babel, _
|
||||||
|
|
||||||
|
logging.basicConfig(format="%(asctime)s %(levelname)s %(name)s %(lineno)d | %(message)s", level=logging.DEBUG)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
babel = Babel(app)
|
||||||
|
app.secret_key = b"1234" # XXX - Should be in config (not in source, but stable across restarts)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
@app.route("/")
|
||||||
|
def home():
|
||||||
|
log.info("in home")
|
||||||
|
app.logger.info("in home (via app.logger)")
|
||||||
|
log.info("greeting = %s", _("hello"))
|
||||||
|
user_public_id = None
|
||||||
|
if "uuid" not in session:
|
||||||
|
session["uuid"], user_public_id = new_user()
|
||||||
|
if not user_public_id:
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("select * from users where uuid = %s", (session["uuid"],))
|
||||||
|
user_public_id = csr.fetchone().public_id # Can this fail?
|
||||||
|
|
||||||
|
log.info("session = %s", session)
|
||||||
|
return render_template("home.html", public_id=user_public_id)
|
||||||
|
|
||||||
|
@app.route("/entry_form", methods=["POST"])
|
||||||
|
def entry_form():
|
||||||
|
log.info("session = %s", session)
|
||||||
|
user_uuid = session["uuid"]
|
||||||
|
log.info("user_uuid = %s", user_uuid)
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute(
|
||||||
|
"select * from country_names natural join countries where lang=%s order by population desc",
|
||||||
|
(get_locale(),))
|
||||||
|
f = ""
|
||||||
|
for r in csr.fetchall():
|
||||||
|
csr.execute(
|
||||||
|
"select * from entries where uuid = %s and country = %s",
|
||||||
|
(user_uuid, r.code,))
|
||||||
|
f += render_template("entry_row.html", code=r.code, name=r.name, entries=csr.fetchall())
|
||||||
|
return f
|
||||||
|
|
||||||
|
@app.route("/update_entries", methods=["POST"])
|
||||||
|
def update_entries():
|
||||||
|
csr = get_cursor()
|
||||||
|
log.debug(request.form)
|
||||||
|
csr = get_cursor()
|
||||||
|
r = ""
|
||||||
|
for k, v in request.form.items():
|
||||||
|
(band, code, id) = k.split("_")
|
||||||
|
if id == "new":
|
||||||
|
csr.execute(
|
||||||
|
"insert into entries(country, uuid, band) values(%s, %s, %s) returning id",
|
||||||
|
(code, session["uuid"], v,))
|
||||||
|
id = csr.fetchone().id
|
||||||
|
extra = render_template_string(
|
||||||
|
"""<input name="band_{{code}}_new" hx-trigger="change" hx-post="/update_entries" hx-swap="outerHTML" autofocus>""",
|
||||||
|
code=code)
|
||||||
|
else:
|
||||||
|
csr.execute(
|
||||||
|
"update entries set band = %s where id = %s and uuid = %s",
|
||||||
|
(v, id, session["uuid"],))
|
||||||
|
extra = ""
|
||||||
|
r += render_template_string(
|
||||||
|
"""<input name="band_{{code}}_{{id}}" value="{{band}}" hx-trigger="change" hx-post="/update_entries" hx-swap="outerHTML">""",
|
||||||
|
code=code, id=id, band=v)
|
||||||
|
r += extra
|
||||||
|
return r
|
||||||
|
|
||||||
|
@app.route("/result/<public_id>")
|
||||||
|
def result(public_id):
|
||||||
|
|
||||||
|
lang = get_locale()
|
||||||
|
with get_dict_cursor() as csr:
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
select country, name,
|
||||||
|
band,
|
||||||
|
count(*) as total,
|
||||||
|
count(*) filter(where public_id = %s) as by_user
|
||||||
|
from entries e
|
||||||
|
join countries c on c.code = e.country
|
||||||
|
join country_names cn on e.country = cn.code and cn.lang = %s
|
||||||
|
join users u using (uuid)
|
||||||
|
group by country, name, population, band
|
||||||
|
order by population desc, name, band
|
||||||
|
""",
|
||||||
|
(public_id, lang,))
|
||||||
|
country_stats = []
|
||||||
|
for r in csr:
|
||||||
|
if not country_stats or country_stats[-1]["code"] != r["country"]:
|
||||||
|
country_stats.append({"code": r["country"],
|
||||||
|
"name": r["name"],
|
||||||
|
"bands": []})
|
||||||
|
country_stats[-1]["bands"].append({"name": r["band"],
|
||||||
|
"total_count": r["total"],
|
||||||
|
"by_user": bool(r["by_user"])})
|
||||||
|
|
||||||
|
stats = { "countries": country_stats }
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
with a as (
|
||||||
|
select public_id, count(distinct(country)) as c
|
||||||
|
from entries join users using (uuid)
|
||||||
|
group by 1)
|
||||||
|
select public_id, c, rank() over (order by c desc) from a;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for r in csr:
|
||||||
|
if r["public_id"] == public_id:
|
||||||
|
stats["country_rank"] = r["rank"]
|
||||||
|
stats["country_count"] = r["c"]
|
||||||
|
stats["user_count"] = csr.rowcount
|
||||||
|
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
with a as (
|
||||||
|
select public_id, count(*) as c
|
||||||
|
from entries join users using (uuid)
|
||||||
|
group by 1)
|
||||||
|
select public_id, c, rank() over (order by c desc) from a;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for r in csr:
|
||||||
|
if r["public_id"] == public_id:
|
||||||
|
# XXX - clobbering previous results?
|
||||||
|
stats["band_rank"] = r["rank"]
|
||||||
|
stats["band_count"] = r["c"]
|
||||||
|
|
||||||
|
ask_mail = session["uuid"].endswith(public_id)
|
||||||
|
log.debug("uuid = %s, public_id=%s, ask_mail=%s", session["uuid"], public_id, ask_mail)
|
||||||
|
return render_template("result.html", public_id=public_id, stats=stats, ask_mail=ask_mail)
|
||||||
|
|
||||||
|
@app.route("/add-email", methods=["POST"])
|
||||||
|
def add_email():
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("update users set email=%s where uuid = %s", (request.form["email"], session["uuid"],))
|
||||||
|
if csr.rowcount:
|
||||||
|
return render_template_string(_("Saved. Expect mail at {{email}}"),
|
||||||
|
email=request.form["email"])
|
||||||
|
else:
|
||||||
|
return _("<span>You don't exist. Go away!</span>")
|
||||||
|
|
||||||
|
# Middleware
|
||||||
|
|
||||||
|
@babel.localeselector
|
||||||
|
def get_locale():
|
||||||
|
lang = request.accept_languages.best_match(["de", "en"])
|
||||||
|
log.info("get_locale: best language is %s", lang)
|
||||||
|
return lang
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
def get_dict_cursor():
|
||||||
|
db = get_db()
|
||||||
|
csr = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
return csr
|
||||||
|
|
||||||
|
def get_cursor():
|
||||||
|
db = get_db()
|
||||||
|
csr = db.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
|
||||||
|
return csr
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if "db" not in g:
|
||||||
|
g.db = psycopg2.connect("dbname=internationale")
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def teardown_db(exception):
|
||||||
|
log.info("in teardown_db: exception = %s", exception)
|
||||||
|
db = g.pop('db', None)
|
||||||
|
if db is not None:
|
||||||
|
if not exception:
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
db.rollback()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def new_user():
|
||||||
|
user_uuid = str(uuid.uuid4())
|
||||||
|
user_public_id = user_uuid[-12:]
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("insert into users(uuid, public_id) values(%s, %s)", (user_uuid, user_public_id,))
|
||||||
|
return user_uuid, user_public_id
|
||||||
|
|
||||||
|
# vim: tw=99
|
|
@ -0,0 +1,3 @@
|
||||||
|
[python: **.py]
|
||||||
|
[jinja2: **/templates/**.html]
|
||||||
|
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Translations template for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-03-20 23:24+0100\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"
|
||||||
|
|
||||||
|
#: app.py:22
|
||||||
|
msgid "hello"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app.py:147
|
||||||
|
msgid "Saved. Expect mail at {{email}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app.py:150
|
||||||
|
msgid "<span>You don't exist. Go away!</span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/home.html:6 templates/result.html:6
|
||||||
|
msgid "The Musical Internatiionale"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/home.html:20
|
||||||
|
msgid ""
|
||||||
|
"All the countries in the world — which bands or solo musicians from these"
|
||||||
|
" countries do you know?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/home.html:32
|
||||||
|
msgid "Show"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/result.html:15
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"You have entered %(band_count)s bands or artists from %(country_count)s "
|
||||||
|
"countries."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/result.html:18
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This puts you at rank %(band_rank)s and %(country_rank)s of "
|
||||||
|
"%(user_count)s, respectively."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/result.html:37
|
||||||
|
msgid ""
|
||||||
|
"You can leave your email address here if you want to be informed about "
|
||||||
|
"future developments"
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Flask
|
||||||
|
Flask-Babel
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 362 KiB |
|
@ -0,0 +1,205 @@
|
||||||
|
cite {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
cite::before {
|
||||||
|
content: "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#the-quote {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#country-list {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 2;
|
||||||
|
justify-self: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#permalink {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
#the-quote {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#country-list {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#permalink {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary {
|
||||||
|
grid-column: 1 /3;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
margin: 3em;
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#country-list input {
|
||||||
|
margin: 0.2em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
box-shadow: 0 0 0.2em 0.2em hsl(240 100% 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
src: url(MontserratAlternates-Light.ttf);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
src: url(MontserratAlternates-Regular.ttf);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
src: url(MontserratAlternates-Medium.ttf);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
src: url(MontserratAlternates-Bold.ttf);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
src: url(MontserratAlternates-LightItalic.ttf);
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
src: url(MontserratAlternates-MediumItalic.ttf);
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
font-weight: 400;
|
||||||
|
font-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url("guitar.png");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
text-shadow: 0 0 1.0em #FFFFFF,
|
||||||
|
0 0 0.2em #FFFFFF;
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band {
|
||||||
|
font-family: MontserratAlternates;
|
||||||
|
font-weight: 300;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.band.by_user {
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 1em 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#permalink a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #444;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px dotted black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary {
|
||||||
|
margin-left: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonrow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
border: solid 1px #888;
|
||||||
|
background-color: #CCD;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
box-shadow: 0 0 0.3em 0.2em hsla(0, 0%, 70%, 0.5);
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus, .btn:hover {
|
||||||
|
background-color: #DDF;
|
||||||
|
border: solid 1px #88F;
|
||||||
|
box-shadow: 0 0 0.3em 0.4em hsla(240, 30%, 70%, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
background-color: #FFFFFF80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
vim: sw=4
|
||||||
|
*/
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{{name}}
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{% for e in entries %}
|
||||||
|
<input name="band_{{code}}_{{e.id}}" value="{{e.band}}" hx-trigger="change" hx-post="/update_entries" hx-swap="outerHTML">
|
||||||
|
{% endfor %}
|
||||||
|
<input name="band_{{code}}_new" hx-trigger="change" hx-post="/update_entries" hx-swap="outerHTML">
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>
|
||||||
|
{{ _("The Musical Internatiionale") }}
|
||||||
|
</title>
|
||||||
|
<script src='https://unpkg.com/htmx.org@1.7.0'></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<blockquote id="the-quote">
|
||||||
|
I don't need a rationale<br/>
|
||||||
|
to sing the Internationale
|
||||||
|
<cite>
|
||||||
|
They might be giants
|
||||||
|
</cite>
|
||||||
|
</blockquote>
|
||||||
|
<div class="intro">{{ _("All the countries in the world — which bands or solo musicians from these countries do you know?") }}</div>
|
||||||
|
<table id="country-list">
|
||||||
|
<tbody hx-post="/entry_form" hx-trigger="load">
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/result/{{ public_id }}" class="btn">
|
||||||
|
{{ _("Show") }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>
|
||||||
|
{{ _("The Musical Internatiionale") }}
|
||||||
|
</title>
|
||||||
|
<script src='https://unpkg.com/htmx.org@1.7.0'></script>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
{{_("You have entered %(band_count)s bands or artists from %(country_count)s countries.", band_count=stats.band_count, country_count=stats.country_count)}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{_("This puts you at rank %(band_rank)s and %(country_rank)s of %(user_count)s, respectively.", band_rank=stats.band_rank, country_rank=stats.country_rank, user_count=stats.user_count)}}
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
{% for c in stats.countries %}
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{{ c.name }}
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{% for b in c.bands %}
|
||||||
|
<span class="band {%if b.by_user %}by_user{%endif%}">{{b.name}}</span>{%if not loop.last%},{%endif%}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% if ask_mail %}
|
||||||
|
<p>
|
||||||
|
{{_("You can leave your email address here if you want to be informed about future developments")}}
|
||||||
|
<br>
|
||||||
|
<input hx-post="/add-email" name="email" hx-swap="outerHTML">
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
|
@ -0,0 +1,68 @@
|
||||||
|
# German translations for PROJECT.
|
||||||
|
# Copyright (C) 2022 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2022-02-27 01:46+0100\n"
|
||||||
|
"PO-Revision-Date: 2022-02-27 01:47+0100\n"
|
||||||
|
"Last-Translator: Peter J. Holzer <hjp@hjp.at>\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"
|
||||||
|
|
||||||
|
#: app.py:23
|
||||||
|
msgid "hello"
|
||||||
|
msgstr "Hallo"
|
||||||
|
|
||||||
|
#: app.py:147
|
||||||
|
msgid "Saved. Expect mail at {{email}}"
|
||||||
|
msgstr "Gespeichert. Du wirst Mail an {{email}} bekommen."
|
||||||
|
|
||||||
|
#: app.py:150
|
||||||
|
msgid "<span>You don't exist. Go away!</span>"
|
||||||
|
msgstr "Du existierst nicht. Verschwinde!"
|
||||||
|
|
||||||
|
#: templates/home.html:6
|
||||||
|
msgid "The Musical Internatiionale"
|
||||||
|
msgstr "Die Musik-Internationale"
|
||||||
|
|
||||||
|
#: templates/home.html:21
|
||||||
|
msgid ""
|
||||||
|
"All the countries in the world — which bands or solo musicians from these"
|
||||||
|
" countries do you know?"
|
||||||
|
msgstr "Alle Länder dieser Erde — welche Bands oder Solo-Musiker aus diesen Ländern kennst Du?"
|
||||||
|
|
||||||
|
#: templates/home.html:24
|
||||||
|
msgid "Show"
|
||||||
|
msgstr "Aufdecken"
|
||||||
|
|
||||||
|
#: templates/result.html:15
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"You have entered %(band_count)s bands or artists from %(country_count)s "
|
||||||
|
"countries."
|
||||||
|
msgstr "Du hast %(band_count)s Bands oder Solo-Musiker aus "
|
||||||
|
"%(country_count)s Ländern eingegeben."
|
||||||
|
|
||||||
|
#: templates/result.html:18
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This puts you at rank %(band_rank)s and %(country_rank)s of "
|
||||||
|
"%(user_count)s, respectively."
|
||||||
|
msgstr "Damit bist Du an %(band_rank)s. bzw. %(country_rank)s. Stelle von %(user_count)s."
|
||||||
|
|
||||||
|
#: templates/result.html:37
|
||||||
|
msgid ""
|
||||||
|
"You can leave your email address here if you want to be informed about "
|
||||||
|
"future developments"
|
||||||
|
msgstr "Wenn Du über zukünftige Entwicklungen informiert werden willst, "
|
||||||
|
"kannst Du Deine Mail-Adresse hier hinterlassen:"
|
||||||
|
|
Loading…
Reference in New Issue