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
This commit is contained in:
2025-08-01 16:40:46 +02:00
parent d88581a663
commit c7d95e5c4c
11 changed files with 1427 additions and 107 deletions

139
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
import numpy as np
from dateutil.relativedelta import relativedelta
@@ -11,12 +12,40 @@ app_start_time = time.time()
app = Flask(__name__)
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
APP_VERSION = "1.3.13"
# HTML-Template wird jetzt aus templates/index.html geladen
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):
"""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}")
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'])
def index():
# Rudimentäres Logging für Page Impressions
@@ -94,24 +141,29 @@ def index():
else:
tage = abs((d2 - d1).days)
except Exception:
tage = 'Ungültige Eingabe'
tage = gettext('Ungültige Eingabe')
elif action == 'wochentag':
active_idx = 1
datum = request.form.get('datum3')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()]
wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
except Exception:
wochentag = 'Ungültige Eingabe'
wochentag = gettext('Ungültige Eingabe')
elif action == 'kw_berechnen':
active_idx = 2
datum = request.form.get('datum6')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
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:
kw_berechnen = 'Ungültige Eingabe'
kw_berechnen = gettext('Ungültige Eingabe')
elif action == 'kw_datum':
active_idx = 3
jahr = request.form.get('jahr7')
@@ -122,9 +174,13 @@ def index():
# Montag der KW
start = datetime.fromisocalendar(jahr, kw, 1)
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:
kw_datum = 'Ungültige Eingabe'
kw_datum = gettext('Ungültige Eingabe')
elif action == 'plusminus':
active_idx = 4
datum = request.form.get('datum_pm')
@@ -137,31 +193,44 @@ def index():
anzahl_int = int(anzahl)
if richtung == 'sub':
anzahl_int = -anzahl_int
locale = get_locale()
if einheit == 'tage':
if is_werktage:
# Werktage: numpy busday_offset
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
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:
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':
if is_werktage:
plusminus_result = 'Nicht unterstützt: Werktage + Wochen.'
plusminus_result = gettext('Nicht unterstützt: Werktage + Wochen.')
else:
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':
if is_werktage:
plusminus_result = 'Nicht unterstützt: Werktage + Monate.'
plusminus_result = gettext('Nicht unterstützt: Werktage + Monate.')
else:
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:
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
, 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
)
@@ -247,7 +316,8 @@ def api_wochentag():
datum = data.get('datum')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()]
wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
return jsonify({'result': wochentag})
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -260,7 +330,12 @@ def api_kw_berechnen():
try:
d = datetime.strptime(datum, '%Y-%m-%d')
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:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -275,7 +350,12 @@ def api_kw_datum():
kw = int(kw)
start = datetime.fromisocalendar(jahr, kw, 1)
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:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -293,26 +373,39 @@ def api_plusminus():
anzahl_int = int(anzahl)
if richtung == 'sub':
anzahl_int = -anzahl_int
locale = get_locale()
if einheit == 'tage':
if is_werktage:
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
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:
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':
if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
else:
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':
if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
else:
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:
return jsonify({'error': 'Ungültige Einheit'}), 400
except Exception as e: