39 Commits

Author SHA1 Message Date
cabe628875 Bump version to 1.4.15 2025-08-03 13:32:43 +02:00
35ecba348b Fix stats route UnboundLocalError and bump version to 1.4.14 2025-08-03 12:59:41 +02:00
31b1c12dcb Code cleanup and dependency updates
- Remove unused imports (abort, g, ngettext) from app.py
- Remove unused variables (werktage, datumsrechnung, werktagsrechnung, wochen_monate)
- Update Flask from 3.0.0 to 3.1.1
- Update requests from 2.31.0 to 2.32.4
- Update pytest from 7.4.3 to 8.4.1
- Update numpy from 1.26.4 to 2.3.2 (safe migration based on NumPy 2.0 guide)
- Add pytest to requirements.txt (was missing)
2025-08-03 12:56:09 +02:00
95ed606796 Update README (manually) 2025-08-03 12:39:26 +02:00
52eac7530a Fix calculator button font (manually) 2025-08-03 12:28:46 +02:00
2f6138b1d6 Bump version to 1.4.13 2025-08-03 12:06:24 +02:00
f5a39e80b4 Taschenrechner-Features hinzugefügt und README aktualisiert 2025-08-03 12:05:02 +02:00
f9f73e24c9 Fix Back-Forward-Cache Lighthouse error by adding proper Cache-Control headers 2025-08-03 10:18:00 +02:00
d697928241 Bump version to 1.4.10 2025-08-03 10:04:09 +02:00
f998f7fff8 Fix JSON syntax error in swagger.json - remove invalid .v{ prefix 2025-08-03 10:01:37 +02:00
1a5aa003a2 v1.4.9: Verbesserte Sprachausgabe mit englischer Unterstützung und Service Worker Fix 2025-08-02 19:54:52 +02:00
eecc2b8b73 Sprachausgabe/englisch gefixt 2025-08-02 19:52:31 +02:00
0b13a408cd Update meta keywords 2025-08-02 19:08:48 +02:00
c4a65bba48 Version 2025-08-02 18:39:30 +02:00
e4b37d9261 Fehler in der API behoben 2025-08-02 18:37:17 +02:00
45cc02b4b0 Fehler in API behoben 2025-08-02 18:30:16 +02:00
05766d9a97 Version auf 1.4.7 erhöht - Dashboard mit Toggle-Funktionalität 2025-08-02 14:28:22 +02:00
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
20 changed files with 20103 additions and 578 deletions

View File

@@ -1,6 +1,10 @@
# Elpatrons Datumsrechner # Elpatrons Datumsrechner
[![Test Coverage](https://img.shields.io/badge/test%20coverage-90%25-brightgreen)](https://github.com/elpatron/datecalc) [![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. Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechnungen über eine übersichtliche, barrierefreie Weboberfläche.
@@ -40,7 +44,11 @@ Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
![App Screenshot](./assets/image-20250725095959116.png) ![App Screenshot](./assets/image-20250725095959116.png)
**Lighthouse-Performance-Score:** [Lighthouse-Ergebnis anzeigen](./lighthouse-score.pdf) - Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (Performance, Accessibility, Best Practices, SEO). **[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
@@ -52,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
- Integrierter Taschenrechner mit History und Sprachausgabe
- Mehrsprachige Unterstützung (Deutsch/Englisch) mit automatischer Browser-Spracherkennung
- Sprachausgabe für alle Ergebnisse (barrierefrei) - Sprachausgabe für alle Ergebnisse (barrierefrei)
- Statistik-Dashboard mit Passwortschutz unter `/stats` - Statistik-Dashboard mit Passwortschutz unter `/stats`
@@ -60,6 +70,7 @@ Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
Die Werktagsberechnung kann optional bundeslandspezifische Feiertage berücksichtigen. Dazu wird die kostenlose API von [feiertage-api.de](https://feiertage-api.de) verwendet. Die Werktagsberechnung kann optional bundeslandspezifische Feiertage berücksichtigen. Dazu wird die kostenlose API von [feiertage-api.de](https://feiertage-api.de) verwendet.
**Verfügbare Bundesländer:** **Verfügbare Bundesländer:**
- Baden-Württemberg (BW) - Baden-Württemberg (BW)
- Bayern (BY) - Bayern (BY)
- Berlin (BE) - Berlin (BE)
@@ -79,6 +90,39 @@ 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
- *Taschenrechner*: Vollständig barrierefrei mit Tastatur-Bedienung und Sprachausgabe
### *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
@@ -163,8 +207,11 @@ docker-compose up --build
## REST API ## REST API
Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptiert und liefert JSON. Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptiert und liefert JSON.
**Basis-URL:** `http://localhost:5000/api/` **Basis-URL:** `http://localhost:5000/api/`
**Swagger Dokumentation:** [https://date.elpatron.me/api-docs](https://date.elpatron.me/api-docs)
**Hinweis:** Die Nutzung der REST API wird im Statistik-Dashboard ausgewertet und als Diagramm angezeigt. **Hinweis:** Die Nutzung der REST API wird im Statistik-Dashboard ausgewertet und als Diagramm angezeigt.
### Endpunkte und Beispiele ### Endpunkte und Beispiele
@@ -297,6 +344,7 @@ curl -X POST http://localhost:5000/api/plusminus \
``` ```
**Hinweis:** **Hinweis:**
- `"einheit"`: `"tage"`, `"wochen"` oder `"monate"` - `"einheit"`: `"tage"`, `"wochen"` oder `"monate"`
- `"richtung"`: `"add"` (plus) oder `"sub"` (minus) - `"richtung"`: `"add"` (plus) oder `"sub"` (minus)
- `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt) - `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt)
@@ -358,6 +406,7 @@ Elpatrons Datumsrechner ist als PWA installierbar (z.B. auf Android/iOS-Homescre
- Manifest und Service Worker sind integriert - Manifest und Service Worker sind integriert
- App-Icon und Theme-Color für Homescreen - App-Icon und Theme-Color für Homescreen
- Installation über Browser-Menü ("Zum Startbildschirm hinzufügen") - Installation über Browser-Menü ("Zum Startbildschirm hinzufügen")
- Taschenrechner funktioniert vollständig clientseitig (offline verfügbar)
## Monitoring & Healthcheck ## Monitoring & Healthcheck
@@ -370,9 +419,7 @@ Die App bietet einen Monitoring-Endpunkt unter `/monitor`, der Statusinformation
Beispiel-Aufruf: Beispiel-Aufruf:
``` `GET https://date.elpatron.me/monitor`
GET https://date.elpatron.me/monitor
```
Antwort: Antwort:
@@ -411,7 +458,7 @@ Finde mal eine Datumsrechner- Webapp, die nicht völlig Werbe- und Tracking vers
### Vibe Coding ### Vibe Coding
Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (*[Vibe Coding](https://de.wikipedia.org/wiki/Vibe_Coding)*) erstellt. Das Grundgerüst war nach ca. 45 Minuten fertig gestellt, insgesamt hat die Entwicklung des Projekts ca. 4 Stunden Zeit beansprucht. Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (*[Vibe Coding](https://de.wikipedia.org/wiki/Vibe_Coding)*) erstellt. Das Grundgerüst war nach ca. 45 Minuten fertig gestellt, insgesamt hat die Entwicklung des Projekts ca. 12 Stunden Zeit beansprucht.
### Statistik-Erfassung, Logging ### Statistik-Erfassung, Logging
@@ -428,6 +475,7 @@ Es werden keine IP-Adressen oder sonstigen persönlichen Daten gespeichert, ledi
- *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). - *Sprachausgabe:* Alle Ergebnisse können über 🔊-Buttons vorgelesen werden (Web Speech API, deutsche Sprache).
- *Taschenrechner:* Vollständig barrierefrei mit Tastatur-Bedienung, Sprachausgabe und History-Funktion.
- *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.
@@ -435,22 +483,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.16 s (137.5 files/s, 29033.4 lines/s) cloc|github.com/AlDanial/cloc v 2.06 T=0.23 s (109.5 files/s, 30735.0 lines/s)
--- | --- --- | ---
Language|files|blank|comment|code Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------: :-------|-------:|-------:|-------:|-------:
HTML|8|36|6|1998 HTML|8|159|8|2805
Python|2|53|57|614 Python|2|66|74|739
JavaScript|2|95|87|571 JavaScript|2|95|88|580
Markdown|2|123|0|353 PO File|2|260|266|544
Markdown|3|184|0|498
JSON|3|0|0|243 JSON|3|0|0|243
CSS|1|186|3|188 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:|22|498|159|3990 SUM:|25|955|445|5620
## Lizenz ## Lizenz
@@ -458,3 +507,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.12** - Integrierter Taschenrechner mit History und Sprachausgabe hinzugefügt

223
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, jsonify, make_response
from flask_babel import Babel, gettext, 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,56 @@ 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.13" APP_VERSION = "1.4.15"
def add_cache_headers(response):
"""Fügt Cache-Control-Header hinzu, die den Back-Forward-Cache ermöglichen"""
# Cache-Control für statische Inhalte und API-Endpunkte
if request.path.startswith('/static/') or request.path.startswith('/api/'):
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=3600'
else:
# Für HTML-Seiten: kurze Cache-Zeit, aber Back-Forward-Cache erlauben
response.headers['Cache-Control'] = 'public, max-age=60, s-maxage=300'
# Wichtig: Keine Vary-Header für User-Agent oder andere dynamische Werte
# Dies verhindert den Back-Forward-Cache
if 'Vary' in response.headers:
del response.headers['Vary']
return response
# 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 +75,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
@@ -55,7 +118,7 @@ def index():
with open(log_path, 'a', encoding='utf-8') as f: with open(log_path, 'a', encoding='utf-8') as f:
from datetime import datetime as dt from datetime import datetime as dt
f.write(f"{dt.now().isoformat()} PAGEVIEW\n") f.write(f"{dt.now().isoformat()} PAGEVIEW\n")
tage = werktage = wochentag = datumsrechnung = werktagsrechnung = kw_berechnen = kw_datum = wochen_monate = None tage = wochentag = kw_berechnen = kw_datum = None
feiertage_anzahl = wochenendtage_anzahl = None feiertage_anzahl = wochenendtage_anzahl = None
active_idx = 0 active_idx = 0
plusminus_result = None plusminus_result = None
@@ -94,24 +157,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
locale = get_locale()
if locale == 'en':
kw_berechnen = f"Week {kw} ({d.year})"
else:
kw_berechnen = f"KW {kw} ({d.year})" 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 +190,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)
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')}" 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 +209,108 @@ 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')
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')}" 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)
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')}" 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)
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')}" 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)
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')}" 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 response = make_response(render_template('index.html', tage=tage, 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
) ))
return add_cache_headers(response)
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():
@@ -198,11 +321,17 @@ def stats():
session['stats_auth'] = True session['stats_auth'] = True
return redirect(url_for('stats')) return redirect(url_for('stats'))
else: else:
return render_template('stats_login.html', error='Falsches Passwort!') response = make_response(render_template('stats_login.html', error='Falsches Passwort!'))
return render_template('stats_login.html', error=None) return add_cache_headers(response)
else:
response = make_response(render_template('stats_login.html', error=None))
return add_cache_headers(response)
# Wenn authentifiziert, zeige Dashboard
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) response = make_response(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))
return add_cache_headers(response)
# --- REST API --- # --- REST API ---
def log_api_usage(api_name): def log_api_usage(api_name):
@@ -236,7 +365,8 @@ def api_tage_werktage():
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays)) tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays))
else: else:
tage = abs((d2 - d1).days) tage = abs((d2 - d1).days)
return jsonify({'result': tage}) response = jsonify({'result': tage})
return add_cache_headers(response)
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
@@ -247,8 +377,10 @@ 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()
return jsonify({'result': wochentag}) wochentag = wochentage[d.weekday()]
response = jsonify({'result': wochentag})
return add_cache_headers(response)
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 +392,13 @@ 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})"
response = jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
return add_cache_headers(response)
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 +413,13 @@ 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')}"
response = jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
return add_cache_headers(response)
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 +437,31 @@ 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')}) response = jsonify({'result': result_dt.strftime('%Y-%m-%d')})
return add_cache_headers(response)
else: else:
result = d + timedelta(days=anzahl_int) result = d + timedelta(days=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')}) response = jsonify({'result': result.strftime('%Y-%m-%d')})
return add_cache_headers(response)
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')}) response = jsonify({'result': result.strftime('%Y-%m-%d')})
return add_cache_headers(response)
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')}) response = jsonify({'result': result.strftime('%Y-%m-%d')})
return add_cache_headers(response)
else: else:
return jsonify({'error': 'Ungültige Einheit'}), 400 return jsonify({'error': 'Ungültige Einheit'}), 400
except Exception as e: except Exception as e:
@@ -321,8 +470,14 @@ def api_plusminus():
@app.route('/api/stats', methods=['GET']) @app.route('/api/stats', methods=['GET'])
def api_stats(): def api_stats():
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) response = jsonify({
"pageviews": pageviews,
"func_counts": func_counts,
"impressions_per_day": impressions_per_day,
"api_counts": api_counts
})
return add_cache_headers(response)
@app.route('/api/monitor', methods=['GET']) @app.route('/api/monitor', methods=['GET'])
def api_monitor(): def api_monitor():
@@ -334,17 +489,19 @@ def api_monitor():
if 'PAGEVIEW' in line: if 'PAGEVIEW' in line:
pageviews += 1 pageviews += 1
uptime = int(time.time() - app_start_time) uptime = int(time.time() - app_start_time)
return jsonify({ response = jsonify({
"status": "ok", "status": "ok",
"message": "App running", "message": "App running",
"time": datetime.now().isoformat(), "time": datetime.now().isoformat(),
"uptime_seconds": uptime, "uptime_seconds": uptime,
"pageviews_last_7_days": pageviews "pageviews_last_7_days": pageviews
}) })
return add_cache_headers(response)
@app.route('/api-docs') @app.route('/api-docs')
def api_docs(): def api_docs():
return render_template('swagger.html') response = make_response(render_template('swagger.html'))
return add_cache_headers(response)
if __name__ == '__main__': if __name__ == '__main__':

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

View File

@@ -1,324 +0,0 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
ESC-j * Forward one file line (or _N file lines).
ESC-k * Backward one file line (or _N file lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
ESC-b * Backward one window, but don't stop at beginning-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
^O^N ^On * Search forward for (_N-th) OSC8 hyperlink.
^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink.
^O^L ^Ol Jump to the currently selected OSC8 hyperlink.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
Search is case-sensitive unless changed with -i or -I.
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^S _n Search for match in _n-th parenthesized subpattern.
^W WRAP search if no match found.
^L Enter next character literally into pattern.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-m_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
^O^O Open the currently selected OSC8 hyperlink.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
#_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k _f_i_l_e ... --lesskey-file=_f_i_l_e
Use a compiled lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n ......... --line-numbers
Suppress line numbers in prompts and messages.
-N ......... --LINE-NUMBERS
Display line number at start of each line.
-o [_f_i_l_e] .. --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t _t_a_g .... --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces, tabs and carriage returns.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--exit-follow-on-close
Exit F command on a pipe when writer closes pipe.
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--form-feed
Stop scrolling when a form feed character is reached.
--header=[_L[,_C[,_N]]]
Use _L lines (starting at line _N) and _C columns as headers.
--incsearch
Search file as each pattern character is typed in.
--intr=[_C]
Use _C instead of ^X to interrupt a read.
--lesskey-context=_t_e_x_t
Use lesskey source file contents.
--lesskey-src=_f_i_l_e
Use a lesskey source file.
--line-num-width=[_N]
Set the width of the -N line number field to _N characters.
--match-shift=[_N]
Show at least _N characters to the left of a search match.
--modelines=[_N]
Read _N lines from the input file and look for vim modelines.
--mouse
Enable mouse input.
--no-edit-warn
Don't warn when using v command on a file opened via LESSOPEN.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--no-number-headers
Don't give line numbers to header lines.
--no-paste
Ignore pasted input.
--no-search-header-lines
Searches do not include header lines.
--no-search-header-columns
Searches do not include header columns.
--no-search-headers
Searches do not include header lines or columns.
--no-vbell
Disable the terminal's visual bell.
--redraw-on-quit
Redraw final screen when quitting.
--rscroll=[_C]
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--search-options=[EFKNRW-]
Set default options for every search.
--show-preproc-errors
Display a message if preprocessor exits with an error status.
--proc-backspace
Process backspaces for bold/underline.
--PROC-BACKSPACE
Treat backspaces as control characters.
--proc-return
Delete carriage returns before newline.
--PROC-RETURN
Treat carriage returns as control characters.
--proc-tab
Expand tabs to spaces.
--PROC-TAB
Treat tabs as control characters.
--status-col-width=[_N]
Set the width of the -J status column to _N characters.
--status-line
Highlight or color the entire line containing a mark.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=[_N]
Each click of the mouse wheel moves _N lines.
--wordwrap
Wrap lines at spaces.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
Flask==3.0.3 Flask==3.1.1
numpy==1.26.4 numpy==2.3.2
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytest==8.2.2 requests==2.32.4
requests Flask-Babel==4.0.0
pytest==8.4.1

View File

@@ -1,15 +1,25 @@
const CACHE_NAME = 'datumsrechner-cache-v1'; const CACHE_NAME = 'datumsrechner-cache-v1';
const urlsToCache = [ const urlsToCache = [
'/', '/',
'/static/style.css',
'/static/favicon.ico', '/static/favicon.ico',
'/static/favicon.png', '/static/favicon.png',
'/static/favicon.svg',
'/static/logo.svg', '/static/logo.svg',
'/static/manifest.json',
]; ];
self.addEventListener('install', event => { self.addEventListener('install', event => {
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache)) .then(cache => {
// Füge nur existierende Dateien zum Cache hinzu
return Promise.allSettled(
urlsToCache.map(url =>
cache.add(url).catch(err => {
console.log('Failed to cache:', url, err);
})
)
);
})
); );
}); });
self.addEventListener('fetch', event => { self.addEventListener('fetch', event => {

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",

File diff suppressed because it is too large Load Diff

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,18 +75,59 @@
</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 }};
const dayApiData = {{ api_counts_hourly|tojson }};
let currentPeriod = 'week';
let currentImprChart = null;
let currentFuncChart = null;
let currentApiChart = null;
// Toggle-Buttons Event Listener
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', function() {
// Aktiven Button aktualisieren
document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
// Zeitraum wechseln
currentPeriod = this.dataset.period;
updateAllCharts();
});
});
function updateImpressionsChart() {
const ctx = document.getElementById('imprChart').getContext('2d');
// Bestehenden Chart zerstören
if (currentImprChart) {
currentImprChart.destroy();
}
let data, labels, counts;
if (currentPeriod === 'week') {
data = weekData;
labels = Object.keys(data);
counts = Object.values(data);
} else {
data = dayData;
labels = Object.keys(data);
counts = Object.values(data);
}
currentImprChart = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: imprLabels, labels: labels,
datasets: [{ datasets: [{
label: 'Impressions/Tag', label: currentPeriod === 'week' ? 'Impressions/Tag' : 'Impressions/Stunde',
data: imprCounts, data: counts,
borderColor: '#059669', borderColor: '#059669',
backgroundColor: 'rgba(5,150,105,0.1)', backgroundColor: 'rgba(5,150,105,0.1)',
tension: 0.2, tension: 0.2,
@@ -62,56 +135,136 @@
}] }]
}, },
options: { options: {
plugins: { legend: { display: true } }, 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 } }
} }
} }
}); });
// Funktionsaufrufe }
// eslint-disable-next-line
const funcCounts = {{ func_counts|tojson }}; function updateFunctionChart() {
const labels = Object.keys(funcCounts); const ctx = document.getElementById('funcChart').getContext('2d');
const data = Object.values(funcCounts);
new Chart(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', type: 'bar',
data: { data: {
labels: labels, labels: labels,
datasets: [{ datasets: [{
label: 'Funktionsaufrufe', label: 'Funktionsaufrufe',
data: data, data: counts,
backgroundColor: '#2563eb', backgroundColor: '#2563eb',
}] }]
}, },
options: { options: {
plugins: { legend: { display: false } }, plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'Funktionsaufrufe (Woche)' : 'Funktionsaufrufe (24h)'
}
},
scales: { scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } } y: { beginAtZero: true, ticks: { stepSize: 1 } }
} }
} }
}); });
// API-Nutzung }
// eslint-disable-next-line
const apiCounts = {{ api_counts|tojson }}; function updateApiChart() {
if (Object.keys(apiCounts).length > 0 && document.getElementById('apiChart')) { const apiChartElement = document.getElementById('apiChart');
new Chart(document.getElementById('apiChart').getContext('2d'), { 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', type: 'bar',
data: { data: {
labels: Object.keys(apiCounts), labels: labels,
datasets: [{ datasets: [{
label: 'API-Aufrufe nach Endpunkt', label: 'API-Aufrufe nach Endpunkt',
data: Object.values(apiCounts), data: counts,
backgroundColor: '#f59e42', backgroundColor: '#f59e42',
}] }]
}, },
options: { options: {
plugins: { legend: { display: false } }, plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'API-Nutzung (Woche)' : 'API-Nutzung (24h)'
}
},
scales: { scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } } y: { beginAtZero: true, ticks: { stepSize: 1 } }
} }
} }
}); });
} }
function updateAllCharts() {
updateImpressionsChart();
updateFunctionChart();
updateApiChart();
}
// Initial Charts erstellen
updateAllCharts();
}); });
</script> </script>
</body> </body>

View File

@@ -221,10 +221,11 @@ 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
# Die Route gibt HTML zurück, nicht JSON data = resp.get_json()
html = resp.data.decode('utf-8') assert "pageviews" in data
# Prüfe auf typische HTML-Elemente des Dashboards assert "func_counts" in data
assert 'Statistik-Dashboard' in html or 'Dashboard' in html assert "impressions_per_day" in data
assert "api_counts" in data
def test_api_monitor(client): def test_api_monitor(client):
resp = client.get('/api/monitor') resp = client.get('/api/monitor')

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,587 @@
# 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"
#: templates/index.html:132
msgid "Taschenrechner"
msgstr "Calculator"
#: templates/index.html:133
msgid "Taschenrechner öffnen"
msgstr "Open calculator"
#: templates/index.html:134
msgid "Taschenrechner schließen"
msgstr "Close calculator"
#: templates/index.html:135
msgid "Verwenden Sie die Tab-Taste um durch die Tasten zu navigieren. Tastatur-Kurzbefehle: Zahlen 0-9, Punkt oder Komma für Dezimal, Plus (+) oder P für Addition, Minus (-) oder M für Subtraktion, Stern (*) oder X für Multiplikation, Schrägstrich (/) oder D für Division, Enter oder Leertaste für Gleich, C für Löschen, Backspace für letzte Ziffer löschen."
msgstr "Use the Tab key to navigate through the buttons. Keyboard shortcuts: Numbers 0-9, period or comma for decimal, Plus (+) or P for addition, Minus (-) or M for subtraction, Asterisk (*) or X for multiplication, Slash (/) or D for division, Enter or Space for equals, C for clear, Backspace for delete last digit."
#: templates/index.html:136
msgid "Taschenrechner Anzeige"
msgstr "Calculator display"
#: templates/index.html:137
msgid "Berechnungsverlauf"
msgstr "Calculation history"
#: templates/index.html:138
msgid "Löschen (Taste: C)"
msgstr "Clear (key: C)"
#: templates/index.html:139
msgid "Letzte Ziffer löschen (Taste: Backspace)"
msgstr "Delete last digit (key: Backspace)"
#: templates/index.html:140
msgid "Dividieren (Taste: / oder D)"
msgstr "Divide (key: / or D)"
#: templates/index.html:141
msgid "Multiplizieren (Taste: * oder X)"
msgstr "Multiply (key: * or X)"
#: templates/index.html:142
msgid "Sieben"
msgstr "Seven"
#: templates/index.html:143
msgid "Acht"
msgstr "Eight"
#: templates/index.html:144
msgid "Neun"
msgstr "Nine"
#: templates/index.html:145
msgid "Subtrahieren (Taste: - oder M)"
msgstr "Subtract (key: - or M)"
#: templates/index.html:146
msgid "Vier"
msgstr "Four"
#: templates/index.html:147
msgid "Fünf"
msgstr "Five"
#: templates/index.html:148
msgid "Sechs"
msgstr "Six"
#: templates/index.html:149
msgid "Addieren (Taste: + oder P)"
msgstr "Add (key: + or P)"
#: templates/index.html:150
msgid "Eins"
msgstr "One"
#: templates/index.html:151
msgid "Zwei"
msgstr "Two"
#: templates/index.html:152
msgid "Drei"
msgstr "Three"
#: templates/index.html:153
msgid "Gleich (Taste: Enter oder Leertaste)"
msgstr "Equals (key: Enter or Space)"
#: templates/index.html:154
msgid "Null"
msgstr "Zero"
#: templates/index.html:155
msgid "Komma (Taste: . oder ,)"
msgstr "Decimal (key: . or ,)"
#: templates/index.html:156
msgid "Fehler"
msgstr "Error"
#: templates/index.html:157
msgid "Berechnung"
msgstr "Calculation"