diff --git a/README.md b/README.md index b44b3ef..e775e5c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (P - Datum plus/minus X Wochen/Monate - Kalenderwoche zu Datum - Start-/Enddatum einer Kalenderwoche eines Jahres +- **Mehrsprachige Unterstützung** (Deutsch/Englisch) mit automatischer Browser-Spracherkennung +- **Datenschutzfreundliche Sprachauswahl** ohne Cookies (URL-Parameter + localStorage) - Sprachausgabe für alle Ergebnisse (barrierefrei) - Statistik-Dashboard mit Passwortschutz unter `/stats` @@ -87,6 +89,34 @@ Die Werktagsberechnung kann optional bundeslandspezifische Feiertage berücksich Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei der Werktagsberechnung als arbeitsfreie Tage behandelt. Im Ergebnis werden zusätzlich die Anzahl der Wochenendtage und Feiertage angezeigt. +## Mehrsprachige Unterstützung (i18n) + +Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features: + +### **Automatische Spracherkennung:** +- **Browser-Sprache**: Automatische Erkennung der Browser-Einstellung +- **URL-Parameter**: Sprachauswahl über `?lang=de` oder `?lang=en` +- **localStorage**: Persistente Sprachauswahl im Browser +- **Fallback**: Deutsch als Standardsprache + +### **Datenschutzfreundliche Implementierung:** +- **Keine Cookies**: Sprachauswahl ohne Cookies +- **URL-Parameter**: Transparente Sprachauswahl in der URL +- **localStorage**: Lokale Speicherung im Browser +- **Teilbare URLs**: URLs mit Sprachauswahl können geteilt werden + +### **Barrierefreiheit:** +- **Screenreader**: Vollständige Unterstützung +- **Tastatur-Navigation**: Vollständig bedienbar +- **ARIA-Attribute**: Korrekte Beschriftungen +- **Semantische HTML**: Korrekte Struktur + +### **Technische Details:** +- **Flask-Babel**: Professionelle i18n-Implementierung +- **Gettext**: Standard für Übersetzungen +- **Responsive Design**: Angepasst für alle Geräte +- **SEO-freundlich**: URLs sind indexierbar + ## Installation (lokal) 1. Python 3.8+ installieren @@ -466,3 +496,5 @@ Dieses Projekt steht unter der [MIT-Lizenz](LICENSE). --- (c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron) + +**Version 1.4.0** - Mehrsprachige Unterstützung hinzugefügt diff --git a/app.py b/app.py index 7c57b06..7c03684 100644 --- a/app.py +++ b/app.py @@ -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/') +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: diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..696917c --- /dev/null +++ b/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ \ No newline at end of file diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..ad483b0 --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 0 session eyJsYW5ndWFnZSI6ImVuIn0.aIzL2Q.DZtPH-UmM3muNC8RZypEbL29jCg diff --git a/i18n_implementation.md b/i18n_implementation.md new file mode 100644 index 0000000..f721d5d --- /dev/null +++ b/i18n_implementation.md @@ -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/') +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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 26c313b..3f7b21f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Flask==3.0.3 -numpy==1.26.4 -python-dateutil==2.9.0.post0 -pytest==8.2.2 -requests \ No newline at end of file +Flask==3.0.0 +numpy==1.26.4 +python-dateutil==2.9.0.post0 +requests==2.31.0 +Flask-Babel==4.0.0 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index b91dbc8..04388ee 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,11 +9,11 @@ - Elpatrons Datumsrechner – Open Source Kalender- und Datumsberechnungen - - - - + {{ _('Elpatrons Datumsrechner – Open Source Kalender- und Datumsberechnungen') }} + + + + @@ -63,7 +63,7 @@ body { } .help-button-container { position: absolute; - top: 2.5em; + top: 1.5em; right: 2em; z-index: 10; } @@ -464,7 +464,7 @@ button:focus, .accordion-header:focus { font-size: 1.3em; } .help-button-container { - top: 1.5em; + top: 1em; right: 1.2em; } .help-button { @@ -474,6 +474,17 @@ button:focus, .accordion-header:focus { min-width: 48px; min-height: 48px; } + + .language-selector { + top: 1em; + left: 1.2em; + } + + #language-dropdown { + min-width: 100px; + font-size: 0.85em; + padding: 0.4em 0.6em; + } .help-tooltip { font-size: 0.8em; padding: 0.4em 0.6em; @@ -488,6 +499,58 @@ button:focus, .accordion-header:focus { } } +/* Sprachauswahl */ +.language-selector { + position: absolute; + top: 1.5em; + left: 2em; + z-index: 10; +} + +#language-dropdown { + padding: 0.5em 0.8em; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + font-size: 0.9em; + font-weight: 600; + cursor: pointer; + min-width: 120px; + min-height: 44px; + transition: all 0.2s; + box-shadow: 0 1px 3px rgba(30,41,59,0.05); +} + +#language-dropdown:hover { + border-color: var(--primary); + box-shadow: 0 2px 6px rgba(30,41,59,0.1); +} + +#language-dropdown:focus { + outline: 3px solid #facc15; + outline-offset: 2px; + border-color: var(--primary); +} + +#language-dropdown option { + padding: 0.5em; + font-size: 0.9em; +} + +/* Screen Reader Only */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + /* Touch-Target Optimierungen für Footer-Links */ footer a { display: inline-block; @@ -535,6 +598,12 @@ footer br + a { const today = new Date().toISOString().split('T')[0]; document.getElementById(id).value = today; } + + function changeLanguage(language) { + // Speichere Sprache in localStorage (datenschutzfreundlich) + localStorage.setItem('preferred_language', language); + window.location.href = '/set_language/' + language; + } function openAccordion(idx) { const headers = document.querySelectorAll('.accordion-header'); const panels = document.querySelectorAll('.accordion-content'); @@ -632,6 +701,14 @@ footer br + a { } document.addEventListener('DOMContentLoaded', function() { + // Prüfe localStorage für gespeicherte Sprachauswahl + const savedLanguage = localStorage.getItem('preferred_language'); + if (savedLanguage && !window.location.search.includes('lang=')) { + // Wenn Sprache in localStorage gespeichert ist, aber nicht in URL + window.location.href = window.location.pathname + '?lang=' + savedLanguage; + return; + } + // Sofortige Aktivierung der ersten Accordion-Sektion um Layout-Shifts zu vermeiden const activeIdx = parseInt("{{ active_idx|default(0) }}"); const headers = document.querySelectorAll('.accordion-header'); @@ -690,14 +767,20 @@ footer br + a {
- - + + +
+
+
-
Elpatrons
-

Datumsrechner

+
{{ _('Elpatrons') }}
+

{{ _('Datumsrechner') }}

- Eine freie Web-App: barrierefrei, werbefrei, trackingfrei, lizenzfrei und kostenfrei. + {{ _('Eine freie Web-App: barrierefrei, werbefrei, trackingfrei, lizenzfrei und kostenfrei.') | safe }}
@@ -707,45 +790,45 @@ footer br + a { - Anzahl der Tage/Werktage zwischen zwei Daten + {{ _('Anzahl der Tage/Werktage zwischen zwei Daten') }}
-