Add bilingual UI and improve event actions layout

Add persistent German/English switching, translated UI messages and English 12-hour times. Align desktop event actions while preserving mobile layouts and record bilingual project guidance.

Include pending diary deletion exclusions. Validate both languages with nine tests.
This commit is contained in:
kai
2026-09-14 15:31:56 +02:00
parent b2af6c6857
commit b2af866cf0
28 changed files with 1339 additions and 506 deletions
+47 -44
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de">
<html lang="{{ language() }}">
<head>
@@ -35,6 +35,7 @@
.diary-form textarea { min-height:130px; resize:vertical; }
</style>
<link rel="stylesheet" href="/static/css/language.css">
</head>
<body>
@@ -42,7 +43,8 @@
<div class="container">
<div class="detail-topbar">
<a href="/" class="back-link">← Zurück zum Kalender</a>
<a href="/" class="back-link">{{ _('← Zurück zum Kalender') }}</a>
{% include '_language_switch.html' %}
{% include '_user_menu.html' %}
</div>
@@ -61,7 +63,7 @@
>
{% if concert.flyer_url %}
<p class="flyer-source"><a href="{{ concert.flyer_url }}" target="_blank" rel="noopener noreferrer nofollow external">Quelle</a><span class="flyer-disclaimer">Extern eingebundener Flyer. Rechte liegen beim jeweiligen Veranstalter bzw. Rechteinhaber. Hinweise zur Entfernung bitte an die Administration.</span></p>
<p class="flyer-source"><a href="{{ concert.flyer_url }}" target="_blank" rel="noopener noreferrer nofollow external">{{ _('Quelle') }}</a><span class="flyer-disclaimer">{{ _('Extern eingebundener Flyer. Rechte liegen beim jeweiligen Veranstalter bzw. Rechteinhaber. Hinweise zur Entfernung bitte an die Administration.') }}</span></p>
{% endif %}
</div>
@@ -71,8 +73,8 @@
<div class="concert-content">
<span class="event-type-badge">{{ concert.event_type_label }}</span>
{% if concert.visibility == 'friends' %}<span class="event-type-badge">🔒 Nur Freunde</span>{% elif concert.visibility == 'private' %}<span class="event-type-badge">🔐 Privat / eingeladen</span>{% endif %}
<span class="event-type-badge">{{ concert.event_type_label | t }}</span>
{% if concert.visibility == 'friends' %}<span class="event-type-badge">{{ _('🔒 Nur Freunde') }}</span>{% elif concert.visibility == 'private' %}<span class="event-type-badge">{{ _('🔐 Privat / eingeladen') }}</span>{% endif %}
<h1>
{{ concert.artist }}
@@ -82,13 +84,13 @@
{% for band in concert_bands %}<form method="post" action="/concerts/{{ concert.id }}/follow-band">
<input type="hidden" name="band_key" value="{{ band.key }}">
<input type="hidden" name="action" value="{% if band.is_following %}unfollow{% else %}follow{% endif %}">
<button class="follow-button {% if band.is_following %}is-following{% endif %}" type="submit">{% if band.is_following %}★ {{ band.name }} gefolgt{% else %}☆ {{ band.name }} folgen{% endif %}</button>
<button class="follow-button {% if band.is_following %}is-following{% endif %}" type="submit">{% if band.is_following %}★ {{ band.name }} {{ _('gefolgt') }}{% else %}☆ {{ band.name }} {{ _('folgen') }}{% endif %}</button>
</form>{% endfor %}
{% if concert.venue.id %}<form method="post" action="/concerts/{{ concert.id }}/follow-venue">
<input type="hidden" name="action" value="{% if follows_venue %}unfollow{% else %}follow{% endif %}">
<button class="follow-button {% if follows_venue %}is-following{% endif %}" type="submit">{% if follows_venue %}★ Location gefolgt{% else %}☆ Location folgen{% endif %}</button>
<button class="follow-button {% if follows_venue %}is-following{% endif %}" type="submit">{% if follows_venue %}{{ _('★ Location gefolgt') }}{% else %}{{ _('☆ Location folgen') }}{% endif %}</button>
</form>{% endif %}
<a class="follow-button" href="/following">Meine gefolgten Inhalte</a>
<a class="follow-button" href="/following">{{ _('Meine gefolgten Inhalte') }}</a>
</div>
@@ -108,11 +110,11 @@
<br>
{{ concert.time }} Uhr
{{ concert.time }} {{ _('Uhr') }}
{% if concert.event_type == 'festival' and concert.end_date %}
<br>
bis {{ concert.end_date }}{% if concert.end_time %}, {{ concert.end_time }} Uhr{% endif %}
{{ _('bis') }} {{ concert.end_date }}{% if concert.end_time %}, {{ concert.end_time }} {{ _('Uhr') }}{% endif %}
{% endif %}
</div>
@@ -191,9 +193,9 @@
{% if concert.parent_event %}
<p>
Gehört zu:
{{ _('Gehört zu:') }}
<a class="parent-event-link" href="/concerts/{{ concert.parent_event.id }}">
{{ concert.parent_event.event_type_label }} · {{ concert.parent_event.title }}
{{ concert.parent_event.event_type_label | t }} · {{ concert.parent_event.title }}
</a>
</p>
{% endif %}
@@ -204,7 +206,7 @@
<section class="description">
<h2>
Über die Veranstaltung
{{ _('Über die Veranstaltung') }}
</h2>
<p>
@@ -216,6 +218,7 @@
{% endif %}
<div class="event-actions">
{% if concert.ticket_url %}
<div class="ticket-button-container">
@@ -233,13 +236,13 @@
{% endif %}
<div class="calendar-export"><a href="/concerts/{{ concert.id }}.ics" class="concert-action">📅 In Kalender speichern (.ics)</a></div>
<div class="calendar-export"><a href="/concerts/{{ concert.id }}.ics" class="concert-action">{{ _('📅 In Kalender speichern (.ics)') }}</a></div>
{% if can_edit %}
<div class="detail-actions" aria-label="Veranstaltung verwalten">
<div class="detail-actions" aria-label="{{ _('Veranstaltung verwalten') }}">
<a href="/concerts/{{ concert.id }}/edit" class="concert-action concert-action-edit">
✏️ Veranstaltung bearbeiten
{{ _('✏️ Veranstaltung bearbeiten') }}
</a>
{% if user.is_admin and can_delete %}
@@ -247,10 +250,10 @@
<form
method="post"
action="/concerts/{{ concert.id }}/delete"
onsubmit="return confirm('Diese Veranstaltung wirklich löschen?');"
onsubmit="return confirm({{ _('Diese Veranstaltung wirklich löschen?') | tojson | forceescape }});"
>
<button type="submit" class="concert-action concert-action-delete">
🗑️ Veranstaltung löschen
{{ _('🗑️ Veranstaltung löschen') }}
</button>
</form>
@@ -260,15 +263,17 @@
{% endif %}
</div>
{% if not concert.is_past %}
<section class="attendance-section" id="attendance">
<h2>
🐧 Wer ist dabei?
{{ _('🐧 Wer ist dabei?') }}
</h2>
<p>Wähle, wie es bei dir aussieht. Deine Auswahl kann jederzeit geändert werden.</p>
<p>{{ _('Wähle, wie es bei dir aussieht. Deine Auswahl kann jederzeit geändert werden.') }}</p>
<div class="attendance-actions">
@@ -278,7 +283,7 @@
type="submit"
class="attendance-option {% if current_attendance == 'attending' %}is-selected{% endif %}"
>
✓ Zugesagt
{{ _('✓ Zugesagt') }}
</button>
</form>
@@ -288,7 +293,7 @@
type="submit"
class="attendance-option {% if current_attendance == 'maybe' %}is-selected{% endif %}"
>
? Vielleicht
{{ _('? Vielleicht') }}
</button>
</form>
@@ -298,7 +303,7 @@
type="submit"
class="attendance-option {% if current_attendance == 'ticket_search' %}is-selected{% endif %}"
>
🔎 Suche Ticket
{{ _('🔎 Suche Ticket') }}
</button>
</form>
@@ -308,7 +313,7 @@
type="submit"
class="attendance-option {% if current_attendance == 'ticket_offer' %}is-selected{% endif %}"
>
🎟️ Biete Ticket
{{ _('🎟️ Biete Ticket') }}
</button>
</form>
@@ -316,16 +321,14 @@
<details class="attendance-list">
<summary>
{{ attending_count }} Zusage{% if attending_count != 1 %}n{% endif %}
· {{ maybe_count }} Vielleicht
· {{ ticket_seeker_count }} sucht Ticket
· {{ ticket_offer_count }} bietet Ticket
{{ attending_count }} {{ _('Zusage') if attending_count == 1 else _('Zusagen') }}
· {{ maybe_count }} {{ _('Vielleicht') }} · {{ ticket_seeker_count }} {{ _('sucht Ticket') }} · {{ ticket_offer_count }} {{ _('bietet Ticket') }}
</summary>
<div class="attendance-groups">
<div>
<h3>✓ Zugesagt</h3>
<h3>{{ _('✓ Zugesagt') }}</h3>
{% if attending_users %}
<ul>
@@ -334,12 +337,12 @@
{% endfor %}
</ul>
{% else %}
<p>Noch niemand hat zugesagt.</p>
<p>{{ _('Noch niemand hat zugesagt.') }}</p>
{% endif %}
</div>
<div>
<h3>? Vielleicht</h3>
<h3>{{ _('? Vielleicht') }}</h3>
{% if maybe_users %}
<ul>
@@ -348,12 +351,12 @@
{% endfor %}
</ul>
{% else %}
<p>Noch keine Vielleicht-Angaben.</p>
<p>{{ _('Noch keine Vielleicht-Angaben.') }}</p>
{% endif %}
</div>
<div>
<h3>🔎 Sucht ein Ticket</h3>
<h3>{{ _('🔎 Sucht ein Ticket') }}</h3>
{% if ticket_seekers %}
<ul>
@@ -362,12 +365,12 @@
{% endfor %}
</ul>
{% else %}
<p>Aktuell sucht niemand ein Ticket.</p>
<p>{{ _('Aktuell sucht niemand ein Ticket.') }}</p>
{% endif %}
</div>
<div>
<h3>🎟️ Bietet ein Ticket</h3>
<h3>{{ _('🎟️ Bietet ein Ticket') }}</h3>
{% if ticket_offers %}
<ul>
@@ -376,7 +379,7 @@
{% endfor %}
</ul>
{% else %}
<p>Aktuell bietet niemand ein Ticket an.</p>
<p>{{ _('Aktuell bietet niemand ein Ticket an.') }}</p>
{% endif %}
</div>
@@ -389,31 +392,31 @@
{% if concert.is_past %}
<section class="diary-section" id="diary">
<h2>📓 Mein Konzerttagebuch</h2>
<h2>{{ _('📓 Mein Konzerttagebuch') }}</h2>
{% if can_write_diary %}
<p>Der private Eintrag wurde automatisch angelegt. <a class="button" href="/diary#concert-{{ concert.id }}">Im Konzerttagebuch bearbeiten</a></p>
<p>{{ _('Der private Eintrag wurde automatisch angelegt.') }} <a class="button" href="/diary#concert-{{ concert.id }}">{{ _('Im Konzerttagebuch bearbeiten') }}</a></p>
{% else %}
<p>Ein Tagebucheintrag wird automatisch angelegt, sobald du das vergangene Konzert als „Zugesagt“ markierst.</p>
<p>{{ _('Ein Tagebucheintrag wird automatisch angelegt, sobald du das vergangene Konzert als „Zugesagt“ markierst.') }}</p>
{% endif %}
</section>
{% endif %}
<section class="comments-section" id="comments">
<h2>💬 Kommentare</h2>
<h2>{{ _('💬 Kommentare') }}</h2>
<form method="post" action="/concerts/{{ concert.id }}/comments" class="comment-form">
<label for="comment-body">Mit anderen Konzertbesuchern austauschen</label>
<label for="comment-body">{{ _('Mit anderen Konzertbesuchern austauschen') }}</label>
<textarea
id="comment-body"
name="body"
rows="4"
maxlength="2000"
required
placeholder="Schreib einen Kommentar …"
placeholder="{{ _('Schreib einen Kommentar …') }}"
></textarea>
<button type="submit" class="attendance-option comment-submit">
Kommentar senden
{{ _('Kommentar senden') }}
</button>
</form>
@@ -433,7 +436,7 @@
{% else %}
<p class="comments-empty">Noch keine Kommentare. Mach den Anfang!</p>
<p class="comments-empty">{{ _('Noch keine Kommentare. Mach den Anfang!') }}</p>
{% endif %}