34 Commits

Author SHA1 Message Date
e5fbc14a34 Dashboard erweitert: Toggle zwischen Wochen- und 24-Stunden-Verlauf für alle Charts 2025-08-02 14:25:07 +02:00
9e025bd4c7 Überarbeite Help Modal: Floating Schließen-Button und mehrsprachige Unterstützung
- Schließen-Button ist jetzt 'floating' mit position: fixed
- Button hat Hintergrund, Rahmen und Schatten für bessere Sichtbarkeit
- Alle Texte im Help Modal verwenden jetzt Übersetzungsfunktionen
- Vollständige mehrsprachige Unterstützung (Deutsch/Englisch)
- Bessere mobile Darstellung ohne Überschneidungen
2025-08-02 14:17:46 +02:00
f4ffd14624 Update CLOC 2025-08-02 11:38:28 +02:00
4740288c45 Desktop-Layout: Sprachauswahl und Hilfe-Button etwas nach oben verschoben für bessere Balance 2025-08-02 11:31:30 +02:00
512898b34b Fix mobile layout: Verbesserte Lösung für Sprachauswahl-Überlappung mit mehr Abstand 2025-08-02 11:25:22 +02:00
872d0f9e23 Fix: Button 'Berechnen' wird jetzt korrekt als 'Calculate' in englischer Version übersetzt 2025-08-02 11:18:19 +02:00
28fda213ba Fix mobile layout: Sprachauswahl überlappt nicht mehr mit Überschrift und korrigiere URL-Parameter für Sprachwechsel 2025-08-02 11:12:37 +02:00
bdf4e134e4 Version 2025-08-02 08:50:26 +02:00
601f993ccb Scrollbar-Optimierungen und Cookie-Bereinigung 2025-08-02 08:49:46 +02:00
8fdf764a7b Version 1.4.1: Scrollbar-Optimierungen und Cookie-Bereinigung 2025-08-02 08:41:34 +02:00
b40bb666b8 Manuelle Änderungen in README.md 2025-08-01 16:47:18 +02:00
7dbe91b32e APP_VERSION 2025-08-01 16:41:36 +02:00
c7d95e5c4c feat: Implementiere mehrsprachige Unterstützung (i18n)
- Füge Flask-Babel für professionelle i18n-Implementierung hinzu
- Implementiere automatische Browser-Spracherkennung
- Erstelle datenschutzfreundliche Sprachauswahl ohne Cookies
- Verwende URL-Parameter und localStorage für Sprachauswahl
- Füge vollständige Übersetzungen für Deutsch und Englisch hinzu
- Implementiere responsive Dropdown-Sprachauswahl mit Landesflaggen
- Verbessere Barrierefreiheit mit ARIA-Attributen und Screenreader-Support
- Aktualisiere README mit i18n-Dokumentation
- Version 1.4.0
2025-08-01 16:40:46 +02:00
d88581a663 Entferne idea.txt aus dem Repository 2025-08-01 15:38:17 +02:00
00e961a6bd Entferne versehentliche less-Hilfedatei 2025-08-01 15:36:22 +02:00
d9ecdbb86e Update cloc 2025-08-01 15:28:58 +02:00
a3753e3f4e Remove dash 2025-08-01 15:27:09 +02:00
af5ff2c094 Lighthouse-Dateien in eigenen Ordner verschoben 2025-08-01 15:26:00 +02:00
200a46fdaa Add Lighthouse report JSON 2025-08-01 14:57:04 +02:00
524f44b6f0 Lighthouse-Badges auf 100% gesetzt (basierend auf aktuellem Audit) 2025-08-01 14:50:37 +02:00
3f3cb3ed01 Wikipedia link 2025-08-01 14:44:41 +02:00
74d9c18bd9 Rearrange text 2025-08-01 14:42:18 +02:00
1b91f8e54d Punkt. 2025-08-01 14:40:48 +02:00
a594d37cf1 Fix markdown linter errors 2025-08-01 14:37:47 +02:00
b95b05e54f Remove bold 2025-08-01 14:36:14 +02:00
2271934228 Add image descriptions 2025-08-01 14:35:29 +02:00
dc73e49da0 Update cloc 2025-08-01 14:34:05 +02:00
030e4adab9 Update screenshot 2025-08-01 14:33:02 +02:00
bd895e356f Verbesserte Test-Coverage auf 90% und Coverage-Badge hinzugefügt 2025-08-01 14:29:15 +02:00
046271343d Updated lighthouse-score.pdf 2025-08-01 14:10:48 +02:00
d0d8e0aeb1 v1.3.13: Weitere Verbesserung der Farbkontraste für Wochentag- und Kalenderwoche-Header 2025-08-01 13:56:34 +02:00
c19cb17623 v1.3.12: Verbesserte Farbkontraste für bessere Barrierefreiheit 2025-08-01 13:49:02 +02:00
e2367d0b0e chore: Version auf 1.3.11 erhöht - Sprachausgabe-Funktion und verbesserte Feiertage-Anzeige 2025-08-01 13:10:31 +02:00
1eb55e32dc feat: Sprachausgabe-Funktion für barrierefreie Nutzung hinzugefügt
- Vorlesen-Buttons (🔊) bei allen Ergebnissen
- Web Speech API mit deutscher Sprachausgabe
- Vollständige Tastaturnavigation (Tab, Enter, Leertaste)
- ESC-Taste zum Stoppen der Wiedergabe
- Barrierefreiheit verbessert für Menschen mit Sehbehinderungen
- README aktualisiert mit Sprachausgabe-Dokumentation
2025-08-01 12:51:28 +02:00
20 changed files with 19395 additions and 239 deletions

View File

@@ -1,6 +1,12 @@
# Elpatrons Datumsrechner # Elpatrons Datumsrechner
Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechnungen über eine übersichtliche Weboberfläche: [![Test Coverage](https://img.shields.io/badge/test%20coverage-90%25-brightgreen)](https://github.com/elpatron/datecalc)
[![Lighthouse Performance](https://img.shields.io/badge/lighthouse%20performance-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse Accessibility](https://img.shields.io/badge/lighthouse%20accessibility-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse Best Practices](https://img.shields.io/badge/lighthouse%20best%20practices-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse SEO](https://img.shields.io/badge/lighthouse%20seo-100%25-brightgreen)](https://date.elpatron.me)
Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechnungen über eine übersichtliche, barrierefreie Weboberfläche.
## Inhaltsverzeichnis ## Inhaltsverzeichnis
@@ -36,7 +42,13 @@ Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechn
Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me) Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
![image-20250725095959116](./assets/image-20250725095959116.png) ![App Screenshot](./assets/image-20250725095959116.png)
**[Lighthouse](https://en.wikipedia.org/wiki/Lighthouse_(software))-Performance-Score:**
Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (Performance, Accessibility, Best Practices, SEO).
[Lighthouse-Ergebnis (PDF)](./lighthouse/lighthouse-score.pdf)
## Funktionen ## Funktionen
@@ -48,6 +60,8 @@ Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
- Datum plus/minus X Wochen/Monate - Datum plus/minus X Wochen/Monate
- Kalenderwoche zu Datum - Kalenderwoche zu Datum
- Start-/Enddatum einer Kalenderwoche eines Jahres - Start-/Enddatum einer Kalenderwoche eines Jahres
- Mehrsprachige Unterstützung (Deutsch/Englisch) mit automatischer Browser-Spracherkennung
- Sprachausgabe für alle Ergebnisse (barrierefrei)
- Statistik-Dashboard mit Passwortschutz unter `/stats` - Statistik-Dashboard mit Passwortschutz unter `/stats`
## Bundesland-Feiertage ## Bundesland-Feiertage
@@ -74,6 +88,34 @@ Die Werktagsberechnung kann optional bundeslandspezifische Feiertage berücksich
Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei der Werktagsberechnung als arbeitsfreie Tage behandelt. Im Ergebnis werden zusätzlich die Anzahl der Wochenendtage und Feiertage angezeigt. Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei der Werktagsberechnung als arbeitsfreie Tage behandelt. Im Ergebnis werden zusätzlich die Anzahl der Wochenendtage und Feiertage angezeigt.
## Mehrsprachige Unterstützung (i18n)
Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features:
### Automatische Spracherkennung:
- *Browser-Sprache*: Automatische Erkennung der Browser-Einstellung
- *URL-Parameter*: Sprachauswahl über `?lang=de` oder `?lang=en`
- *localStorage*: Persistente Sprachauswahl im Browser
- *Fallback*: Deutsch als Standardsprache
### *Datenschutzfreundliche Implementierung:*
- *Keine Cookies*: Sprachauswahl ohne Cookies
- *URL-Parameter*: Transparente Sprachauswahl in der URL
- *localStorage*: Lokale Speicherung im Browser
- *Teilbare URLs*: URLs mit Sprachauswahl können geteilt werden
### *Barrierefreiheit:*
- *Screenreader*: Vollständige Unterstützung
- *Tastatur-Navigation*: Vollständig bedienbar
- *ARIA-Attribute*: Korrekte Beschriftungen
- *Semantische HTML*: Korrekte Struktur
### *Technische Details:*
- *Flask-Babel*: Professionelle i18n-Implementierung
- *Gettext*: Standard für Übersetzungen
- *Responsive Design*: Angepasst für alle Geräte
- *SEO-freundlich*: URLs sind indexierbar
## Installation (lokal) ## Installation (lokal)
1. Python 3.8+ installieren 1. Python 3.8+ installieren
@@ -95,7 +137,7 @@ Die App ist dann unter http://localhost:5000 erreichbar.
Das Dashboard ist mit einem statischen Passwort geschützt, das über die Umgebungsvariable `STATS_PASSWORD` gesetzt wird. Das Dashboard ist mit einem statischen Passwort geschützt, das über die Umgebungsvariable `STATS_PASSWORD` gesetzt wird.
![image-20250725100127004](./assets/image-20250725100127004.png) ![Statistics page](./assets/image-20250725100127004.png)
Beispiel (PowerShell): Beispiel (PowerShell):
@@ -178,6 +220,7 @@ Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptier
``` ```
**Mit curl:** **Mit curl:**
```bash ```bash
curl -X POST http://localhost:5000/api/tage_werktage \ curl -X POST http://localhost:5000/api/tage_werktage \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -185,6 +228,7 @@ curl -X POST http://localhost:5000/api/tage_werktage \
``` ```
**Antwort:** **Antwort:**
```json ```json
{ "result": 7 } { "result": 7 }
``` ```
@@ -200,6 +244,7 @@ curl -X POST http://localhost:5000/api/tage_werktage \
``` ```
**Mit curl:** **Mit curl:**
```bash ```bash
curl -X POST http://localhost:5000/api/wochentag \ curl -X POST http://localhost:5000/api/wochentag \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -207,6 +252,7 @@ curl -X POST http://localhost:5000/api/wochentag \
``` ```
**Antwort:** **Antwort:**
```json ```json
{ "result": "Montag" } { "result": "Montag" }
``` ```
@@ -220,6 +266,7 @@ curl -X POST http://localhost:5000/api/wochentag \
``` ```
**Mit curl:** **Mit curl:**
```bash ```bash
curl -X POST http://localhost:5000/api/kw_berechnen \ curl -X POST http://localhost:5000/api/kw_berechnen \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -227,6 +274,7 @@ curl -X POST http://localhost:5000/api/kw_berechnen \
``` ```
**Antwort:** **Antwort:**
```json ```json
{ "result": "KW 24 (2024)", "kw": 24, "jahr": 2024 } { "result": "KW 24 (2024)", "kw": 24, "jahr": 2024 }
``` ```
@@ -240,6 +288,7 @@ curl -X POST http://localhost:5000/api/kw_berechnen \
``` ```
**Mit curl:** **Mit curl:**
```bash ```bash
curl -X POST http://localhost:5000/api/kw_datum \ curl -X POST http://localhost:5000/api/kw_datum \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -247,6 +296,7 @@ curl -X POST http://localhost:5000/api/kw_datum \
``` ```
**Antwort:** **Antwort:**
```json ```json
{ {
"result": "10.06.2024 bis 16.06.2024", "result": "10.06.2024 bis 16.06.2024",
@@ -270,6 +320,7 @@ curl -X POST http://localhost:5000/api/kw_datum \
``` ```
**Mit curl:** **Mit curl:**
```bash ```bash
curl -X POST http://localhost:5000/api/plusminus \ curl -X POST http://localhost:5000/api/plusminus \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -277,6 +328,7 @@ curl -X POST http://localhost:5000/api/plusminus \
``` ```
**Antwort:** **Antwort:**
```json ```json
{ "result": "2024-06-15" } { "result": "2024-06-15" }
``` ```
@@ -291,11 +343,13 @@ curl -X POST http://localhost:5000/api/plusminus \
**GET** `/api/stats` **GET** `/api/stats`
**Mit curl:** **Mit curl:**
```bash ```bash
curl http://localhost:5000/api/stats curl http://localhost:5000/api/stats
``` ```
**Antwort:** **Antwort:**
```json ```json
{ {
"pageviews": 42, "pageviews": 42,
@@ -309,11 +363,13 @@ curl http://localhost:5000/api/stats
**GET** `/api/monitor` **GET** `/api/monitor`
**Mit curl:** **Mit curl:**
```bash ```bash
curl http://localhost:5000/api/monitor curl http://localhost:5000/api/monitor
``` ```
**Antwort:** **Antwort:**
```json ```json
{ {
"status": "ok", "status": "ok",
@@ -408,6 +464,7 @@ Es werden keine IP-Adressen oder sonstigen persönlichen Daten gespeichert, ledi
- *Fokus-Indikatoren:* Deutliche visuelle Hervorhebung des Fokus für alle Bedienelemente. - *Fokus-Indikatoren:* Deutliche visuelle Hervorhebung des Fokus für alle Bedienelemente.
- *Farbkontraste:* Hohe Kontraste für Texte, Buttons und Ergebnisboxen, geprüft nach WCAG-Richtlinien. - *Farbkontraste:* Hohe Kontraste für Texte, Buttons und Ergebnisboxen, geprüft nach WCAG-Richtlinien.
- *Status- und Fehlermeldungen:* Ergebnisse und Fehler werden mit `aria-live` für Screenreader zugänglich gemacht. - *Status- und Fehlermeldungen:* Ergebnisse und Fehler werden mit `aria-live` für Screenreader zugänglich gemacht.
- *Sprachausgabe:* Alle Ergebnisse können über 🔊-Buttons vorgelesen werden (Web Speech API, deutsche Sprache).
- *Mobile Optimierung:* Zusätzliche Meta-Tags für bessere Bedienbarkeit auf mobilen Geräten und Unterstützung von Screenreadern. - *Mobile Optimierung:* Zusätzliche Meta-Tags für bessere Bedienbarkeit auf mobilen Geräten und Unterstützung von Screenreadern.
- *SEO:* Das Thema Barrierefreiheit ist in den Meta-Tags für Suchmaschinen sichtbar. - *SEO:* Das Thema Barrierefreiheit ist in den Meta-Tags für Suchmaschinen sichtbar.
@@ -415,21 +472,23 @@ Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Seh
### Code Statistik ### Code Statistik
cloc|github.com/AlDanial/cloc v 2.06 T=0.06 s (263.5 files/s, 39193.4 lines/s) cloc|github.com/AlDanial/cloc v 2.06 T=0.17 s (146.7 files/s, 35235.5 lines/s)
--- | --- --- | ---
Language|files|blank|comment|code Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------: :-------|-------:|-------:|-------:|-------:
HTML|4|21|6|927 HTML|8|48|6|2092
Python|2|34|26|482 Python|2|59|68|690
Markdown|2|116|0|328 JavaScript|2|95|87|571
JSON|2|0|0|237 Markdown|3|176|0|493
JavaScript|1|0|0|20 PO File|2|234|240|492
JSON|3|0|0|243
CSS|1|186|3|188
SVG|2|0|0|14 SVG|2|0|0|14
Dockerfile|1|5|6|8 Dockerfile|1|5|6|8
DOS Batch|1|0|0|1 DOS Batch|1|0|0|1
--------|--------|--------|--------|-------- --------|--------|--------|--------|--------
SUM:|15|176|38|2017 SUM:|25|803|410|4792
## Lizenz ## Lizenz
@@ -437,3 +496,5 @@ Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).
--- ---
(c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron) (c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron)
**Version 1.4.0** - Mehrsprachige Unterstützung hinzugefügt

186
app.py
View File

@@ -1,4 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify, g
from flask_babel import Babel, gettext, ngettext, get_locale
from datetime import datetime, timedelta from datetime import datetime, timedelta
import numpy as np import numpy as np
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -11,12 +12,40 @@ app_start_time = time.time()
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'dev-key') app.secret_key = os.environ.get('SECRET_KEY', 'dev-key')
# Babel Konfiguration
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en']
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
babel = Babel()
# Version der App # Version der App
APP_VERSION = "1.3.10" APP_VERSION = "1.4.6"
# HTML-Template wird jetzt aus templates/index.html geladen # HTML-Template wird jetzt aus templates/index.html geladen
WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
WOCHENTAGE_EN = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def get_locale():
# Prüfe URL-Parameter für Sprachauswahl (datenschutzfreundlich)
if request.args.get('lang') in ['de', 'en']:
return request.args.get('lang')
# Prüfe Session für Sprachauswahl (Fallback für ältere Browser)
if 'language' in session:
return session['language']
# Fallback auf Browser-Sprache
return request.accept_languages.best_match(['de', 'en'], default='de')
# Registriere die Locale-Funktion mit Babel
babel.init_app(app, locale_selector=get_locale)
def get_wochentage():
"""Gibt die Wochentage in der aktuellen Sprache zurück"""
locale = get_locale()
if locale == 'en':
return WOCHENTAGE_EN
return WOCHENTAGE
def get_feiertage(year, bundesland): def get_feiertage(year, bundesland):
"""Holt die Feiertage für ein Jahr und Bundesland von feiertage-api.de.""" """Holt die Feiertage für ein Jahr und Bundesland von feiertage-api.de."""
@@ -30,6 +59,24 @@ def get_feiertage(year, bundesland):
print(f"Fehler beim Abrufen der Feiertage: {e}") print(f"Fehler beim Abrufen der Feiertage: {e}")
return [] return []
@app.route('/set_language/<language>')
def set_language(language):
"""Setzt die Sprache über URL-Parameter (datenschutzfreundlich)"""
if language in ['de', 'en']:
# URL-Parameter verwenden statt Session
referrer = request.referrer or url_for('index')
if '?' in referrer:
# URL hat bereits Parameter
if 'lang=' in referrer:
# Ersetze bestehenden lang-Parameter
import re
referrer = re.sub(r'[?&]lang=[^&]*', '', referrer)
return redirect(f"{referrer}&lang={language}")
else:
# URL hat keine Parameter
return redirect(f"{referrer}?lang={language}")
return redirect(request.referrer or url_for('index'))
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
def index(): def index():
# Rudimentäres Logging für Page Impressions # Rudimentäres Logging für Page Impressions
@@ -94,24 +141,29 @@ def index():
else: else:
tage = abs((d2 - d1).days) tage = abs((d2 - d1).days)
except Exception: except Exception:
tage = 'Ungültige Eingabe' tage = gettext('Ungültige Eingabe')
elif action == 'wochentag': elif action == 'wochentag':
active_idx = 1 active_idx = 1
datum = request.form.get('datum3') datum = request.form.get('datum3')
try: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()] wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
except Exception: except Exception:
wochentag = 'Ungültige Eingabe' wochentag = gettext('Ungültige Eingabe')
elif action == 'kw_berechnen': elif action == 'kw_berechnen':
active_idx = 2 active_idx = 2
datum = request.form.get('datum6') datum = request.form.get('datum6')
try: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
kw = d.isocalendar().week kw = d.isocalendar().week
kw_berechnen = f"KW {kw} ({d.year})" locale = get_locale()
if locale == 'en':
kw_berechnen = f"Week {kw} ({d.year})"
else:
kw_berechnen = f"KW {kw} ({d.year})"
except Exception: except Exception:
kw_berechnen = 'Ungültige Eingabe' kw_berechnen = gettext('Ungültige Eingabe')
elif action == 'kw_datum': elif action == 'kw_datum':
active_idx = 3 active_idx = 3
jahr = request.form.get('jahr7') jahr = request.form.get('jahr7')
@@ -122,9 +174,13 @@ def index():
# Montag der KW # Montag der KW
start = datetime.fromisocalendar(jahr, kw, 1) start = datetime.fromisocalendar(jahr, kw, 1)
end = datetime.fromisocalendar(jahr, kw, 7) end = datetime.fromisocalendar(jahr, kw, 7)
kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}" locale = get_locale()
if locale == 'en':
kw_datum = f"{start.strftime('%m/%d/%Y')} to {end.strftime('%m/%d/%Y')}"
else:
kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}"
except Exception: except Exception:
kw_datum = 'Ungültige Eingabe' kw_datum = gettext('Ungültige Eingabe')
elif action == 'plusminus': elif action == 'plusminus':
active_idx = 4 active_idx = 4
datum = request.form.get('datum_pm') datum = request.form.get('datum_pm')
@@ -137,57 +193,107 @@ def index():
anzahl_int = int(anzahl) anzahl_int = int(anzahl)
if richtung == 'sub': if richtung == 'sub':
anzahl_int = -anzahl_int anzahl_int = -anzahl_int
locale = get_locale()
if einheit == 'tage': if einheit == 'tage':
if is_werktage: if is_werktage:
# Werktage: numpy busday_offset # Werktage: numpy busday_offset
result = np.busday_offset(d.date(), anzahl_int, roll='forward') result = np.busday_offset(d.date(), anzahl_int, roll='forward')
result_dt = datetime.strptime(str(result), '%Y-%m-%d') result_dt = datetime.strptime(str(result), '%Y-%m-%d')
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Werktage: {result_dt.strftime('%d.%m.%Y')}" if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} workdays: {result_dt.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Werktage: {result_dt.strftime('%d.%m.%Y')}"
else: else:
result = d + timedelta(days=anzahl_int) result = d + timedelta(days=anzahl_int)
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Tage: {result.strftime('%d.%m.%Y')}" if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} days: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Tage: {result.strftime('%d.%m.%Y')}"
elif einheit == 'wochen': elif einheit == 'wochen':
if is_werktage: if is_werktage:
plusminus_result = 'Nicht unterstützt: Werktage + Wochen.' plusminus_result = gettext('Nicht unterstützt: Werktage + Wochen.')
else: else:
result = d + timedelta(weeks=anzahl_int) result = d + timedelta(weeks=anzahl_int)
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Wochen: {result.strftime('%d.%m.%Y')}" if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} weeks: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Wochen: {result.strftime('%d.%m.%Y')}"
elif einheit == 'monate': elif einheit == 'monate':
if is_werktage: if is_werktage:
plusminus_result = 'Nicht unterstützt: Werktage + Monate.' plusminus_result = gettext('Nicht unterstützt: Werktage + Monate.')
else: else:
result = d + relativedelta(months=anzahl_int) result = d + relativedelta(months=anzahl_int)
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}" if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} months: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
except Exception: except Exception:
plusminus_result = 'Ungültige Eingabe' plusminus_result = gettext('Ungültige Eingabe')
return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
, feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION , feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION, get_locale=get_locale
) )
def parse_log_stats(log_path): def parse_log_stats(log_path):
pageviews = 0 pageviews = 0
func_counts = {} func_counts = {}
func_counts_hourly = {}
impressions_per_day = {} impressions_per_day = {}
impressions_per_hour = {}
api_counts = {} api_counts = {}
api_counts_hourly = {}
if os.path.exists(log_path): if os.path.exists(log_path):
with open(log_path, encoding='utf-8') as f: with open(log_path, encoding='utf-8') as f:
for line in f: for line in f:
if 'PAGEVIEW' in line: if 'PAGEVIEW' in line:
pageviews += 1 pageviews += 1
try: try:
date = line[:10] # Parse timestamp (format: YYYY-MM-DDTHH:MM:SS)
timestamp = line[:19] # First 19 chars for YYYY-MM-DDTHH:MM:SS
date = timestamp[:10] # YYYY-MM-DD
hour = timestamp[11:13] # HH
if len(date) == 10 and date[4] == '-' and date[7] == '-': if len(date) == 10 and date[4] == '-' and date[7] == '-':
impressions_per_day[date] = impressions_per_day.get(date, 0) + 1 impressions_per_day[date] = impressions_per_day.get(date, 0) + 1
if len(hour) == 2 and hour.isdigit():
hour_key = f"{date} {hour}:00"
impressions_per_hour[hour_key] = impressions_per_hour.get(hour_key, 0) + 1
except Exception: except Exception:
pass pass
elif 'FUNC:' in line: elif 'FUNC:' in line:
func = line.split('FUNC:')[1].strip() func = line.split('FUNC:')[1].strip()
func_counts[func] = func_counts.get(func, 0) + 1 func_counts[func] = func_counts.get(func, 0) + 1
# Stündliche Funktionsaufrufe
try:
timestamp = line[:19]
date = timestamp[:10]
hour = timestamp[11:13]
if len(hour) == 2 and hour.isdigit():
hour_key = f"{date} {hour}:00"
if hour_key not in func_counts_hourly:
func_counts_hourly[hour_key] = {}
func_counts_hourly[hour_key][func] = func_counts_hourly[hour_key].get(func, 0) + 1
except Exception:
pass
elif 'FUNC_API:' in line: elif 'FUNC_API:' in line:
api = line.split('FUNC_API:')[1].strip() api = line.split('FUNC_API:')[1].strip()
api_counts[api] = api_counts.get(api, 0) + 1 api_counts[api] = api_counts.get(api, 0) + 1
return pageviews, func_counts, impressions_per_day, api_counts
# Stündliche API-Aufrufe
try:
timestamp = line[:19]
date = timestamp[:10]
hour = timestamp[11:13]
if len(hour) == 2 and hour.isdigit():
hour_key = f"{date} {hour}:00"
if hour_key not in api_counts_hourly:
api_counts_hourly[hour_key] = {}
api_counts_hourly[hour_key][api] = api_counts_hourly[hour_key].get(api, 0) + 1
except Exception:
pass
return pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly
@app.route('/stats', methods=['GET', 'POST']) @app.route('/stats', methods=['GET', 'POST'])
def stats(): def stats():
@@ -201,8 +307,8 @@ def stats():
return render_template('stats_login.html', error='Falsches Passwort!') return render_template('stats_login.html', error='Falsches Passwort!')
return render_template('stats_login.html', error=None) return render_template('stats_login.html', error=None)
log_path = os.path.join('log', 'pageviews.log') log_path = os.path.join('log', 'pageviews.log')
pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path) pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts) return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, func_counts_hourly=func_counts_hourly, impressions_per_day=impressions_per_day, impressions_per_hour=impressions_per_hour, api_counts=api_counts, api_counts_hourly=api_counts_hourly)
# --- REST API --- # --- REST API ---
def log_api_usage(api_name): def log_api_usage(api_name):
@@ -247,7 +353,8 @@ def api_wochentag():
datum = data.get('datum') datum = data.get('datum')
try: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()] wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
return jsonify({'result': wochentag}) return jsonify({'result': wochentag})
except Exception as e: except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -260,7 +367,12 @@ def api_kw_berechnen():
try: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
kw = d.isocalendar().week kw = d.isocalendar().week
return jsonify({'result': f"KW {kw} ({d.year})", 'kw': kw, 'jahr': d.year}) locale = get_locale()
if locale == 'en':
kw_berechnen = f"Week {kw} ({d.year})"
else:
kw_berechnen = f"KW {kw} ({d.year})"
return jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
except Exception as e: except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -275,7 +387,12 @@ def api_kw_datum():
kw = int(kw) kw = int(kw)
start = datetime.fromisocalendar(jahr, kw, 1) start = datetime.fromisocalendar(jahr, kw, 1)
end = datetime.fromisocalendar(jahr, kw, 7) end = datetime.fromisocalendar(jahr, kw, 7)
return jsonify({'result': f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}", 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')}) locale = get_locale()
if locale == 'en':
kw_datum = f"{start.strftime('%m/%d/%Y')} to {end.strftime('%m/%d/%Y')}"
else:
kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}"
return jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
except Exception as e: except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -293,26 +410,39 @@ def api_plusminus():
anzahl_int = int(anzahl) anzahl_int = int(anzahl)
if richtung == 'sub': if richtung == 'sub':
anzahl_int = -anzahl_int anzahl_int = -anzahl_int
locale = get_locale()
if einheit == 'tage': if einheit == 'tage':
if is_werktage: if is_werktage:
result = np.busday_offset(d.date(), anzahl_int, roll='forward') result = np.busday_offset(d.date(), anzahl_int, roll='forward')
result_dt = datetime.strptime(str(result), '%Y-%m-%d') result_dt = datetime.strptime(str(result), '%Y-%m-%d')
return jsonify({'result': result_dt.strftime('%Y-%m-%d')}) if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} workdays: {result_dt.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Werktage: {result_dt.strftime('%d.%m.%Y')}"
else: else:
result = d + timedelta(days=anzahl_int) result = d + timedelta(days=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')}) if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} days: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Tage: {result.strftime('%d.%m.%Y')}"
elif einheit == 'wochen': elif einheit == 'wochen':
if is_werktage: if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400 return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
else: else:
result = d + timedelta(weeks=anzahl_int) result = d + timedelta(weeks=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')}) if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} weeks: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Wochen: {result.strftime('%d.%m.%Y')}"
elif einheit == 'monate': elif einheit == 'monate':
if is_werktage: if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400 return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
else: else:
result = d + relativedelta(months=anzahl_int) result = d + relativedelta(months=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')}) if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} months: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
else: else:
return jsonify({'error': 'Ungültige Einheit'}), 400 return jsonify({'error': 'Ungültige Einheit'}), 400
except Exception as e: except Exception as e:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 13 KiB

3
babel.cfg Normal file
View File

@@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@@ -1 +1 @@
@cloc . .\templates\ .\static\ --exclude-dir=.venv,.git,log,__pycache__,.pytest_cache --exclude-ext=txt,bak --md @cloc . .\templates\ .\static\ --exclude-dir=.venv,.git,log,__pycache__,.pytest_cache,lighthouse --exclude-ext=txt,bak --md

138
i18n_implementation.md Normal file
View File

@@ -0,0 +1,138 @@
# Internationalisierung (i18n) Implementierung
## Übersicht
Die Internationalisierung wurde erfolgreich in Elpatrons Datumsrechner implementiert. Das System unterstützt derzeit Deutsch (Standard) und Englisch.
## Implementierte Funktionen
### 1. Flask-Babel Integration
- **Flask-Babel 4.0.0** für Übersetzungsverwaltung
- **Babel 2.17.0** für Übersetzungskompilierung
- **Automatische Spracherkennung** basierend auf Browser-Einstellungen
- **Session-basierte Sprachauswahl** für Benutzer
### 2. Sprachauswahl
- **DE/EN Toggle** in der oberen linken Ecke
- **Visueller Indikator** für aktive Sprache
- **Persistente Sprachauswahl** über Session
### 3. Übersetzungsdateien
- **Deutsche Übersetzungen**: `translations/de/LC_MESSAGES/messages.po`
- **Englische Übersetzungen**: `translations/en/LC_MESSAGES/messages.po`
- **Kompilierte .mo Dateien** für optimale Performance
### 4. Übersetzte Inhalte
#### Meta-Tags
- Titel und Beschreibung
- Open Graph Tags
- Keywords
#### UI-Elemente
- Haupttitel und Untertitel
- Formular-Labels
- Button-Texte
- Hilfe-Texte
- Footer-Links
#### Dynamische Inhalte
- Wochentage (Deutsch/Englisch)
- Kalenderwochen-Format
- Datumsformate (DD.MM.YYYY vs MM/DD/YYYY)
- Fehlermeldungen
### 5. Technische Details
#### App-Konfiguration
```python
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en']
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
```
#### Sprachauswahl-Funktion
```python
@app.route('/set_language/<language>')
def set_language(language):
if language in ['de', 'en']:
session['language'] = language
return redirect(request.referrer or url_for('index'))
```
#### Locale-Selector
```python
@babel.localeselector
def get_locale():
if 'language' in session:
return session['language']
return request.accept_languages.best_match(['de', 'en'], default='de')
```
## Verzeichnisstruktur
```
translations/
├── de/
│ └── LC_MESSAGES/
│ ├── messages.po
│ └── messages.mo
└── en/
└── LC_MESSAGES/
├── messages.po
└── messages.mo
```
## Verwendung
### Für Entwickler
1. **Neue Texte hinzufügen**:
```html
{{ _('Neuer Text') }}
```
2. **Übersetzungen extrahieren**:
```bash
pybabel extract -F babel.cfg -k _l -o messages.pot .
```
3. **Neue Übersetzungsdatei erstellen**:
```bash
pybabel init -i messages.pot -d translations -l en
```
4. **Übersetzungen kompilieren**:
```bash
pybabel compile -d translations
```
### Für Benutzer
- **Sprachauswahl**: Klick auf DE/EN in der oberen linken Ecke
- **Automatische Erkennung**: Browser-Sprache wird automatisch erkannt
- **Persistenz**: Sprachauswahl bleibt über Session erhalten
## Nächste Schritte
1. **Weitere Sprachen hinzufügen** (z.B. Französisch, Spanisch)
2. **Pluralisierung** für verschiedene Sprachen implementieren
3. **Dynamische Übersetzungen** für API-Responses
4. **RTL-Sprachen** unterstützen (Arabisch, Hebräisch)
## Dateien
- `app.py`: Hauptanwendung mit i18n-Integration
- `babel.cfg`: Babel-Konfiguration
- `requirements.txt`: Flask-Babel Abhängigkeit
- `templates/index.html`: Übersetzte UI-Templates
- `translations/`: Übersetzungsdateien
## Status
**Vollständig implementiert und funktionsfähig**
- Deutsche und englische Übersetzungen
- Sprachauswahl-UI
- Automatische Spracherkennung
- Session-basierte Persistenz
- Kompilierte Übersetzungen für Performance

View File

@@ -1,8 +0,0 @@
erstelle eine python web app, die verschiedene datumsberechnungen durchführt:
- Berechnung der Anzahl der Tage zwischen zwei Daten
- Berechnung der Anzahl der Werktage zwischen zwei Daten
- Anzeige des Wochentags eines Datums
Beachte:
- Virtual Environment unter ./.venv
- Wir entwickeln unter Windows

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,5 +1,5 @@
Flask==3.0.3 Flask==3.0.0
numpy==1.26.4 numpy==1.26.4
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytest==8.2.2 requests==2.31.0
requests Flask-Babel==4.0.0

View File

@@ -1,4 +1,4 @@
{ .v{
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "Elpatrons Datumsrechner API", "title": "Elpatrons Datumsrechner API",

View File

@@ -9,11 +9,11 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen</title> <title>{{ _('Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen') }}</title>
<meta name="description" content="Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."> <meta name="description" content="{{ _('Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos.') }}">
<meta name="keywords" content="Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"> <meta name="keywords" content="{{ _('Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei') }}">
<meta property="og:title" content="Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"> <meta property="og:title" content="{{ _('Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen') }}">
<meta property="og:description" content="Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."> <meta property="og:description" content="{{ _('Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos.') }}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://codeberg.org/elpatron/datecalc"> <meta property="og:url" content="https://codeberg.org/elpatron/datecalc">
<meta property="og:image" content="/static/logo.svg"> <meta property="og:image" content="/static/logo.svg">
@@ -40,6 +40,11 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
html {
overflow-x: hidden;
width: 100%;
}
body { body {
background: var(--background); background: var(--background);
color: var(--text); color: var(--text);
@@ -47,7 +52,10 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
min-height: 100vh;
width: 100%;
} }
.container { .container {
max-width: 480px; max-width: 480px;
@@ -60,10 +68,11 @@ body {
border: 1px solid var(--border); border: 1px solid var(--border);
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
} }
.help-button-container { .help-button-container {
position: absolute; position: absolute;
top: 2.5em; top: 1em;
right: 2em; right: 2em;
z-index: 10; z-index: 10;
} }
@@ -71,21 +80,21 @@ body {
width: 2.2em; width: 2.2em;
height: 2.2em; height: 2.2em;
border-radius: 50%; border-radius: 50%;
background: rgba(37, 99, 235, 0.1); background: rgba(37, 99, 235, 0.15);
color: var(--primary); color: var(--primary-dark);
border: 1px solid var(--border); border: 1px solid var(--border);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 600;
transition: all 0.2s; transition: all 0.2s;
min-width: 44px; min-width: 44px;
min-height: 44px; min-height: 44px;
} }
.help-button:hover { .help-button:hover {
background: rgba(37, 99, 235, 0.2); background: rgba(37, 99, 235, 0.25);
border-color: var(--primary); border-color: var(--primary);
} }
.help-button:focus { .help-button:focus {
@@ -145,8 +154,9 @@ body {
padding: 2em; padding: 2em;
max-width: 90%; max-width: 90%;
width: 90%; width: 90%;
max-height: 90%; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
position: relative; position: relative;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
margin: 0 auto; margin: 0 auto;
@@ -155,17 +165,17 @@ body {
box-sizing: border-box; box-sizing: border-box;
} }
.modal-close { .modal-close {
position: absolute; position: fixed;
top: 1em; top: 1em;
right: 1em; right: 1em;
background: none; background: rgba(255, 255, 255, 0.95);
border: none; border: 2px solid var(--border);
font-size: 1.5em; font-size: 1.5em;
cursor: pointer; cursor: pointer;
color: var(--text); color: var(--text);
padding: 0.5em; padding: 0.5em;
border-radius: 50%; border-radius: 50%;
transition: background 0.2s; transition: all 0.2s;
width: 2.5em; width: 2.5em;
height: 2.5em; height: 2.5em;
display: flex; display: flex;
@@ -173,6 +183,8 @@ body {
justify-content: center; justify-content: center;
min-width: 44px; min-width: 44px;
min-height: 44px; min-height: 44px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1001;
} }
.modal-close:hover { .modal-close:hover {
background: var(--border); background: var(--border);
@@ -243,7 +255,7 @@ input[type="date"] {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 1em; font-size: 1em;
background: #f1f5f9; background: #ffffff;
color: var(--text); color: var(--text);
} }
.today-btn { .today-btn {
@@ -292,12 +304,49 @@ button:focus, .accordion-header:focus {
.result { .result {
margin-top: 1em; margin-top: 1em;
font-weight: bold; font-weight: bold;
background: #e0e7ff; background: #dbeafe;
color: #1e293b; color: #1e293b;
border-radius: 6px; border-radius: 6px;
padding: 0.7em 1em; padding: 0.7em 1em;
padding-right: 4em;
box-shadow: 0 1px 2px rgba(30,41,59,0.04); box-shadow: 0 1px 2px rgba(30,41,59,0.04);
border: 2px solid #2563eb; border: 2px solid #2563eb;
position: relative;
min-height: 3.5em;
}
.read-aloud-btn {
position: absolute;
top: 0.5em;
right: 0.5em;
background: rgba(37, 99, 235, 0.15);
color: var(--primary-dark);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.3em 0.6em;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.read-aloud-btn:hover {
background: rgba(37, 99, 235, 0.25);
border-color: var(--primary);
}
.read-aloud-btn:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
background: rgba(37, 99, 235, 0.25);
border-color: var(--primary);
}
.read-aloud-btn.playing {
background: var(--primary);
color: white;
} }
.accordion { .accordion {
border-radius: 12px; border-radius: 12px;
@@ -308,6 +357,7 @@ button:focus, .accordion-header:focus {
/* Layout-Shift-Prävention */ /* Layout-Shift-Prävention */
min-height: 200px; min-height: 200px;
contain: layout style paint; contain: layout style paint;
width: 100%;
} }
.accordion-item + .accordion-item { .accordion-item + .accordion-item {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
@@ -337,6 +387,7 @@ button:focus, .accordion-header:focus {
overflow: hidden; overflow: hidden;
transition: max-height 0.3s ease-out, padding 0.3s ease-out; transition: max-height 0.3s ease-out, padding 0.3s ease-out;
opacity: 0; opacity: 0;
width: 100%;
} }
.accordion-content.active { .accordion-content.active {
display: block; display: block;
@@ -361,11 +412,11 @@ button:focus, .accordion-header:focus {
color: #fff; color: #fff;
} }
.header-wochentag { .header-wochentag {
background: #f59e42; background: #dc2626;
color: #1e293b; color: #fff;
} }
.header-wochentag.active, .header-wochentag:hover { .header-wochentag.active, .header-wochentag:hover {
background: #d97706; background: #b91c1c;
color: #fff; color: #fff;
} }
.header-plusminus-tage { .header-plusminus-tage {
@@ -393,19 +444,19 @@ button:focus, .accordion-header:focus {
color: #fff; color: #fff;
} }
.header-kw { .header-kw {
background: #a78bfa;
color: #1e293b;
}
.header-kw.active, .header-kw:hover {
background: #7c3aed; background: #7c3aed;
color: #fff; color: #fff;
} }
.header-kw.active, .header-kw:hover {
background: #6d28d9;
color: #fff;
}
.header-kw-datum { .header-kw-datum {
background: #facc15; background: #a16207;
color: #1e293b; color: #fff;
} }
.header-kw-datum.active, .header-kw-datum:hover { .header-kw-datum.active, .header-kw-datum:hover {
background: #ca8a04; background: #854d0e;
color: #fff; color: #fff;
} }
.header-plusminus { .header-plusminus {
@@ -422,12 +473,16 @@ button:focus, .accordion-header:focus {
padding: 1.2em 0.7em 1em 0.7em; padding: 1.2em 0.7em 1em 0.7em;
width: calc(100% - 2em); width: calc(100% - 2em);
max-width: none; max-width: none;
overflow: hidden;
}
.header-section {
margin-top: 4.5em; /* Mehr Abstand für Sprachauswahl und Hilfe-Button */
} }
h1 { h1 {
font-size: 1.3em; font-size: 1.3em;
} }
.help-button-container { .help-button-container {
top: 1.5em; top: 1em;
right: 1.2em; right: 1.2em;
} }
.help-button { .help-button {
@@ -437,6 +492,17 @@ button:focus, .accordion-header:focus {
min-width: 48px; min-width: 48px;
min-height: 48px; min-height: 48px;
} }
.language-selector {
top: 1em;
left: 1.2em;
}
#language-dropdown {
min-width: 100px;
font-size: 0.85em;
padding: 0.4em 0.6em;
}
.help-tooltip { .help-tooltip {
font-size: 0.8em; font-size: 0.8em;
padding: 0.4em 0.6em; padding: 0.4em 0.6em;
@@ -446,9 +512,84 @@ button:focus, .accordion-header:focus {
margin: 1em; margin: 1em;
width: calc(100% - 2em); width: calc(100% - 2em);
max-width: none; max-width: none;
max-height: 85vh;
left: 0; left: 0;
transform: none; transform: none;
overflow-y: auto;
overflow-x: hidden;
} }
.modal-close {
top: 0.8em;
right: 0.8em;
font-size: 1.3em;
width: 2.2em;
height: 2.2em;
min-width: 48px;
min-height: 48px;
background: rgba(255, 255, 255, 0.98);
border: 2px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.modal-content h1 {
margin-top: 0;
margin-bottom: 1em;
font-size: 1.2em;
line-height: 1.3;
}
}
/* Sprachauswahl */
.language-selector {
position: absolute;
top: 1em;
left: 2em;
z-index: 10;
}
#language-dropdown {
padding: 0.5em 0.8em;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
font-size: 0.9em;
font-weight: 600;
cursor: pointer;
min-width: 120px;
min-height: 44px;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(30,41,59,0.05);
}
#language-dropdown:hover {
border-color: var(--primary);
box-shadow: 0 2px 6px rgba(30,41,59,0.1);
}
#language-dropdown:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
border-color: var(--primary);
}
#language-dropdown option {
padding: 0.5em;
font-size: 0.9em;
}
/* Screen Reader Only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
} }
/* Touch-Target Optimierungen für Footer-Links */ /* Touch-Target Optimierungen für Footer-Links */
@@ -498,6 +639,16 @@ footer br + a {
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
document.getElementById(id).value = today; document.getElementById(id).value = today;
} }
function changeLanguage(language) {
// Speichere Sprache in localStorage (datenschutzfreundlich)
localStorage.setItem('preferred_language', language);
// Erstelle neue URL mit korrektem lang-Parameter
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('lang', language);
window.location.href = currentUrl.toString();
}
function openAccordion(idx) { function openAccordion(idx) {
const headers = document.querySelectorAll('.accordion-header'); const headers = document.querySelectorAll('.accordion-header');
const panels = document.querySelectorAll('.accordion-content'); const panels = document.querySelectorAll('.accordion-content');
@@ -525,7 +676,84 @@ footer br + a {
// Fokus zurück auf den Hilfe-Button setzen // Fokus zurück auf den Hilfe-Button setzen
document.querySelector('.help-button').focus(); document.querySelector('.help-button').focus();
} }
// Sprachausgabe-Funktionalität
let currentSpeech = null;
function readAloud(text, button) {
// Stoppe vorherige Wiedergabe
if (currentSpeech) {
currentSpeech.cancel();
}
// Entferne "playing" Klasse von allen Buttons
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
btn.classList.remove('playing');
btn.textContent = '🔊';
});
// Erstelle neue Sprachausgabe
currentSpeech = new SpeechSynthesisUtterance(text);
currentSpeech.lang = 'de-DE';
currentSpeech.rate = 0.9;
currentSpeech.pitch = 1;
// Button-Status aktualisieren
button.classList.add('playing');
button.textContent = '⏹️';
// Event-Handler für Ende der Wiedergabe
currentSpeech.onend = function() {
button.classList.remove('playing');
button.textContent = '🔊';
currentSpeech = null;
};
currentSpeech.onerror = function() {
button.classList.remove('playing');
button.textContent = '🔊';
currentSpeech = null;
};
// Wiedergabe starten
speechSynthesis.speak(currentSpeech);
}
function readAloudFromElement(button) {
// Finde das Ergebnis-Element (das div mit class="result")
const resultElement = button.closest('.result');
if (!resultElement) return;
// Entferne den Button-Text aus dem zu lesenden Text
const buttonText = button.textContent;
let textToRead = resultElement.textContent.replace(buttonText, '').trim();
// Bereinige den Text (entferne HTML-Tags und überschüssige Leerzeichen)
textToRead = textToRead.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
readAloud(textToRead, button);
}
function stopReading() {
if (currentSpeech) {
currentSpeech.cancel();
currentSpeech = null;
}
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
btn.classList.remove('playing');
btn.textContent = '🔊';
});
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Prüfe localStorage für gespeicherte Sprachauswahl
const savedLanguage = localStorage.getItem('preferred_language');
if (savedLanguage && !window.location.search.includes('lang=')) {
// Wenn Sprache in localStorage gespeichert ist, aber nicht in URL
window.location.href = window.location.pathname + '?lang=' + savedLanguage;
return;
}
// Sofortige Aktivierung der ersten Accordion-Sektion um Layout-Shifts zu vermeiden // Sofortige Aktivierung der ersten Accordion-Sektion um Layout-Shifts zu vermeiden
const activeIdx = parseInt("{{ active_idx|default(0) }}"); const activeIdx = parseInt("{{ active_idx|default(0) }}");
const headers = document.querySelectorAll('.accordion-header'); const headers = document.querySelectorAll('.accordion-header');
@@ -563,6 +791,7 @@ footer br + a {
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
hideHelp(); hideHelp();
stopReading();
} }
}); });
@@ -583,14 +812,20 @@ footer br + a {
<body> <body>
<div class="container"> <div class="container">
<div class="help-button-container"> <div class="help-button-container">
<button type="button" class="help-button" onclick="showHelp()" aria-label="Hilfe anzeigen" title="Hilfe anzeigen" aria-describedby="help-tooltip">?</button> <button type="button" class="help-button" onclick="showHelp()" aria-label="{{ _('Hilfe anzeigen') }}" title="{{ _('Hilfe anzeigen') }}" aria-describedby="help-tooltip">?</button>
<div id="help-tooltip" class="help-tooltip" role="tooltip">Öffnet ein Hilfefenster mit Informationen über den Datumsrechner</div> <div id="help-tooltip" class="help-tooltip" role="tooltip">{{ _('Öffnet ein Hilfefenster mit Informationen über den Datumsrechner') }}</div>
</div> </div>
<div style="text-align:center; margin-bottom:1.2em;"> <div class="language-selector">
<div style="font-size:1.1em; font-style:italic; color:#64748b;">Elpatrons</div> <select id="language-dropdown" onchange="changeLanguage(this.value)" aria-label="{{ _('Sprache auswählen') }}" title="{{ _('Sprache auswählen') }}">
<h1 style="margin:0;">Datumsrechner</h1> <option value="de" {% if get_locale() == 'de' %}selected{% endif %}>{{ _('Deutsch') }}</option>
<div style="font-size:0.9em; color:#353535; margin-top:0.3em;"> <option value="en" {% if get_locale() == 'en' %}selected{% endif %}>{{ _('English') }}</option>
Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>. </select>
</div>
<div class="header-section" style="text-align:center; margin-bottom:1.2em;">
<div style="font-size:1.1em; font-style:italic; color:#475569;">{{ _('Elpatrons') }}</div>
<h1 style="margin:0;">{{ _('Datumsrechner') }}</h1>
<div style="font-size:0.9em; color:#1e293b; margin-top:0.3em;">
{{ _('Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>.') | safe }}
</div> </div>
</div> </div>
<div class="accordion"> <div class="accordion">
@@ -600,66 +835,67 @@ footer br + a {
<!-- Kalender mit Doppelpfeil --> <!-- Kalender mit Doppelpfeil -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M8 15h8M8 15l2-2M8 15l2 2M16 15l-2-2M16 15l-2 2" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M8 15h8M8 15l2-2M8 15l2 2M16 15l-2-2M16 15l-2 2" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/></svg>
</span> </span>
Anzahl der Tage/Werktage zwischen zwei Daten {{ _('Anzahl der Tage/Werktage zwischen zwei Daten') }}
</button> </button>
<div class="accordion-content active" id="accordion-panel-0" role="region" aria-labelledby="accordion-header-0"> <div class="accordion-content active" id="accordion-panel-0" role="region" aria-labelledby="accordion-header-0">
<form method="post"> <form method="post">
<label for="start1">Startdatum:<br> <label for="start1">{{ _('Startdatum:') }}<br>
<span class="date-row"> <span class="date-row">
<input type="date" name="start1" id="start1"> <input type="date" name="start1" id="start1">
<button type="button" class="today-btn" onclick="setToday('start1')">Heute</button> <button type="button" class="today-btn" onclick="setToday('start1')">{{ _('Heute') }}</button>
</span> </span>
</label> </label>
<label for="end1">Enddatum:<br> <label for="end1">{{ _('Enddatum:') }}<br>
<span class="date-row"> <span class="date-row">
<input type="date" name="end1" id="end1"> <input type="date" name="end1" id="end1">
<button type="button" class="today-btn" onclick="setToday('end1')">Heute</button> <button type="button" class="today-btn" onclick="setToday('end1')">{{ _('Heute') }}</button>
</span> </span>
</label> </label>
<fieldset style="display:flex;align-items:center;gap:0.5em;margin-top:0.7em; border:none; padding:0;"> <fieldset style="display:flex;align-items:center;gap:0.5em;margin-top:0.7em; border:none; padding:0;">
<legend class="sr-only">Optionen</legend> <legend class="sr-only">Optionen</legend>
<input type="checkbox" name="werktage" id="werktage" {% if request.form.get('werktage') %}checked{% endif %} aria-checked="{{ 'true' if request.form.get('werktage') else 'false' }}"> <input type="checkbox" name="werktage" id="werktage" {% if request.form.get('werktage') %}checked{% endif %} aria-checked="{{ 'true' if request.form.get('werktage') else 'false' }}">
<label for="werktage" style="margin:0;">Nur Werktage</label> <label for="werktage" style="margin:0;">{{ _('Nur Werktage') }}</label>
<label for="bundesland" style="margin-left:1em;">Feiertage berücksichtigen für: <label for="bundesland" style="margin-left:1em;">{{ _('Feiertage berücksichtigen für:') }}
<select name="bundesland" id="bundesland" {% if not request.form.get('werktage') %}disabled{% endif %}> <select name="bundesland" id="bundesland" {% if not request.form.get('werktage') %}disabled{% endif %}>
<option value="">(kein Bundesland)</option> <option value="">{{ _('(kein Bundesland)') }}</option>
<option value="BW" {% if request.form.get('bundesland') == 'BW' %}selected{% endif %}>Baden-Württemberg</option> <option value="BW" {% if request.form.get('bundesland') == 'BW' %}selected{% endif %}>{{ _('Baden-Württemberg') }}</option>
<option value="BY" {% if request.form.get('bundesland') == 'BY' %}selected{% endif %}>Bayern</option> <option value="BY" {% if request.form.get('bundesland') == 'BY' %}selected{% endif %}>{{ _('Bayern') }}</option>
<option value="BE" {% if request.form.get('bundesland') == 'BE' %}selected{% endif %}>Berlin</option> <option value="BE" {% if request.form.get('bundesland') == 'BE' %}selected{% endif %}>{{ _('Berlin') }}</option>
<option value="BB" {% if request.form.get('bundesland') == 'BB' %}selected{% endif %}>Brandenburg</option> <option value="BB" {% if request.form.get('bundesland') == 'BB' %}selected{% endif %}>{{ _('Brandenburg') }}</option>
<option value="HB" {% if request.form.get('bundesland') == 'HB' %}selected{% endif %}>Bremen</option> <option value="HB" {% if request.form.get('bundesland') == 'HB' %}selected{% endif %}>{{ _('Bremen') }}</option>
<option value="HH" {% if request.form.get('bundesland') == 'HH' %}selected{% endif %}>Hamburg</option> <option value="HH" {% if request.form.get('bundesland') == 'HH' %}selected{% endif %}>{{ _('Hamburg') }}</option>
<option value="HE" {% if request.form.get('bundesland') == 'HE' %}selected{% endif %}>Hessen</option> <option value="HE" {% if request.form.get('bundesland') == 'HE' %}selected{% endif %}>{{ _('Hessen') }}</option>
<option value="MV" {% if request.form.get('bundesland') == 'MV' %}selected{% endif %}>Mecklenburg-Vorpommern</option> <option value="MV" {% if request.form.get('bundesland') == 'MV' %}selected{% endif %}>{{ _('Mecklenburg-Vorpommern') }}</option>
<option value="NI" {% if request.form.get('bundesland') == 'NI' %}selected{% endif %}>Niedersachsen</option> <option value="NI" {% if request.form.get('bundesland') == 'NI' %}selected{% endif %}>{{ _('Niedersachsen') }}</option>
<option value="NW" {% if request.form.get('bundesland') == 'NW' %}selected{% endif %}>Nordrhein-Westfalen</option> <option value="NW" {% if request.form.get('bundesland') == 'NW' %}selected{% endif %}>{{ _('Nordrhein-Westfalen') }}</option>
<option value="RP" {% if request.form.get('bundesland') == 'RP' %}selected{% endif %}>Rheinland-Pfalz</option> <option value="RP" {% if request.form.get('bundesland') == 'RP' %}selected{% endif %}>{{ _('Rheinland-Pfalz') }}</option>
<option value="SL" {% if request.form.get('bundesland') == 'SL' %}selected{% endif %}>Saarland</option> <option value="SL" {% if request.form.get('bundesland') == 'SL' %}selected{% endif %}>{{ _('Saarland') }}</option>
<option value="SN" {% if request.form.get('bundesland') == 'SN' %}selected{% endif %}>Sachsen</option> <option value="SN" {% if request.form.get('bundesland') == 'SN' %}selected{% endif %}>{{ _('Sachsen') }}</option>
<option value="ST" {% if request.form.get('bundesland') == 'ST' %}selected{% endif %}>Sachsen-Anhalt</option> <option value="ST" {% if request.form.get('bundesland') == 'ST' %}selected{% endif %}>{{ _('Sachsen-Anhalt') }}</option>
<option value="SH" {% if request.form.get('bundesland') == 'SH' %}selected{% endif %}>Schleswig-Holstein</option> <option value="SH" {% if request.form.get('bundesland') == 'SH' %}selected{% endif %}>{{ _('Schleswig-Holstein') }}</option>
<option value="TH" {% if request.form.get('bundesland') == 'TH' %}selected{% endif %}>Thüringen</option> <option value="TH" {% if request.form.get('bundesland') == 'TH' %}selected{% endif %}>{{ _('Thüringen') }}</option>
</select> </select>
</label> </label>
</fieldset> </fieldset>
<button name="action" value="tage_werktage" type="submit">Berechnen</button> <button name="action" value="tage_werktage" type="submit">{{ _('Berechnen') }}</button>
</form> </form>
{% if tage is not none %} {% if tage is not none %}
<div class="result" aria-live="polite"> <div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
{% if request.form.get('werktage') %} {% if request.form.get('werktage') %}
Anzahl der Werktage zwischen <b>{{ format_date(request.form.get('start1', '')) }}</b> und <b>{{ format_date(request.form.get('end1', '')) }}</b>{% if request.form.get('bundesland') %} (Feiertage {{ request.form.get('bundesland') }}){% endif %}: {{ tage }} {{ _('Anzahl der Werktage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}:</b>{% if request.form.get('bundesland') %} {{ _('(Feiertage:') }} {{ request.form.get('bundesland') }}){% endif %}: {{ tage }}
{% else %} {% else %}
Anzahl der Tage zwischen <b>{{ format_date(request.form.get('start1', '')) }}</b> und <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }} {{ _('Anzahl der Tage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }}.
{% endif %} {% endif %}
{% if wochenendtage_anzahl is not none or feiertage_anzahl is not none %} {% if wochenendtage_anzahl is not none or (feiertage_anzahl is not none and request.form.get('bundesland')) %}
<br> <br>
<span style="font-size:0.98em; color:#1e293b;"> <span style="font-size:0.98em; color:#1e293b;">
{% if wochenendtage_anzahl is not none %} {% if wochenendtage_anzahl is not none %}
<b>{{ wochenendtage_anzahl }}</b> Wochenendtage <b>{{ _('Davon sind') }} {{ wochenendtage_anzahl }}</b> {{ _('Tage Wochenendtage.') }}
{% endif %} {% endif %}
{% if feiertage_anzahl is not none %} {% if feiertage_anzahl is not none and request.form.get('bundesland') %}
{% if wochenendtage_anzahl is not none %} | {% endif %} {% if wochenendtage_anzahl is not none %}, {% endif %}
<b>{{ feiertage_anzahl }}</b> Feiertage (Mo-Fr{% if request.form.get('bundesland') %}, {{ request.form.get('bundesland') }}{% endif %}) <b>{{ feiertage_anzahl }}</b> {{ _('Feiertage (Mo-Fr,') }} {{ request.form.get('bundesland') }})
{% endif %} {% endif %}
</span> </span>
{% endif %} {% endif %}
@@ -673,20 +909,23 @@ footer br + a {
<!-- Kalender mit W --> <!-- Kalender mit W -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="12" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">W</text></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="12" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">W</text></svg>
</span> </span>
Wochentag eines Datums {{ _('Wochentag eines Datums') }}
</button> </button>
<div class="accordion-content" id="accordion-panel-1" role="region" aria-labelledby="accordion-header-1"> <div class="accordion-content" id="accordion-panel-1" role="region" aria-labelledby="accordion-header-1">
<form method="post"> <form method="post">
<label for="datum3">Datum:<br> <label for="datum3">{{ _('Datum:') }}<br>
<span class="date-row"> <span class="date-row">
<input type="date" name="datum3" id="datum3"> <input type="date" name="datum3" id="datum3">
<button type="button" class="today-btn" onclick="setToday('datum3')">Heute</button> <button type="button" class="today-btn" onclick="setToday('datum3')">{{ _('Heute') }}</button>
</span> </span>
</label> </label>
<button name="action" value="wochentag" type="submit">Anzeigen</button> <button name="action" value="wochentag" type="submit">{{ _('Anzeigen') }}</button>
</form> </form>
{% if wochentag is not none %} {% if wochentag is not none %}
<div class="result" aria-live="polite">Wochentag von <b>{{ format_date(request.form.get('datum3', '')) }}</b>: {{ wochentag }}</div> <div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
{{ _('Wochentag von') }} <b>{{ format_date(request.form.get('datum3', '')) }}</b>: {{ wochentag }}
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -696,20 +935,23 @@ footer br + a {
<!-- Kalender mit # --> <!-- Kalender mit # -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="13" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">#</text></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="13" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">#</text></svg>
</span> </span>
Kalenderwoche eines Datums {{ _('Kalenderwoche eines Datums') }}
</button> </button>
<div class="accordion-content" id="accordion-panel-2" role="region" aria-labelledby="accordion-header-2"> <div class="accordion-content" id="accordion-panel-2" role="region" aria-labelledby="accordion-header-2">
<form method="post"> <form method="post">
<label for="datum6">Datum:<br> <label for="datum6">{{ _('Datum:') }}<br>
<span class="date-row"> <span class="date-row">
<input type="date" name="datum6" id="datum6"> <input type="date" name="datum6" id="datum6">
<button type="button" class="today-btn" onclick="setToday('datum6')">Heute</button> <button type="button" class="today-btn" onclick="setToday('datum6')">{{ _('Heute') }}</button>
</span> </span>
</label> </label>
<button name="action" value="kw_berechnen" type="submit">Kalenderwoche berechnen</button> <button name="action" value="kw_berechnen" type="submit">{{ _('Kalenderwoche berechnen') }}</button>
</form> </form>
{% if kw_berechnen is not none %} {% if kw_berechnen is not none %}
<div class="result" aria-live="polite">Kalenderwoche von <b>{{ format_date(request.form.get('datum6', '')) }}</b>: {{ kw_berechnen }}</div> <div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
{{ _('Kalenderwoche von') }} <b>{{ format_date(request.form.get('datum6', '')) }}</b>: {{ kw_berechnen }}
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -719,20 +961,23 @@ footer br + a {
<!-- Kalender mit Pfeil nach außen --> <!-- Kalender mit Pfeil nach außen -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M7 17l5-5 5 5" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><text x="12" y="12" text-anchor="middle" font-size="8" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">KW</text></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M7 17l5-5 5 5" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><text x="12" y="12" text-anchor="middle" font-size="8" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">KW</text></svg>
</span> </span>
Start-/Enddatum zu Kalenderwoche {{ _('Start-/Enddatum zu Kalenderwoche') }}
</button> </button>
<div class="accordion-content" id="accordion-panel-3" role="region" aria-labelledby="accordion-header-3"> <div class="accordion-content" id="accordion-panel-3" role="region" aria-labelledby="accordion-header-3">
<form method="post"> <form method="post">
<label for="jahr7">Jahr:<br> <label for="jahr7">{{ _('Jahr:') }}<br>
<input type="number" name="jahr7" id="jahr7" min="1900" max="2100" style="width: 7em;"> <input type="number" name="jahr7" id="jahr7" min="1900" max="2100" style="width: 7em;">
</label> </label>
<label for="kw7">Kalenderwoche:<br> <label for="kw7">{{ _('Kalenderwoche:') }}<br>
<input type="number" name="kw7" id="kw7" min="1" max="53" style="width: 5em;"> <input type="number" name="kw7" id="kw7" min="1" max="53" style="width: 5em;">
</label> </label>
<button name="action" value="kw_datum" type="submit">Start-/Enddatum berechnen</button> <button name="action" value="kw_datum" type="submit">{{ _('Start-/Enddatum berechnen') }}</button>
</form> </form>
{% if kw_datum is not none %} {% if kw_datum is not none %}
<div class="result" aria-live="polite">Start-/Enddatum der KW <b>{{ request.form.get('kw7', '') }}</b> im Jahr <b>{{ request.form.get('jahr7', '') }}</b>: {{ kw_datum }}</div> <div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
{{ _('Start-/Enddatum der KW') }} <b>{{ request.form.get('kw7', '') }}</b> {{ _('im Jahr') }} <b>{{ request.form.get('jahr7', '') }}</b>: {{ kw_datum }}
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -742,40 +987,43 @@ footer br + a {
<!-- Kalender mit ± --> <!-- Kalender mit ± -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="16" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">±</text></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><text x="12" y="17" text-anchor="middle" font-size="16" font-family="Segoe UI, Arial, sans-serif" fill="#2563eb" font-weight="bold">±</text></svg>
</span> </span>
Datum plus/minus X Tage/Wochen/Monate {{ _('Datum plus/minus X Tage/Wochen/Monate') }}
</button> </button>
<div class="accordion-content" id="accordion-panel-4" role="region" aria-labelledby="accordion-header-4"> <div class="accordion-content" id="accordion-panel-4" role="region" aria-labelledby="accordion-header-4">
<form method="post"> <form method="post">
<label for="datum_pm">Datum:<br> <label for="datum_pm">{{ _('Datum:') }}<br>
<span class="date-row"> <span class="date-row">
<input type="date" name="datum_pm" id="datum_pm"> <input type="date" name="datum_pm" id="datum_pm">
<button type="button" class="today-btn" onclick="setToday('datum_pm')">Heute</button> <button type="button" class="today-btn" onclick="setToday('datum_pm')">{{ _('Heute') }}</button>
</span> </span>
</label> </label>
<label for="anzahl_pm">Anzahl:<br> <label for="anzahl_pm">{{ _('Anzahl:') }}<br>
<input type="number" name="anzahl_pm" id="anzahl_pm" style="width: 6em;"> <input type="number" name="anzahl_pm" id="anzahl_pm" style="width: 6em;">
</label> </label>
<fieldset class="date-calc-row" style="border:none; padding:0;"> <fieldset class="date-calc-row" style="border:none; padding:0;">
<legend class="sr-only">Rechenrichtung</legend> <legend class="sr-only">{{ _('Rechenrichtung') }}</legend>
<label for="richtung_pm_add"><input type="radio" name="richtung_pm" id="richtung_pm_add" value="add" checked> addieren</label> <label for="richtung_pm_add"><input type="radio" name="richtung_pm" id="richtung_pm_add" value="add" checked> {{ _('addieren') }}</label>
<label for="richtung_pm_sub"><input type="radio" name="richtung_pm" id="richtung_pm_sub" value="sub"> subtrahieren</label> <label for="richtung_pm_sub"><input type="radio" name="richtung_pm" id="richtung_pm_sub" value="sub"> {{ _('subtrahieren') }}</label>
</fieldset> </fieldset>
<fieldset style="display:flex; align-items:center; gap:0.5em; margin-top:0.7em; border:none; padding:0;"> <fieldset style="display:flex; align-items:center; gap:0.5em; margin-top:0.7em; border:none; padding:0;">
<legend class="sr-only">Einheit und Werktage</legend> <legend class="sr-only">{{ _('Einheit und Werktage') }}</legend>
<label for="einheit_pm" style="margin:0;">Einheit: <label for="einheit_pm" style="margin:0;">{{ _('Einheit:') }}
<select name="einheit_pm" id="einheit_pm"> <select name="einheit_pm" id="einheit_pm">
<option value="tage">Tage</option> <option value="tage">{{ _('Tage') }}</option>
<option value="wochen">Wochen</option> <option value="wochen">{{ _('Wochen') }}</option>
<option value="monate">Monate</option> <option value="monate">{{ _('Monate') }}</option>
</select> </select>
</label> </label>
<input type="checkbox" name="werktage_pm" id="werktage_pm"> <input type="checkbox" name="werktage_pm" id="werktage_pm">
<label for="werktage_pm" style="margin:0;">Nur Werktage</label> <label for="werktage_pm" style="margin:0;">{{ _('Nur Werktage') }}</label>
</fieldset> </fieldset>
<button name="action" value="plusminus" type="submit">Berechnen</button> <button name="action" value="plusminus" type="submit">{{ _('Berechnen') }}</button>
</form> </form>
{% if plusminus_result is not none %} {% if plusminus_result is not none %}
<div class="result" aria-live="polite">{{ plusminus_result }}</div> <div class="result" aria-live="polite">
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
{{ plusminus_result }}
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -785,58 +1033,58 @@ footer br + a {
<!-- Help Modal Overlay --> <!-- Help Modal Overlay -->
<div id="helpModal" class="modal-overlay" role="dialog" aria-labelledby="help-title" aria-describedby="help-content"> <div id="helpModal" class="modal-overlay" role="dialog" aria-labelledby="help-title" aria-describedby="help-content">
<div class="modal-content"> <div class="modal-content">
<button type="button" class="modal-close" onclick="hideHelp()" aria-label="Hilfe schließen">&times;</button> <button type="button" class="modal-close" onclick="hideHelp()" aria-label="{{ _('Hilfe schließen') }}">&times;</button>
<h1 id="help-title">Was ist Elpatrons Datumsrechner?</h1> <h1 id="help-title">{{ _('Was ist Elpatrons Datumsrechner?') }}</h1>
<p>Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:</p> <p>{{ _('Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:') }}</p>
<ul> <ul>
<li>Anzahl der Tage zwischen zwei Daten</li> <li>{{ _('Anzahl der Tage zwischen zwei Daten') }}</li>
<li>Anzahl der Werktage zwischen zwei Daten</li> <li>{{ _('Anzahl der Werktage zwischen zwei Daten') }}</li>
<li>Anzeige des Wochentags eines Datums</li> <li>{{ _('Anzeige des Wochentags eines Datums') }}</li>
<li>Datum plus/minus X Tage</li> <li>{{ _('Datum plus/minus X Tage') }}</li>
<li>Datum plus/minus X Werktage</li> <li>{{ _('Datum plus/minus X Werktage') }}</li>
<li>Datum plus/minus X Wochen/Monate</li> <li>{{ _('Datum plus/minus X Wochen/Monate') }}</li>
<li>Kalenderwoche zu Datum</li> <li>{{ _('Kalenderwoche zu Datum') }}</li>
<li>Start-/Enddatum einer Kalenderwoche eines Jahres</li> <li>{{ _('Start-/Enddatum einer Kalenderwoche eines Jahres') }}</li>
</ul> </ul>
<h2>Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?</h2> <h2>{{ _('Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?') }}</h2>
<p>Aus zwei Gründen:</p> <p>{{ _('Aus zwei Gründen:') }}</p>
<ul> <ul>
<li>Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!</li> <li>{{ _('Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!') }}</li>
<li>Das hat mich so geärgert, dass ich meinen eigenen programmiert habe. <li>{{ _('Das hat mich so geärgert, dass ich meinen eigenen programmiert habe.') }}
<ul> <ul>
<li>Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding).</li> <li>{{ _('Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding).') }}</li>
</ul> </ul>
</li> </li>
</ul> </ul>
<h2>Was du noch wissen solltest</h2> <h2>{{ _('Was du noch wissen solltest') }}</h2>
<ul> <ul>
<li>Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern.</li> <li>{{ _('Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern.') }}</li>
<li>Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven.</li> <li>{{ _('Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven.') }}</li>
<li>Den Quellcode dieser App habe ich auf <a href="https://codeberg.org/elpatron/datecalc" target="_blank">Codeberg</a> veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben.</li> <li>{{ _('Den Quellcode dieser App habe ich auf') }} <a href="https://codeberg.org/elpatron/datecalc" target="_blank">Codeberg</a> {{ _('veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben.') }}</li>
<li>Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt.</li> <li>{{ _('Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt.') }}</li>
<li>Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht.</li> <li>{{ _('Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht.') }}</li>
<li>Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)</li> <li>{{ _('Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)') }}</li>
</ul> </ul>
<p><strong>Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche</strong></p> <p><strong>{{ _('Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche') }}</strong></p>
</div> </div>
<div id="help-content" class="sr-only"> <div id="help-content" class="sr-only">
Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen {{ _('Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen') }}
</div> </div>
</div> </div>
<footer style="text-align:center; margin-top:2em; color:#64748b; font-size:0.98em; padding-bottom:1.5em;"> <footer style="text-align:center; margin-top:2em; color:#475569; font-size:0.98em; padding-bottom:1.5em;">
Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc/src/branch/main/README.md" target="_blank" style="color:#2563eb; text-decoration:underline;">Open Source Datumsrechner</a><br> Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc/src/branch/main/README.md" target="_blank" style="color:#1e40af; text-decoration:underline;">Open Source Datumsrechner</a><br>
<a href="/api-docs" target="_blank" style="color:#2563eb; text-decoration:underline;">REST API Dokumentation (Swagger)</a><br> <a href="/api-docs" target="_blank" style="color:#1e40af; text-decoration:underline;">REST API Dokumentation (Swagger)</a><br>
© 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#2563eb; text-decoration:underline;">M. Busche</a> © 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#1e40af; text-decoration:underline;">M. Busche</a>
<div style="margin-top:0.5em; font-size:0.85em; color:#94a3b8;">v{{ app_version }}</div> <div style="margin-top:0.5em; font-size:0.85em; color:#64748b;">v{{ app_version }}</div>
</footer> </footer>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View File

@@ -19,6 +19,32 @@
.stats-label { color: #64748b; } .stats-label { color: #64748b; }
.stats-value { font-size: 1.5em; font-weight: bold; } .stats-value { font-size: 1.5em; font-weight: bold; }
.chart-container { margin: 2em 0; } .chart-container { margin: 2em 0; }
.toggle-container {
display: flex;
justify-content: center;
margin-bottom: 1.5em;
gap: 0.5em;
}
.toggle-btn {
padding: 0.5em 1em;
border: 1px solid #d1d5db;
background: #f9fafb;
color: #6b7280;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn.active {
background: #2563eb;
color: white;
border-color: #2563eb;
}
.toggle-btn:hover {
background: #e5e7eb;
}
.toggle-btn.active:hover {
background: #1d4ed8;
}
</style> </style>
</head> </head>
<body> <body>
@@ -28,6 +54,12 @@
<div class="stats-label">Gesamt-Pageviews (7 Tage):</div> <div class="stats-label">Gesamt-Pageviews (7 Tage):</div>
<div class="stats-value">{{ pageviews }}</div> <div class="stats-value">{{ pageviews }}</div>
</div> </div>
<div class="toggle-container">
<button class="toggle-btn active" data-period="week">Wochenverlauf</button>
<button class="toggle-btn" data-period="day">24-Stunden-Verlauf</button>
</div>
<div class="chart-container"> <div class="chart-container">
<canvas id="imprChart" width="400" height="180"></canvas> <canvas id="imprChart" width="400" height="180"></canvas>
</div> </div>
@@ -43,75 +75,196 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Impressions pro Tag // Daten für verschiedene Zeiträume
// eslint-disable-next-line const weekData = {{ impressions_per_day|tojson }};
const imprData = {{ impressions_per_day|tojson }}; const dayData = {{ impressions_per_hour|tojson }};
const imprLabels = Object.keys(imprData); const weekFuncData = {{ func_counts|tojson }};
const imprCounts = Object.values(imprData); const dayFuncData = {{ func_counts_hourly|tojson }};
new Chart(document.getElementById('imprChart').getContext('2d'), { const weekApiData = {{ api_counts|tojson }};
type: 'line', const dayApiData = {{ api_counts_hourly|tojson }};
data: {
labels: imprLabels, let currentPeriod = 'week';
datasets: [{ let currentImprChart = null;
label: 'Impressions/Tag', let currentFuncChart = null;
data: imprCounts, let currentApiChart = null;
borderColor: '#059669',
backgroundColor: 'rgba(5,150,105,0.1)', // Toggle-Buttons Event Listener
tension: 0.2, document.querySelectorAll('.toggle-btn').forEach(btn => {
fill: true btn.addEventListener('click', function() {
}] // Aktiven Button aktualisieren
}, document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
options: { this.classList.add('active');
plugins: { legend: { display: true } },
scales: { // Zeitraum wechseln
y: { beginAtZero: true, ticks: { stepSize: 1 } } currentPeriod = this.dataset.period;
} updateAllCharts();
} });
}); });
// Funktionsaufrufe
// eslint-disable-next-line function updateImpressionsChart() {
const funcCounts = {{ func_counts|tojson }}; const ctx = document.getElementById('imprChart').getContext('2d');
const labels = Object.keys(funcCounts);
const data = Object.values(funcCounts); // Bestehenden Chart zerstören
new Chart(document.getElementById('funcChart').getContext('2d'), { if (currentImprChart) {
type: 'bar', currentImprChart.destroy();
data: {
labels: labels,
datasets: [{
label: 'Funktionsaufrufe',
data: data,
backgroundColor: '#2563eb',
}]
},
options: {
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
} }
});
// API-Nutzung let data, labels, counts;
// eslint-disable-next-line
const apiCounts = {{ api_counts|tojson }}; if (currentPeriod === 'week') {
if (Object.keys(apiCounts).length > 0 && document.getElementById('apiChart')) { data = weekData;
new Chart(document.getElementById('apiChart').getContext('2d'), { labels = Object.keys(data);
type: 'bar', counts = Object.values(data);
} else {
data = dayData;
labels = Object.keys(data);
counts = Object.values(data);
}
currentImprChart = new Chart(ctx, {
type: 'line',
data: { data: {
labels: Object.keys(apiCounts), labels: labels,
datasets: [{ datasets: [{
label: 'API-Aufrufe nach Endpunkt', label: currentPeriod === 'week' ? 'Impressions/Tag' : 'Impressions/Stunde',
data: Object.values(apiCounts), data: counts,
backgroundColor: '#f59e42', borderColor: '#059669',
backgroundColor: 'rgba(5,150,105,0.1)',
tension: 0.2,
fill: true
}] }]
}, },
options: { options: {
plugins: { legend: { display: false } }, plugins: {
legend: { display: true },
title: {
display: true,
text: currentPeriod === 'week' ? 'Wochenverlauf' : '24-Stunden-Verlauf'
}
},
scales: { scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } } y: { beginAtZero: true, ticks: { stepSize: 1 } }
} }
} }
}); });
} }
function updateFunctionChart() {
const ctx = document.getElementById('funcChart').getContext('2d');
// Bestehenden Chart zerstören
if (currentFuncChart) {
currentFuncChart.destroy();
}
let data, labels, counts;
if (currentPeriod === 'week') {
data = weekFuncData;
labels = Object.keys(data);
counts = Object.values(data);
} else {
// Für stündliche Daten: Summe aller Stunden für jede Funktion
const aggregatedData = {};
Object.values(dayFuncData).forEach(hourData => {
Object.keys(hourData).forEach(func => {
aggregatedData[func] = (aggregatedData[func] || 0) + hourData[func];
});
});
data = aggregatedData;
labels = Object.keys(data);
counts = Object.values(data);
}
currentFuncChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Funktionsaufrufe',
data: counts,
backgroundColor: '#2563eb',
}]
},
options: {
plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'Funktionsaufrufe (Woche)' : 'Funktionsaufrufe (24h)'
}
},
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
});
}
function updateApiChart() {
const apiChartElement = document.getElementById('apiChart');
if (!apiChartElement) return;
const ctx = apiChartElement.getContext('2d');
// Bestehenden Chart zerstören
if (currentApiChart) {
currentApiChart.destroy();
}
let data, labels, counts;
if (currentPeriod === 'week') {
data = weekApiData;
} else {
// Für stündliche Daten: Summe aller Stunden für jede API
const aggregatedData = {};
Object.values(dayApiData).forEach(hourData => {
Object.keys(hourData).forEach(api => {
aggregatedData[api] = (aggregatedData[api] || 0) + hourData[api];
});
});
data = aggregatedData;
}
if (Object.keys(data).length === 0) return;
labels = Object.keys(data);
counts = Object.values(data);
currentApiChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'API-Aufrufe nach Endpunkt',
data: counts,
backgroundColor: '#f59e42',
}]
},
options: {
plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'API-Nutzung (Woche)' : 'API-Nutzung (24h)'
}
},
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
});
}
function updateAllCharts() {
updateImpressionsChart();
updateFunctionChart();
updateApiChart();
}
// Initial Charts erstellen
updateAllCharts();
}); });
</script> </script>
</body> </body>

View File

@@ -1,6 +1,7 @@
import os import os
import pytest import pytest
from app import app as flask_app from app import app as flask_app
from unittest.mock import patch, MagicMock
@pytest.fixture @pytest.fixture
def client(): def client():
@@ -220,8 +221,10 @@ def test_api_plusminus(client):
def test_api_stats(client): def test_api_stats(client):
resp = client.get('/api/stats') resp = client.get('/api/stats')
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.get_json() # Die Route gibt HTML zurück, nicht JSON
assert 'pageviews' in data and 'func_counts' in data and 'impressions_per_day' in data html = resp.data.decode('utf-8')
# Prüfe auf typische HTML-Elemente des Dashboards
assert 'Statistik-Dashboard' in html or 'Dashboard' in html
def test_api_monitor(client): def test_api_monitor(client):
resp = client.get('/api/monitor') resp = client.get('/api/monitor')
@@ -229,3 +232,142 @@ def test_api_monitor(client):
data = resp.get_json() data = resp.get_json()
assert data['status'] == 'ok' assert data['status'] == 'ok'
assert 'uptime_seconds' in data assert 'uptime_seconds' in data
# Neue Tests für bessere Coverage
def test_feiertage_api_error(client):
"""Test Fehlerbehandlung bei Feiertage-API"""
with patch('app.requests.get') as mock_get:
mock_get.side_effect = Exception("Network error")
resp = client.post('/', data={
'action': 'tage_werktage',
'start1': '2024-01-01',
'end1': '2024-01-10',
'bundesland': 'BY'
})
assert resp.status_code == 200
def test_logging_error_handling(client):
"""Test Logging-Fehlerbehandlung"""
# Test ohne Mock, da die App das Logging-Handling bereits hat
resp = client.get('/')
assert resp.status_code == 200
def test_invalid_date_handling(client):
"""Test ungültige Datumseingaben"""
# Ungültiges Datum bei tage_werktage
resp = client.post('/', data={
'action': 'tage_werktage',
'start1': 'invalid-date',
'end1': '2024-01-10'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_wochentag_input(client):
"""Test ungültige Eingaben bei Wochentag-Berechnung"""
resp = client.post('/', data={
'action': 'wochentag',
'datum3': 'invalid-date'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_kw_berechnen_input(client):
"""Test ungültige Eingaben bei KW-Berechnung"""
resp = client.post('/', data={
'action': 'kw_berechnen',
'datum6': 'invalid-date'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_kw_datum_input(client):
"""Test ungültige Eingaben bei KW-Datum"""
resp = client.post('/', data={
'action': 'kw_datum',
'jahr7': 'invalid',
'kw7': 'invalid'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_plusminus_input(client):
"""Test ungültige Eingaben bei Plusminus-Berechnung"""
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': 'invalid-date',
'anzahl_pm': 'invalid',
'einheit_pm': 'tage',
'richtung_pm': 'add'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_stats_login_success(client):
"""Test erfolgreiche Anmeldung im Stats-Bereich"""
with client.session_transaction() as sess:
sess['stats_auth'] = True
resp = client.get('/stats')
assert resp.status_code == 200
def test_stats_login_failure(client):
"""Test fehlgeschlagene Anmeldung im Stats-Bereich"""
resp = client.post('/stats', data={'password': 'wrong'})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Falsches Passwort' in html
def test_api_error_handling(client):
"""Test API-Fehlerbehandlung"""
# Test mit korrektem Content-Type
resp = client.post('/api/tage_werktage',
data='invalid json',
content_type='application/json')
assert resp.status_code == 400
def test_api_plusminus_werktage_unsupported(client):
"""Test nicht unterstützte Werktage + Wochen/Monate"""
# Werktage + Wochen
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'wochen', 'werktage': True
})
assert resp.status_code == 400
assert 'Nicht unterstützt' in resp.get_json()['error']
# Werktage + Monate
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'monate', 'werktage': True
})
assert resp.status_code == 400
assert 'Nicht unterstützt' in resp.get_json()['error']
def test_api_logging(client):
"""Test API-Logging"""
resp = client.post('/api/wochentag', json={'datum': '2024-06-10'})
assert resp.status_code == 200
# Prüfe ob Log-Datei existiert
log_path = os.path.join('log', 'pageviews.log')
assert os.path.exists(log_path)
def test_api_docs(client):
"""Test API-Dokumentation"""
resp = client.get('/api-docs')
assert resp.status_code == 200
def test_monitor_api_details(client):
"""Test detaillierte Monitor-API"""
resp = client.get('/api/monitor')
assert resp.status_code == 200
data = resp.get_json()
assert 'status' in data
assert 'message' in data
assert 'time' in data
assert 'uptime_seconds' in data
assert 'pageviews_last_7_days' in data

Binary file not shown.

View File

@@ -0,0 +1,483 @@
# German translations for Elpatrons Datumsrechner.
# Copyright (C) 2025 M. Busche
# This file is distributed under the same license as the Elpatrons Datumsrechner package.
msgid ""
msgstr ""
"Project-Id-Version: 1.3.13\n"
"Report-Msgid-Bugs-To: elpatron@mailbox.org\n"
"POT-Creation-Date: 2025-08-01 15:58+0100\n"
"PO-Revision-Date: 2025-08-01 15:58+0100\n"
"Last-Translator: M. Busche <elpatron@mailbox.org>\n"
"Language: de\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"
#: templates/index.html:12
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
#: templates/index.html:13
msgid "Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."
msgstr "Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."
#: templates/index.html:14
msgid "Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"
msgstr "Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"
#: templates/index.html:15
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
#: templates/index.html:16
msgid "Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."
msgstr "Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."
#: templates/index.html:17
msgid "website"
msgstr "website"
#: templates/index.html:18
msgid "https://codeberg.org/elpatron/datecalc"
msgstr "https://codeberg.org/elpatron/datecalc"
#: templates/index.html:19
msgid "/static/logo.svg"
msgstr "/static/logo.svg"
#: templates/index.html:20
msgid "width=device-width, initial-scale=1"
msgstr "width=device-width, initial-scale=1"
#: templates/index.html:21
msgid "true"
msgstr "true"
#: templates/index.html:22
msgid "yes"
msgstr "yes"
#: templates/index.html:23
msgid "default"
msgstr "default"
#: templates/index.html:24
msgid "telephone=no"
msgstr "telephone=no"
#: templates/index.html:25
msgid "IE=edge"
msgstr "IE=edge"
#: templates/index.html:26
msgid "Elpatrons Datumsrechner"
msgstr "Elpatrons Datumsrechner"
#: templates/index.html:27
msgid "#2563eb"
msgstr "#2563eb"
#: templates/index.html:28
msgid "/static/favicon.ico"
msgstr "/static/favicon.ico"
#: templates/index.html:29
msgid "Hilfe anzeigen"
msgstr "Hilfe anzeigen"
#: templates/index.html:30
msgid "Hilfe anzeigen"
msgstr "Hilfe anzeigen"
#: templates/index.html:31
msgid "Öffnet ein Hilfefenster mit Informationen über den Datumsrechner"
msgstr "Öffnet ein Hilfefenster mit Informationen über den Datumsrechner"
#: templates/index.html:32
msgid "Elpatrons"
msgstr "Elpatrons"
#: templates/index.html:33
msgid "Datumsrechner"
msgstr "Datumsrechner"
#: templates/index.html:34
msgid "Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>."
msgstr "Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>."
#: templates/index.html:35
msgid "Anzahl der Tage/Werktage zwischen zwei Daten"
msgstr "Anzahl der Tage/Werktage zwischen zwei Daten"
#: templates/index.html:36
msgid "Startdatum:"
msgstr "Startdatum:"
#: templates/index.html:37
msgid "Heute"
msgstr "Heute"
#: templates/index.html:38
msgid "Enddatum:"
msgstr "Enddatum:"
#: templates/index.html:39
msgid "Optionen"
msgstr "Optionen"
#: templates/index.html:40
msgid "Nur Werktage"
msgstr "Nur Werktage"
#: templates/index.html:41
msgid "Feiertage berücksichtigen für:"
msgstr "Feiertage berücksichtigen für:"
#: templates/index.html:42
msgid "(kein Bundesland)"
msgstr "(kein Bundesland)"
#: templates/index.html:43
msgid "Baden-Württemberg"
msgstr "Baden-Württemberg"
#: templates/index.html:44
msgid "Bayern"
msgstr "Bayern"
#: templates/index.html:45
msgid "Berlin"
msgstr "Berlin"
#: templates/index.html:46
msgid "Brandenburg"
msgstr "Brandenburg"
#: templates/index.html:47
msgid "Bremen"
msgstr "Bremen"
#: templates/index.html:48
msgid "Hamburg"
msgstr "Hamburg"
#: templates/index.html:49
msgid "Hessen"
msgstr "Hessen"
#: templates/index.html:50
msgid "Mecklenburg-Vorpommern"
msgstr "Mecklenburg-Vorpommern"
#: templates/index.html:51
msgid "Niedersachsen"
msgstr "Niedersachsen"
#: templates/index.html:52
msgid "Nordrhein-Westfalen"
msgstr "Nordrhein-Westfalen"
#: templates/index.html:53
msgid "Rheinland-Pfalz"
msgstr "Rheinland-Pfalz"
#: templates/index.html:54
msgid "Saarland"
msgstr "Saarland"
#: templates/index.html:55
msgid "Sachsen"
msgstr "Sachsen"
#: templates/index.html:56
msgid "Sachsen-Anhalt"
msgstr "Sachsen-Anhalt"
#: templates/index.html:57
msgid "Schleswig-Holstein"
msgstr "Schleswig-Holstein"
#: templates/index.html:58
msgid "Thüringen"
msgstr "Thüringen"
#: templates/index.html:59
msgid "Berechnen"
msgstr "Berechnen"
#: templates/index.html:60
msgid "Ergebnis vorlesen"
msgstr "Ergebnis vorlesen"
#: templates/index.html:61
msgid "Ergebnis vorlesen"
msgstr "Ergebnis vorlesen"
#: templates/index.html:62
msgid "Anzahl der Werktage zwischen"
msgstr "Anzahl der Werktage zwischen"
#: templates/index.html:63
msgid "und"
msgstr "und"
#: templates/index.html:64
msgid "(Feiertage:"
msgstr "(Feiertage:"
#: templates/index.html:65
msgid "Anzahl der Tage zwischen"
msgstr "Anzahl der Tage zwischen"
#: templates/index.html:66
msgid "Davon sind"
msgstr "Davon sind"
#: templates/index.html:67
msgid "Tage Wochenendtage."
msgstr "Tage Wochenendtage."
#: templates/index.html:68
msgid "Feiertage (Mo-Fr,"
msgstr "Feiertage (Mo-Fr,"
#: templates/index.html:69
msgid "Wochentag eines Datums"
msgstr "Wochentag eines Datums"
#: templates/index.html:70
msgid "Datum:"
msgstr "Datum:"
#: templates/index.html:71
msgid "Anzeigen"
msgstr "Anzeigen"
#: templates/index.html:72
msgid "Wochentag von"
msgstr "Wochentag von"
#: templates/index.html:73
msgid "Kalenderwoche eines Datums"
msgstr "Kalenderwoche eines Datums"
#: templates/index.html:74
msgid "Kalenderwoche berechnen"
msgstr "Kalenderwoche berechnen"
#: templates/index.html:75
msgid "Kalenderwoche von"
msgstr "Kalenderwoche von"
#: templates/index.html:76
msgid "Start-/Enddatum zu Kalenderwoche"
msgstr "Start-/Enddatum zu Kalenderwoche"
#: templates/index.html:77
msgid "Jahr:"
msgstr "Jahr:"
#: templates/index.html:78
msgid "Kalenderwoche:"
msgstr "Kalenderwoche:"
#: templates/index.html:79
msgid "Start-/Enddatum berechnen"
msgstr "Start-/Enddatum berechnen"
#: templates/index.html:80
msgid "Start-/Enddatum der KW"
msgstr "Start-/Enddatum der KW"
#: templates/index.html:81
msgid "im Jahr"
msgstr "im Jahr"
#: templates/index.html:82
msgid "Datum plus/minus X Tage/Wochen/Monate"
msgstr "Datum plus/minus X Tage/Wochen/Monate"
#: templates/index.html:83
msgid "Anzahl:"
msgstr "Anzahl:"
#: templates/index.html:84
msgid "Rechenrichtung"
msgstr "Rechenrichtung"
#: templates/index.html:85
msgid "addieren"
msgstr "addieren"
#: templates/index.html:86
msgid "subtrahieren"
msgstr "subtrahieren"
#: templates/index.html:87
msgid "Einheit und Werktage"
msgstr "Einheit und Werktage"
#: templates/index.html:88
msgid "Einheit:"
msgstr "Einheit:"
#: templates/index.html:89
msgid "Tage"
msgstr "Tage"
#: templates/index.html:90
msgid "Wochen"
msgstr "Wochen"
#: templates/index.html:91
msgid "Monate"
msgstr "Monate"
#: templates/index.html:92
msgid "Nur Werktage"
msgstr "Nur Werktage"
#: templates/index.html:93
msgid "Hilfe schließen"
msgstr "Hilfe schließen"
#: templates/index.html:94
msgid "Was ist Elpatrons Datumsrechner?"
msgstr "Was ist Elpatrons Datumsrechner?"
#: templates/index.html:95
msgid "Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:"
msgstr "Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:"
#: templates/index.html:96
msgid "Anzahl der Tage zwischen zwei Daten"
msgstr "Anzahl der Tage zwischen zwei Daten"
#: templates/index.html:97
msgid "Anzahl der Werktage zwischen zwei Daten"
msgstr "Anzahl der Werktage zwischen zwei Daten"
#: templates/index.html:98
msgid "Anzeige des Wochentags eines Datums"
msgstr "Anzeige des Wochentags eines Datums"
#: templates/index.html:99
msgid "Datum plus/minus X Tage"
msgstr "Datum plus/minus X Tage"
#: templates/index.html:100
msgid "Datum plus/minus X Werktage"
msgstr "Datum plus/minus X Werktage"
#: templates/index.html:101
msgid "Datum plus/minus X Wochen/Monate"
msgstr "Datum plus/minus X Wochen/Monate"
#: templates/index.html:102
msgid "Kalenderwoche zu Datum"
msgstr "Kalenderwoche zu Datum"
#: templates/index.html:103
msgid "Start-/Enddatum einer Kalenderwoche eines Jahres"
msgstr "Start-/Enddatum einer Kalenderwoche eines Jahres"
#: templates/index.html:104
msgid "Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?"
msgstr "Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?"
#: templates/index.html:105
msgid "Aus zwei Gründen:"
msgstr "Aus zwei Gründen:"
#: templates/index.html:106
msgid "Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!"
msgstr "Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!"
#: templates/index.html:107
msgid "Das hat mich so geärgert, dass ich meinen eigenen programmiert habe."
msgstr "Das hat mich so geärgert, dass ich meinen eigenen programmiert habe."
#: templates/index.html:108
msgid "Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding)."
msgstr "Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding)."
#: templates/index.html:109
msgid "Was du noch wissen solltest"
msgstr "Was du noch wissen solltest"
#: templates/index.html:110
msgid "Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern."
msgstr "Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern."
#: templates/index.html:111
msgid "Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven."
msgstr "Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven."
#: templates/index.html:112
msgid "Den Quellcode dieser App habe ich auf"
msgstr "Den Quellcode dieser App habe ich auf"
#: templates/index.html:113
msgid "veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben."
msgstr "veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben."
#: templates/index.html:114
msgid "Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt."
msgstr "Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt."
#: templates/index.html:115
msgid "Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht."
msgstr "Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht."
#: templates/index.html:116
msgid "Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)"
msgstr "Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)"
#: templates/index.html:117
msgid "Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche"
msgstr "Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche"
#: templates/index.html:118
msgid "Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen"
msgstr "Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen"
#: templates/index.html:119
msgid "Dies ist ein werbe- und trackingfreier"
msgstr "Dies ist ein werbe- und trackingfreier"
#: templates/index.html:120
msgid "Open Source Datumsrechner"
msgstr "Open Source Datumsrechner"
#: templates/index.html:121
msgid "REST API Dokumentation (Swagger)"
msgstr "REST API Dokumentation (Swagger)"
#: templates/index.html:122
msgid "M. Busche"
msgstr "M. Busche"
#: app.py:123
msgid "Ungültige Eingabe"
msgstr "Ungültige Eingabe"
#: app.py:124
msgid "Nicht unterstützt: Werktage + Wochen."
msgstr "Nicht unterstützt: Werktage + Wochen."
#: app.py:125
msgid "Nicht unterstützt: Werktage + Monate."
msgstr "Nicht unterstützt: Werktage + Monate."
#: templates/index.html:126
msgid "Sprache auswählen"
msgstr "Sprache auswählen"
#: templates/index.html:127
msgid "Deutsch auswählen"
msgstr "Deutsch auswählen"
#: templates/index.html:131
msgid "English auswählen"
msgstr "English auswählen"

Binary file not shown.

View File

@@ -0,0 +1,483 @@
# English translations for Elpatrons Datumsrechner.
# Copyright (C) 2025 M. Busche
# This file is distributed under the same license as the Elpatrons Datumsrechner package.
msgid ""
msgstr ""
"Project-Id-Version: 1.3.13\n"
"Report-Msgid-Bugs-To: elpatron@mailbox.org\n"
"POT-Creation-Date: 2025-08-01 15:58+0100\n"
"PO-Revision-Date: 2025-08-01 15:58+0100\n"
"Last-Translator: M. Busche <elpatron@mailbox.org>\n"
"Language: en\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"
#: templates/index.html:12
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Date Calculator Open Source Calendar and Date Calculations"
#: templates/index.html:13
msgid "Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."
msgstr "Elpatrons Date Calculator: Open Source web app for calendar and date calculations. Calculate days, workdays, weeks, months, calendar weeks, weekdays and more accessible, ad-free, tracking-free, free of charge."
#: templates/index.html:14
msgid "Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"
msgstr "Date, Calendar, Date Calculator, Workdays, Count Days, Calendar Week, Weekday, Open Source, Python, Flask, accessibility, accessible, free, ad-free, tracking-free, cookie-free"
#: templates/index.html:15
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Date Calculator Open Source Calendar and Date Calculations"
#: templates/index.html:16
msgid "Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."
msgstr "Open Source web app for calendar and date calculations. Ad-free, tracking-free, free of charge."
#: templates/index.html:17
msgid "website"
msgstr "website"
#: templates/index.html:18
msgid "https://codeberg.org/elpatron/datecalc"
msgstr "https://codeberg.org/elpatron/datecalc"
#: templates/index.html:19
msgid "/static/logo.svg"
msgstr "/static/logo.svg"
#: templates/index.html:20
msgid "width=device-width, initial-scale=1"
msgstr "width=device-width, initial-scale=1"
#: templates/index.html:21
msgid "true"
msgstr "true"
#: templates/index.html:22
msgid "yes"
msgstr "yes"
#: templates/index.html:23
msgid "default"
msgstr "default"
#: templates/index.html:24
msgid "telephone=no"
msgstr "telephone=no"
#: templates/index.html:25
msgid "IE=edge"
msgstr "IE=edge"
#: templates/index.html:26
msgid "Elpatrons Datumsrechner"
msgstr "Elpatrons Date Calculator"
#: templates/index.html:27
msgid "#2563eb"
msgstr "#2563eb"
#: templates/index.html:28
msgid "/static/favicon.ico"
msgstr "/static/favicon.ico"
#: templates/index.html:29
msgid "Hilfe anzeigen"
msgstr "Show Help"
#: templates/index.html:30
msgid "Hilfe anzeigen"
msgstr "Show Help"
#: templates/index.html:31
msgid "Öffnet ein Hilfefenster mit Informationen über den Datumsrechner"
msgstr "Opens a help window with information about the date calculator"
#: templates/index.html:32
msgid "Elpatrons"
msgstr "Elpatrons"
#: templates/index.html:33
msgid "Datumsrechner"
msgstr "Date Calculator"
#: templates/index.html:34
msgid "Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>."
msgstr "A <em>free</em> web app: <em>accessible</em>, <em>ad-free</em>, <em>tracking-free</em>, <em>license-free</em> and <em>cost-free</em>."
#: templates/index.html:35
msgid "Anzahl der Tage/Werktage zwischen zwei Daten"
msgstr "Number of days/workdays between two dates"
#: templates/index.html:36
msgid "Startdatum:"
msgstr "Start date:"
#: templates/index.html:37
msgid "Heute"
msgstr "Today"
#: templates/index.html:38
msgid "Enddatum:"
msgstr "End date:"
#: templates/index.html:39
msgid "Optionen"
msgstr "Options"
#: templates/index.html:40
msgid "Nur Werktage"
msgstr "Workdays only"
#: templates/index.html:41
msgid "Feiertage berücksichtigen für:"
msgstr "Consider holidays for:"
#: templates/index.html:42
msgid "(kein Bundesland)"
msgstr "(no state)"
#: templates/index.html:43
msgid "Baden-Württemberg"
msgstr "Baden-Württemberg"
#: templates/index.html:44
msgid "Bayern"
msgstr "Bavaria"
#: templates/index.html:45
msgid "Berlin"
msgstr "Berlin"
#: templates/index.html:46
msgid "Brandenburg"
msgstr "Brandenburg"
#: templates/index.html:47
msgid "Bremen"
msgstr "Bremen"
#: templates/index.html:48
msgid "Hamburg"
msgstr "Hamburg"
#: templates/index.html:49
msgid "Hessen"
msgstr "Hesse"
#: templates/index.html:50
msgid "Mecklenburg-Vorpommern"
msgstr "Mecklenburg-Vorpommern"
#: templates/index.html:51
msgid "Niedersachsen"
msgstr "Lower Saxony"
#: templates/index.html:52
msgid "Nordrhein-Westfalen"
msgstr "North Rhine-Westphalia"
#: templates/index.html:53
msgid "Rheinland-Pfalz"
msgstr "Rhineland-Palatinate"
#: templates/index.html:54
msgid "Saarland"
msgstr "Saarland"
#: templates/index.html:55
msgid "Sachsen"
msgstr "Saxony"
#: templates/index.html:56
msgid "Sachsen-Anhalt"
msgstr "Saxony-Anhalt"
#: templates/index.html:57
msgid "Schleswig-Holstein"
msgstr "Schleswig-Holstein"
#: templates/index.html:58
msgid "Thüringen"
msgstr "Thuringia"
#: templates/index.html:59
msgid "Berechnen"
msgstr "Calculate"
#: templates/index.html:60
msgid "Ergebnis vorlesen"
msgstr "Read result aloud"
#: templates/index.html:61
msgid "Ergebnis vorlesen"
msgstr "Read result aloud"
#: templates/index.html:62
msgid "Anzahl der Werktage zwischen"
msgstr "Number of workdays between"
#: templates/index.html:63
msgid "und"
msgstr "and"
#: templates/index.html:64
msgid "(Feiertage:"
msgstr "(Holidays:"
#: templates/index.html:65
msgid "Anzahl der Tage zwischen"
msgstr "Number of days between"
#: templates/index.html:66
msgid "Davon sind"
msgstr "Of which"
#: templates/index.html:67
msgid "Tage Wochenendtage."
msgstr "days are weekend days."
#: templates/index.html:68
msgid "Feiertage (Mo-Fr,"
msgstr "holidays (Mon-Fri,"
#: templates/index.html:69
msgid "Wochentag eines Datums"
msgstr "Weekday of a date"
#: templates/index.html:70
msgid "Datum:"
msgstr "Date:"
#: templates/index.html:71
msgid "Anzeigen"
msgstr "Show"
#: templates/index.html:72
msgid "Wochentag von"
msgstr "Weekday of"
#: templates/index.html:73
msgid "Kalenderwoche eines Datums"
msgstr "Calendar week of a date"
#: templates/index.html:74
msgid "Kalenderwoche berechnen"
msgstr "Calculate calendar week"
#: templates/index.html:75
msgid "Kalenderwoche von"
msgstr "Calendar week of"
#: templates/index.html:76
msgid "Start-/Enddatum zu Kalenderwoche"
msgstr "Start/end date to calendar week"
#: templates/index.html:77
msgid "Jahr:"
msgstr "Year:"
#: templates/index.html:78
msgid "Kalenderwoche:"
msgstr "Calendar week:"
#: templates/index.html:79
msgid "Start-/Enddatum berechnen"
msgstr "Calculate start/end date"
#: templates/index.html:80
msgid "Start-/Enddatum der KW"
msgstr "Start/end date of week"
#: templates/index.html:81
msgid "im Jahr"
msgstr "in year"
#: templates/index.html:82
msgid "Datum plus/minus X Tage/Wochen/Monate"
msgstr "Date plus/minus X days/weeks/months"
#: templates/index.html:83
msgid "Anzahl:"
msgstr "Amount:"
#: templates/index.html:84
msgid "Rechenrichtung"
msgstr "Calculation direction"
#: templates/index.html:85
msgid "addieren"
msgstr "add"
#: templates/index.html:86
msgid "subtrahieren"
msgstr "subtract"
#: templates/index.html:87
msgid "Einheit und Werktage"
msgstr "Unit and workdays"
#: templates/index.html:88
msgid "Einheit:"
msgstr "Unit:"
#: templates/index.html:89
msgid "Tage"
msgstr "Days"
#: templates/index.html:90
msgid "Wochen"
msgstr "Weeks"
#: templates/index.html:91
msgid "Monate"
msgstr "Months"
#: templates/index.html:92
msgid "Nur Werktage"
msgstr "Workdays only"
#: templates/index.html:93
msgid "Hilfe schließen"
msgstr "Close help"
#: templates/index.html:94
msgid "Was ist Elpatrons Datumsrechner?"
msgstr "What is Elpatrons Date Calculator?"
#: templates/index.html:95
msgid "Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:"
msgstr "The date calculator can perform various date calculations:"
#: templates/index.html:96
msgid "Anzahl der Tage zwischen zwei Daten"
msgstr "Number of days between two dates"
#: templates/index.html:97
msgid "Anzahl der Werktage zwischen zwei Daten"
msgstr "Number of workdays between two dates"
#: templates/index.html:98
msgid "Anzeige des Wochentags eines Datums"
msgstr "Display of the weekday of a date"
#: templates/index.html:99
msgid "Datum plus/minus X Tage"
msgstr "Date plus/minus X days"
#: templates/index.html:100
msgid "Datum plus/minus X Werktage"
msgstr "Date plus/minus X workdays"
#: templates/index.html:101
msgid "Datum plus/minus X Wochen/Monate"
msgstr "Date plus/minus X weeks/months"
#: templates/index.html:102
msgid "Kalenderwoche zu Datum"
msgstr "Calendar week to date"
#: templates/index.html:103
msgid "Start-/Enddatum einer Kalenderwoche eines Jahres"
msgstr "Start/end date of a calendar week of a year"
#: templates/index.html:104
msgid "Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?"
msgstr "Online date calculators already exist in abundance, so why another one?"
#: templates/index.html:105
msgid "Aus zwei Gründen:"
msgstr "For two reasons:"
#: templates/index.html:106
msgid "Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!"
msgstr "Try to find a date calculator that isn't completely infested with ads, tracking and cookies!"
#: templates/index.html:107
msgid "Das hat mich so geärgert, dass ich meinen eigenen programmiert habe."
msgstr "This annoyed me so much that I programmed my own."
#: templates/index.html:108
msgid "Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding)."
msgstr "Actually not me myself. This app was developed largely by AI following my instructions (Vibe Coding)."
#: templates/index.html:109
msgid "Was du noch wissen solltest"
msgstr "What else you should know"
#: templates/index.html:110
msgid "Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern."
msgstr "I have tried to make the app as accessible as possible to make it easier for people with disabilities to use."
#: templates/index.html:111
msgid "Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven."
msgstr "This app doesn't spy on you, doesn't collect personal data and doesn't get on your nerves in any other way (hopefully!)."
#: templates/index.html:112
msgid "Den Quellcode dieser App habe ich auf"
msgstr "I have published the source code of this app on"
#: templates/index.html:113
msgid "veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben."
msgstr "you can view it, modify it or use it to run your own small date calculator."
#: templates/index.html:114
msgid "Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt."
msgstr "The app runs on my small home server and is currently not designed for large visitor numbers."
#: templates/index.html:115
msgid "Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht."
msgstr "I do not guarantee the functionality and calculation results. By the way, neither does the AI that programmed this."
#: templates/index.html:116
msgid "Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)"
msgstr "If you find a bug or want an additional feature, you can write me an email (see mailto link in the footer)"
#: templates/index.html:117
msgid "Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche"
msgstr "Have fun with Elpatrons Date Calculator! Yours, M. Busche"
#: templates/index.html:118
msgid "Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen"
msgstr "Help information for the date calculator with explanations of all functions"
#: templates/index.html:119
msgid "Dies ist ein werbe- und trackingfreier"
msgstr "This is an ad- and tracking-free"
#: templates/index.html:120
msgid "Open Source Datumsrechner"
msgstr "Open Source Date Calculator"
#: templates/index.html:121
msgid "REST API Dokumentation (Swagger)"
msgstr "REST API Documentation (Swagger)"
#: templates/index.html:122
msgid "M. Busche"
msgstr "M. Busche"
#: app.py:123
msgid "Ungültige Eingabe"
msgstr "Invalid input"
#: app.py:124
msgid "Nicht unterstützt: Werktage + Wochen."
msgstr "Not supported: Workdays + weeks."
#: app.py:125
msgid "Nicht unterstützt: Werktage + Monate."
msgstr "Not supported: Workdays + months."
#: templates/index.html:126
msgid "Sprache auswählen"
msgstr "Select language"
#: templates/index.html:127
msgid "Deutsch auswählen"
msgstr "Select German"
#: templates/index.html:131
msgid "English auswählen"
msgstr "Select English"