modified: app.py
modified: static/css/style.css modified: templates/base.html modified: templates/index.html modified: templates/weather.html new file: translations.py
This commit is contained in:
31
app.py
31
app.py
@@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify, redirect, make_response, g
|
||||||
|
from translations import TRANSLATIONS
|
||||||
from geopy.geocoders import Nominatim
|
from geopy.geocoders import Nominatim
|
||||||
from geopy.exc import GeocoderTimedOut
|
from geopy.exc import GeocoderTimedOut
|
||||||
import math
|
import math
|
||||||
@@ -19,6 +20,18 @@ import requests as _requests
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.logger.setLevel(logging.INFO)
|
app.logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _set_language():
|
||||||
|
lang = request.cookies.get("lang", "de")
|
||||||
|
if lang not in TRANSLATIONS:
|
||||||
|
lang = "de"
|
||||||
|
g.lang = lang
|
||||||
|
g.T = TRANSLATIONS[lang]
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def _inject_i18n():
|
||||||
|
return {"T": g.get("T", TRANSLATIONS["de"]), "lang": g.get("lang", "de")}
|
||||||
|
|
||||||
_GEOLOCATOR = Nominatim(user_agent="skywatcher-app/1.0")
|
_GEOLOCATOR = Nominatim(user_agent="skywatcher-app/1.0")
|
||||||
|
|
||||||
# ── Zeitzone ────────────────────────────────────────────────────────────
|
# ── Zeitzone ────────────────────────────────────────────────────────────
|
||||||
@@ -810,6 +823,16 @@ def filter_unrealistic_warnings(warnings, forecast, now_local=None):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@app.route("/set-lang")
|
||||||
|
def set_lang():
|
||||||
|
lang = request.args.get("lang", "de")
|
||||||
|
if lang not in TRANSLATIONS:
|
||||||
|
lang = "de"
|
||||||
|
dest = request.args.get("next") or request.referrer or "/"
|
||||||
|
resp = make_response(redirect(dest))
|
||||||
|
resp.set_cookie("lang", lang, max_age=60*60*24*365, samesite="Lax")
|
||||||
|
return resp
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
@@ -840,14 +863,14 @@ def wetter():
|
|||||||
|
|
||||||
if lat is None:
|
if lat is None:
|
||||||
if not ort:
|
if not ort:
|
||||||
return render_template("index.html", error="Bitte einen Ort eingeben.")
|
return render_template("index.html", error=g.T["error_no_place"])
|
||||||
lat, lon, display_name, state_hint, location_names = geocode_location(ort)
|
lat, lon, display_name, state_hint, location_names = geocode_location(ort)
|
||||||
if lat is None:
|
if lat is None:
|
||||||
return render_template("index.html", error=f'Ort "{ort}" konnte nicht gefunden werden.')
|
return render_template("index.html", error=g.T["error_place_not_found"].format(ort=ort))
|
||||||
loc_tz, location_tz = _get_location_tz(lat, lon)
|
loc_tz, location_tz = _get_location_tz(lat, lon)
|
||||||
forecast, mosmix_station = get_mosmix_forecast(lat, lon, hours=240, loc_tz=loc_tz)
|
forecast, mosmix_station = get_mosmix_forecast(lat, lon, hours=240, loc_tz=loc_tz)
|
||||||
if not forecast:
|
if not forecast:
|
||||||
return render_template("index.html", error="Keine Wetterdaten verfügbar. Bitte später erneut versuchen.")
|
return render_template("index.html", error=g.T["error_no_data"])
|
||||||
station_name = mosmix_station.get("name", ort)
|
station_name = mosmix_station.get("name", ort)
|
||||||
station_id = mosmix_station.get("station_id", "–")
|
station_id = mosmix_station.get("station_id", "–")
|
||||||
station_lat = float(mosmix_station.get("latitude", lat))
|
station_lat = float(mosmix_station.get("latitude", lat))
|
||||||
|
|||||||
@@ -61,6 +61,24 @@ main { flex: 1; }
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.lang-switch {
|
||||||
|
display: flex; align-items: center; gap: 0.15rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.lang-btn {
|
||||||
|
font-size: 0.78rem; font-weight: 600;
|
||||||
|
padding: 0.2rem 0.45rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.lang-btn:hover { color: var(--text); }
|
||||||
|
.lang-btn--active {
|
||||||
|
color: var(--orange);
|
||||||
|
background: rgba(255,140,50,0.12);
|
||||||
|
}
|
||||||
.nav-search-wrap { position: relative; display: flex; align-items: center; }
|
.nav-search-wrap { position: relative; display: flex; align-items: center; }
|
||||||
.nav-search-icon {
|
.nav-search-icon {
|
||||||
position: absolute; left: 10px; width: 16px; height: 16px;
|
position: absolute; left: 10px; width: 16px; height: 16px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="{{ lang }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
@@ -15,17 +15,21 @@
|
|||||||
<form class="nav-search" action="/wetter" method="get" autocomplete="off">
|
<form class="nav-search" action="/wetter" method="get" autocomplete="off">
|
||||||
<div class="nav-search-wrap">
|
<div class="nav-search-wrap">
|
||||||
<svg class="nav-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
<svg class="nav-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||||
<input type="text" name="ort" id="nav-ort" placeholder="Ort suchen …" value="{{ request.args.get('ort','') }}"/>
|
<input type="text" name="ort" id="nav-ort" placeholder="{{ T.nav_placeholder }}" value="{{ request.args.get('ort','') }}"/>
|
||||||
<ul class="ac-list" id="nav-ac"></ul>
|
<ul class="ac-list" id="nav-ac"></ul>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<span class="nav-clock" id="nav-clock"></span>
|
<span class="nav-clock" id="nav-clock" data-suffix="{{ T.clock_suffix }}"></span>
|
||||||
|
<nav class="lang-switch" aria-label="Language">
|
||||||
|
<a href="/set-lang?lang=de&next={{ request.full_path }}" class="lang-btn{% if lang == 'de' %} lang-btn--active{% endif %}" hreflang="de">DE</a>
|
||||||
|
<a href="/set-lang?lang=en&next={{ request.full_path }}" class="lang-btn{% if lang == 'en' %} lang-btn--active{% endif %}" hreflang="en">EN</a>
|
||||||
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main>{% block content %}{% endblock %}</main>
|
<main>{% block content %}{% endblock %}</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
Wetterdaten: <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst – Open Data (MOSMIX)</a>
|
{{ T.data_source }} <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst – Open Data (MOSMIX)</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -73,6 +77,7 @@ window.setupAC = setupAC;
|
|||||||
const tz = tzMeta ? tzMeta.getAttribute("content") : null;
|
const tz = tzMeta ? tzMeta.getAttribute("content") : null;
|
||||||
const navClock = document.getElementById("nav-clock");
|
const navClock = document.getElementById("nav-clock");
|
||||||
const heroTime = document.getElementById("hero-time");
|
const heroTime = document.getElementById("hero-time");
|
||||||
|
const suffix = navClock ? (navClock.dataset.suffix ? " " + navClock.dataset.suffix : "") : "";
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -83,8 +88,8 @@ window.setupAC = setupAC;
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
timeStr = new Intl.DateTimeFormat("de-DE", opts).format(now);
|
timeStr = new Intl.DateTimeFormat("de-DE", opts).format(now);
|
||||||
}
|
}
|
||||||
if (navClock) navClock.textContent = timeStr + " Uhr";
|
if (navClock) navClock.textContent = timeStr + suffix;
|
||||||
if (heroTime) heroTime.textContent = timeStr + " Uhr";
|
if (heroTime) heroTime.textContent = timeStr + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Skywatcher – Wetter für Deutschland{% endblock %}
|
{% block title %}{{ T.page_title_home }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="home-wrap">
|
<div class="home-wrap">
|
||||||
<div class="home-bg-text" aria-hidden="true">WETTER</div>
|
<div class="home-bg-text" aria-hidden="true">WETTER</div>
|
||||||
|
|
||||||
<div class="home-center">
|
<div class="home-center">
|
||||||
<p class="home-eyebrow">Präzise Vorhersagen · Daten via DWD Open Data</p>
|
<p class="home-eyebrow">{{ T.home_eyebrow }}</p>
|
||||||
<h1 class="home-title">Wo willst du<br>das Wetter wissen?</h1>
|
<h1 class="home-title">{{ T.home_headline|safe }}</h1>
|
||||||
|
|
||||||
<form class="home-form" action="/wetter" method="get" autocomplete="off">
|
<form class="home-form" action="/wetter" method="get" autocomplete="off">
|
||||||
<div class="home-input-wrap">
|
<div class="home-input-wrap">
|
||||||
<svg class="home-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="home-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text" name="ort" id="home-ort" placeholder="Stadt, Gemeinde oder PLZ …" required autofocus/>
|
<input type="text" name="ort" id="home-ort" placeholder="{{ T.home_placeholder }}" required autofocus/>
|
||||||
<ul class="ac-list" id="home-ac"></ul>
|
<ul class="ac-list" id="home-ac"></ul>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="home-btn">
|
<button type="submit" class="home-btn">
|
||||||
Abrufen
|
{{ T.home_btn }}
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
<a href="/wetter?ort={{ city }}" class="chip">{{ city }}</a>
|
<a href="/wetter?ort={{ city }}" class="chip">{{ city }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-location" id="btn-location" title="Meinen Standort nutzen">
|
<button class="btn-location" id="btn-location" title="{{ T.btn_location }}">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/><circle cx="12" cy="12" r="9" opacity=".3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/><circle cx="12" cy="12" r="9" opacity=".3"/></svg>
|
||||||
Meinen Standort nutzen
|
{{ T.btn_location }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,14 +49,20 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
const _i18n = {
|
||||||
|
locating: {{ T.locating | tojson }},
|
||||||
|
locationUnavailable: {{ T.location_unavailable | tojson }},
|
||||||
|
geoUnsupported: {{ T.geolocation_unsupported | tojson }},
|
||||||
|
recentlySearched: {{ T.recently_searched | tojson }},
|
||||||
|
};
|
||||||
// Standort-Button
|
// Standort-Button
|
||||||
document.getElementById("btn-location").addEventListener("click", function() {
|
document.getElementById("btn-location").addEventListener("click", function() {
|
||||||
if (!navigator.geolocation) { alert("Geolocation nicht unterstützt."); return; }
|
if (!navigator.geolocation) { alert(_i18n.geoUnsupported); return; }
|
||||||
this.textContent = "Wird ermittelt …";
|
this.textContent = _i18n.locating;
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
pos => { window.location = `/wetter?lat=${pos.coords.latitude}&lon=${pos.coords.longitude}&ort=Mein+Standort`; },
|
pos => { window.location = `/wetter?lat=${pos.coords.latitude}&lon=${pos.coords.longitude}&ort=Mein+Standort`; },
|
||||||
() => { this.textContent = "Standort nicht verfügbar"; this.disabled = false; }
|
() => { this.textContent = _i18n.locationUnavailable; this.disabled = false; }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ document.getElementById("btn-location").addEventListener("click", function() {
|
|||||||
const wrap = document.getElementById("home-chips");
|
const wrap = document.getElementById("home-chips");
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "recent-label";
|
div.className = "recent-label";
|
||||||
div.textContent = "Zuletzt gesucht:";
|
div.textContent = _i18n.recentlySearched;
|
||||||
wrap.before(div);
|
wrap.before(div);
|
||||||
const strip = document.createElement("div");
|
const strip = document.createElement("div");
|
||||||
strip.className = "home-chips home-chips--recent";
|
strip.className = "home-chips home-chips--recent";
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
{{ display_name.split(',')[0] }}
|
{{ display_name.split(',')[0] }}
|
||||||
<span class="hero-meta-sub">{{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }}</span>
|
<span class="hero-meta-sub">{{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }}</span>
|
||||||
<span class="hero-meta-time" id="hero-time">{{ now_local }} Uhr</span>
|
<span class="hero-meta-time" id="hero-time">{{ now_local }}{% if T.clock_suffix %} {{ T.clock_suffix }}{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hero-main">
|
<div class="hero-main">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<span>{{ current.wind_kmh }} km/h {{ wind_dir_name(current.wind_dir) }}</span>
|
<span>{{ current.wind_kmh }} km/h {{ wind_dir_name(current.wind_dir) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current.cloud_pct is not none %}
|
{% if current.cloud_pct is not none %}
|
||||||
<span>{{ current.cloud_pct }}% Bedeckung</span>
|
<span>{{ current.cloud_pct }}% {{ T.cloud_cover }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current.pressure_hpa is not none %}
|
{% if current.pressure_hpa is not none %}
|
||||||
<span>{{ current.pressure_hpa }} hPa</span>
|
<span>{{ current.pressure_hpa }} hPa</span>
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
{% set uv_curr_str = "–" %}
|
{% set uv_curr_str = "–" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set items = [
|
{% set items = [
|
||||||
("Gefühlt wie", (current.feels_like|string + " °C") if current.feels_like is not none else "–"),
|
(T.feels_like, (current.feels_like|string + " °C") if current.feels_like is not none else "–"),
|
||||||
("Böen", (current.gust_kmh|string + " km/h") if current.gust_kmh is not none else "–"),
|
(T.gusts, (current.gust_kmh|string + " km/h") if current.gust_kmh is not none else "–"),
|
||||||
("Niederschlag", (current.precip_mm|string + " mm") if (current.precip_mm is not none and current.precip_mm > 0) else ((current.rain_prob|string + " %") if (current.rain_prob is not none and current.rain_prob > 0) else "0 mm")),
|
(T.precipitation, (current.precip_mm|string + " mm") if (current.precip_mm is not none and current.precip_mm > 0) else ((current.rain_prob|string + " %") if (current.rain_prob is not none and current.rain_prob > 0) else "0 mm")),
|
||||||
("Sonne", (current.sun_min|string + " min/h") if (current.sun_min is not none and current.sun_min > 0) else "–"),
|
(T.sun, (current.sun_min|string + " min/h") if (current.sun_min is not none and current.sun_min > 0) else "–"),
|
||||||
("UV-Index", uv_curr_str),
|
(T.uv_index, uv_curr_str),
|
||||||
] %}
|
] %}
|
||||||
{% for label, val in items %}
|
{% for label, val in items %}
|
||||||
<div class="hero-metric">
|
<div class="hero-metric">
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hero-station">
|
<div class="hero-station">
|
||||||
📡 Station <strong>{{ station_name }}</strong> ({{ station_dist }} km)
|
📡 {{ T.station }} <strong>{{ station_name }}</strong> ({{ station_dist }} km)
|
||||||
{% if sunrise %}
|
{% if sunrise %}
|
||||||
· 🌅 {{ sunrise }} · 🌇 {{ sunset }}
|
· 🌅 {{ sunrise }} · 🌇 {{ sunset }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<p class="warn-desc">{{ short_desc }}{% if short_desc|length < w.description|length %}.{% endif %}</p>
|
<p class="warn-desc">{{ short_desc }}{% if short_desc|length < w.description|length %}.{% endif %}</p>
|
||||||
{% if short_desc|length < w.description|length %}
|
{% if short_desc|length < w.description|length %}
|
||||||
<details class="warn-more">
|
<details class="warn-more">
|
||||||
<summary>Details</summary>
|
<summary>{{ T.warn_details }}</summary>
|
||||||
<p>{{ w.description }}</p>
|
<p>{{ w.description }}</p>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -113,33 +113,33 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="section insights">
|
<section class="section insights">
|
||||||
<h2 class="section-title">Kurzfazit</h2>
|
<h2 class="section-title">{{ T.section_summary }}</h2>
|
||||||
<div class="insight-grid">
|
<div class="insight-grid">
|
||||||
<div class="insight-card">
|
<div class="insight-card">
|
||||||
<span class="insight-label">Temperaturtrend (6h)</span>
|
<span class="insight-label">{{ T.temp_trend_label }}</span>
|
||||||
<strong class="insight-value">
|
<strong class="insight-value">
|
||||||
{% if temp_delta_6h is not none %}
|
{% if temp_delta_6h is not none %}
|
||||||
{% if temp_delta_6h > 0 %}+{% endif %}{{ temp_delta_6h }}° · {{ temp_trend_6h }}
|
{% if temp_delta_6h > 0 %}+{% endif %}{{ temp_delta_6h }}° · {{ T.get(temp_trend_6h, temp_trend_6h) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="insight-card">
|
<div class="insight-card">
|
||||||
<span class="insight-label">Drucktrend (6h)</span>
|
<span class="insight-label">{{ T.pressure_trend_label }}</span>
|
||||||
<strong class="insight-value">
|
<strong class="insight-value">
|
||||||
{% if pressure_delta is not none %}
|
{% if pressure_delta is not none %}
|
||||||
{% if pressure_delta > 0 %}+{% endif %}{{ pressure_delta }} hPa · {{ pressure_trend }}
|
{% if pressure_delta > 0 %}+{% endif %}{{ pressure_delta }} hPa · {{ T.get(pressure_trend, pressure_trend) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="insight-card">
|
<div class="insight-card">
|
||||||
<span class="insight-label">Bestes Aktivitätsfenster</span>
|
<span class="insight-label">{{ T.best_window_label }}</span>
|
||||||
<strong class="insight-value">
|
<strong class="insight-value">
|
||||||
{% if best_window %}
|
{% if best_window %}
|
||||||
{{ best_window.start.strftime('%H:%M') }}–{{ best_window.end.strftime('%H:%M') }} · Score {{ best_window.score }}
|
{{ best_window.start.strftime('%H:%M') }}–{{ best_window.end.strftime('%H:%M') }} · {{ T.score }} {{ best_window.score }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -150,23 +150,23 @@
|
|||||||
|
|
||||||
<!-- STÜNDLICH -->
|
<!-- STÜNDLICH -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Stundenweise</h2>
|
<h2 class="section-title">{{ T.section_hourly }}</h2>
|
||||||
<div class="hourly-legend">
|
<div class="hourly-legend">
|
||||||
<span class="hl-label">Legende:</span>
|
<span class="hl-label">{{ T.legend }}</span>
|
||||||
<span class="hl-badge hl-conf hl-conf--hoch">Konfidenz hoch</span>
|
<span class="hl-badge hl-conf hl-conf--hoch">{{ T.conf_high }}</span>
|
||||||
<span class="hl-badge hl-conf hl-conf--mittel">mittel</span>
|
<span class="hl-badge hl-conf hl-conf--mittel">{{ T.conf_mid }}</span>
|
||||||
<span class="hl-badge hl-conf hl-conf--niedrig">niedrig</span>
|
<span class="hl-badge hl-conf hl-conf--niedrig">{{ T.conf_low }}</span>
|
||||||
<span class="hl-sep">·</span>
|
<span class="hl-sep">·</span>
|
||||||
<span class="hl-badge hl-act">Aktivität 0–100</span>
|
<span class="hl-badge hl-act">{{ T.activity_legend }}</span>
|
||||||
<span class="hl-sep">·</span>
|
<span class="hl-sep">·</span>
|
||||||
<span class="hl-badge hl-uv">UV-Index</span>
|
<span class="hl-badge hl-uv">{{ T.uv_index }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hourly-strip-wrap">
|
<div class="hourly-strip-wrap">
|
||||||
<div class="hourly-strip">
|
<div class="hourly-strip">
|
||||||
{% for h in forecast %}
|
{% for h in forecast %}
|
||||||
<div class="hcard {% if loop.first %}hcard--now{% endif %}">
|
<div class="hcard {% if loop.first %}hcard--now{% endif %}">
|
||||||
<div class="hcard-time">
|
<div class="hcard-time">
|
||||||
{% if loop.first %}Jetzt
|
{% if loop.first %}{{ T.now }}
|
||||||
{% elif h.datetime is string %}{{ h.datetime[11:16] }}
|
{% elif h.datetime is string %}{{ h.datetime[11:16] }}
|
||||||
{% else %}{{ h.datetime.strftime('%H:%M') }}{% endif %}
|
{% else %}{{ h.datetime.strftime('%H:%M') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -193,9 +193,9 @@
|
|||||||
<div class="hcard-wind">{{ h.wind_kmh }}<small>km/h</small></div>
|
<div class="hcard-wind">{{ h.wind_kmh }}<small>km/h</small></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="hcard-confidence hcard-confidence--{{ h.confidence_label }}">{{ h.confidence }}%</div>
|
<div class="hcard-confidence hcard-confidence--{{ h.confidence_label }}">{{ h.confidence }}%</div>
|
||||||
<div class="hcard-activity">Aktiv {{ h.activity_score }}</div>
|
<div class="hcard-activity">{{ T.active }} {{ h.activity_score }}</div>
|
||||||
{% if h.uv_index is not none and h.uv_index > 0 %}
|
{% if h.uv_index is not none and h.uv_index > 0 %}
|
||||||
<div class="hcard-uv hcard-uv--{{ h.uv_level }}">UV {{ h.uv_index }} {{ h.uv_label }}</div>
|
<div class="hcard-uv hcard-uv--{{ h.uv_level }}">UV {{ h.uv_index }} {{ T.get(h.uv_label, h.uv_label) }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -206,8 +206,8 @@
|
|||||||
<!-- CHART -->
|
<!-- CHART -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="chart-head">
|
<div class="chart-head">
|
||||||
<h2 class="section-title">Temperatur, Gefühlt & Niederschlag</h2>
|
<h2 class="section-title">{{ T.section_chart }}</h2>
|
||||||
<span class="chart-range">72 Stunden</span>
|
<span class="chart-range">{{ T.chart_range }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-box">
|
<div class="chart-box">
|
||||||
<canvas id="wxChart"></canvas>
|
<canvas id="wxChart"></canvas>
|
||||||
@@ -216,11 +216,11 @@
|
|||||||
|
|
||||||
<!-- TAGESÜBERSICHT -->
|
<!-- TAGESÜBERSICHT -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Tagesübersicht</h2>
|
<h2 class="section-title">{{ T.section_daily }}</h2>
|
||||||
<div class="daily-header">
|
<div class="daily-header">
|
||||||
<span>Tag</span>
|
<span>{{ T.daily_hdr_day }}</span>
|
||||||
<span>Temperaturbereich</span>
|
<span>{{ T.daily_hdr_range }}</span>
|
||||||
<span>Min · Max · Nieder. · UV</span>
|
<span>{{ T.daily_hdr_details }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="daily-list">
|
<div class="daily-list">
|
||||||
{# calc global min/max for bar scaling – do it in a loop to stay Jinja2-safe #}
|
{# calc global min/max for bar scaling – do it in a loop to stay Jinja2-safe #}
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="data-note">
|
<p class="data-note">
|
||||||
Wetterdaten: <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst – Open Data (MOSMIX)</a>
|
{{ T.data_source }} <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst – Open Data (MOSMIX)</a>
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
// Prüfen ob echte Niederschlagsmengen vorhanden sind
|
// Prüfen ob echte Niederschlagsmengen vorhanden sind
|
||||||
const hasRealPrecip = precip.some(v => v > 0);
|
const hasRealPrecip = precip.some(v => v > 0);
|
||||||
const barData = hasRealPrecip ? precip : rainProb;
|
const barData = hasRealPrecip ? precip : rainProb;
|
||||||
const barLabel = hasRealPrecip ? "Niederschlag (mm)" : "Regenwahrsch. (%)";
|
const barLabel = hasRealPrecip ? {{ T.chart_precip | tojson }} : {{ T.chart_rainprob | tojson }};
|
||||||
const barMax = hasRealPrecip ? undefined : 100;
|
const barMax = hasRealPrecip ? undefined : 100;
|
||||||
|
|
||||||
// Nur jeden 3. Label anzeigen, Rest leer lassen
|
// Nur jeden 3. Label anzeigen, Rest leer lassen
|
||||||
@@ -327,7 +327,7 @@
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
type: "line",
|
type: "line",
|
||||||
label: "Temperatur (°C)",
|
label: {{ T.chart_temp | tojson }},
|
||||||
data: temps,
|
data: temps,
|
||||||
borderColor: "#ff8c32",
|
borderColor: "#ff8c32",
|
||||||
backgroundColor: grad,
|
backgroundColor: grad,
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "line",
|
type: "line",
|
||||||
label: "Gefühlt (°C)",
|
label: {{ T.chart_feels | tojson }},
|
||||||
data: feels,
|
data: feels,
|
||||||
borderColor: "#ffd4a8",
|
borderColor: "#ffd4a8",
|
||||||
borderDash: [5, 4],
|
borderDash: [5, 4],
|
||||||
|
|||||||
176
translations.py
Normal file
176
translations.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""UI translation strings for Skywatcher (de / en)."""
|
||||||
|
|
||||||
|
TRANSLATIONS = {
|
||||||
|
"de": {
|
||||||
|
# ── Nav / global ───────────────────────────────────────────
|
||||||
|
"nav_placeholder": "Ort suchen …",
|
||||||
|
"clock_suffix": "Uhr",
|
||||||
|
|
||||||
|
# ── Index page ─────────────────────────────────────────────
|
||||||
|
"page_title_home": "Skywatcher – Wetter für Deutschland",
|
||||||
|
"home_eyebrow": "Präzise Vorhersagen · Daten via DWD Open Data",
|
||||||
|
"home_headline": "Wo willst du<br>das Wetter wissen?",
|
||||||
|
"home_placeholder": "Stadt, Gemeinde oder PLZ …",
|
||||||
|
"home_btn": "Abrufen",
|
||||||
|
"btn_location": "Meinen Standort nutzen",
|
||||||
|
"locating": "Wird ermittelt …",
|
||||||
|
"location_unavailable": "Standort nicht verfügbar",
|
||||||
|
"geolocation_unsupported": "Geolocation nicht unterstützt.",
|
||||||
|
"recently_searched": "Zuletzt gesucht:",
|
||||||
|
|
||||||
|
# ── Error messages ─────────────────────────────────────────
|
||||||
|
"error_no_place": "Bitte einen Ort eingeben.",
|
||||||
|
"error_place_not_found": "Ort \"{ort}\" konnte nicht gefunden werden.",
|
||||||
|
"error_no_data": "Keine Wetterdaten verfügbar. Bitte später erneut versuchen.",
|
||||||
|
|
||||||
|
# ── Hero ───────────────────────────────────────────────────
|
||||||
|
"cloud_cover": "Bedeckung",
|
||||||
|
"feels_like": "Gefühlt wie",
|
||||||
|
"gusts": "Böen",
|
||||||
|
"precipitation": "Niederschlag",
|
||||||
|
"sun": "Sonne",
|
||||||
|
"uv_index": "UV-Index",
|
||||||
|
"station": "Station",
|
||||||
|
|
||||||
|
# ── Warnings ───────────────────────────────────────────────
|
||||||
|
"warn_details": "Details",
|
||||||
|
|
||||||
|
# ── Insights ───────────────────────────────────────────────
|
||||||
|
"section_summary": "Kurzfazit",
|
||||||
|
"temp_trend_label": "Temperaturtrend (6h)",
|
||||||
|
"pressure_trend_label": "Drucktrend (6h)",
|
||||||
|
"best_window_label": "Bestes Aktivitätsfenster",
|
||||||
|
"score": "Score",
|
||||||
|
|
||||||
|
# ── Hourly strip ───────────────────────────────────────────
|
||||||
|
"section_hourly": "Stundenweise",
|
||||||
|
"legend": "Legende:",
|
||||||
|
"conf_high": "Konfidenz hoch",
|
||||||
|
"conf_mid": "mittel",
|
||||||
|
"conf_low": "niedrig",
|
||||||
|
"activity_legend": "Aktivität 0–100",
|
||||||
|
"now": "Jetzt",
|
||||||
|
"active": "Aktiv",
|
||||||
|
|
||||||
|
# ── Chart ──────────────────────────────────────────────────
|
||||||
|
"section_chart": "Temperatur, Gefühlt & Niederschlag",
|
||||||
|
"chart_range": "72 Stunden",
|
||||||
|
"chart_temp": "Temperatur (°C)",
|
||||||
|
"chart_feels": "Gefühlt (°C)",
|
||||||
|
"chart_precip": "Niederschlag (mm)",
|
||||||
|
"chart_rainprob": "Regenwahrsch. (%)",
|
||||||
|
|
||||||
|
# ── Daily ──────────────────────────────────────────────────
|
||||||
|
"section_daily": "Tagesübersicht",
|
||||||
|
"daily_hdr_day": "Tag",
|
||||||
|
"daily_hdr_range": "Temperaturbereich",
|
||||||
|
"daily_hdr_details": "Min · Max · Nieder. · UV",
|
||||||
|
|
||||||
|
# ── Footer ─────────────────────────────────────────────────
|
||||||
|
"data_source": "Wetterdaten:",
|
||||||
|
|
||||||
|
# ── Dynamic back-end label translations ───────────────────
|
||||||
|
# confidence
|
||||||
|
"hoch": "hoch",
|
||||||
|
"mittel": "mittel",
|
||||||
|
"niedrig": "niedrig",
|
||||||
|
# UV
|
||||||
|
"moderat": "moderat",
|
||||||
|
"sehr hoch": "sehr hoch",
|
||||||
|
"extrem": "extrem",
|
||||||
|
# pressure / temp trend
|
||||||
|
"steigend": "steigend",
|
||||||
|
"fallend": "fallend",
|
||||||
|
"stabil": "stabil",
|
||||||
|
"wärmer": "wärmer",
|
||||||
|
"kälter": "kälter",
|
||||||
|
"konstant": "konstant",
|
||||||
|
},
|
||||||
|
|
||||||
|
"en": {
|
||||||
|
# ── Nav / global ───────────────────────────────────────────
|
||||||
|
"nav_placeholder": "Search location …",
|
||||||
|
"clock_suffix": "",
|
||||||
|
|
||||||
|
# ── Index page ─────────────────────────────────────────────
|
||||||
|
"page_title_home": "Skywatcher – Weather Forecast",
|
||||||
|
"home_eyebrow": "Precise forecasts · Data via DWD Open Data",
|
||||||
|
"home_headline": "Where do you want<br>to know the weather?",
|
||||||
|
"home_placeholder": "City, municipality or ZIP …",
|
||||||
|
"home_btn": "Get forecast",
|
||||||
|
"btn_location": "Use my location",
|
||||||
|
"locating": "Locating …",
|
||||||
|
"location_unavailable": "Location unavailable",
|
||||||
|
"geolocation_unsupported": "Geolocation not supported.",
|
||||||
|
"recently_searched": "Recently searched:",
|
||||||
|
|
||||||
|
# ── Error messages ─────────────────────────────────────────
|
||||||
|
"error_no_place": "Please enter a location.",
|
||||||
|
"error_place_not_found": "Location \"{ort}\" could not be found.",
|
||||||
|
"error_no_data": "No weather data available. Please try again later.",
|
||||||
|
|
||||||
|
# ── Hero ───────────────────────────────────────────────────
|
||||||
|
"cloud_cover": "Cloud cover",
|
||||||
|
"feels_like": "Feels like",
|
||||||
|
"gusts": "Gusts",
|
||||||
|
"precipitation": "Precipitation",
|
||||||
|
"sun": "Sun",
|
||||||
|
"uv_index": "UV index",
|
||||||
|
"station": "Station",
|
||||||
|
|
||||||
|
# ── Warnings ───────────────────────────────────────────────
|
||||||
|
"warn_details": "Details",
|
||||||
|
|
||||||
|
# ── Insights ───────────────────────────────────────────────
|
||||||
|
"section_summary": "Summary",
|
||||||
|
"temp_trend_label": "Temperature trend (6h)",
|
||||||
|
"pressure_trend_label": "Pressure trend (6h)",
|
||||||
|
"best_window_label": "Best activity window",
|
||||||
|
"score": "Score",
|
||||||
|
|
||||||
|
# ── Hourly strip ───────────────────────────────────────────
|
||||||
|
"section_hourly": "Hourly",
|
||||||
|
"legend": "Legend:",
|
||||||
|
"conf_high": "Confidence high",
|
||||||
|
"conf_mid": "medium",
|
||||||
|
"conf_low": "low",
|
||||||
|
"activity_legend": "Activity 0–100",
|
||||||
|
"now": "Now",
|
||||||
|
"active": "Active",
|
||||||
|
|
||||||
|
# ── Chart ──────────────────────────────────────────────────
|
||||||
|
"section_chart": "Temperature, Feels like & Precipitation",
|
||||||
|
"chart_range": "72 hours",
|
||||||
|
"chart_temp": "Temperature (°C)",
|
||||||
|
"chart_feels": "Feels like (°C)",
|
||||||
|
"chart_precip": "Precipitation (mm)",
|
||||||
|
"chart_rainprob": "Rain prob. (%)",
|
||||||
|
|
||||||
|
# ── Daily ──────────────────────────────────────────────────
|
||||||
|
"section_daily": "Daily overview",
|
||||||
|
"daily_hdr_day": "Day",
|
||||||
|
"daily_hdr_range": "Temperature range",
|
||||||
|
"daily_hdr_details": "Min · Max · Precip. · UV",
|
||||||
|
|
||||||
|
# ── Footer ─────────────────────────────────────────────────
|
||||||
|
"data_source": "Weather data:",
|
||||||
|
|
||||||
|
# ── Dynamic back-end label translations ───────────────────
|
||||||
|
# confidence
|
||||||
|
"hoch": "high",
|
||||||
|
"mittel": "medium",
|
||||||
|
"niedrig": "low",
|
||||||
|
# UV
|
||||||
|
"moderat": "moderate",
|
||||||
|
"sehr hoch": "very high",
|
||||||
|
"extrem": "extreme",
|
||||||
|
# pressure / temp trend
|
||||||
|
"steigend": "rising",
|
||||||
|
"fallend": "falling",
|
||||||
|
"stabil": "stable",
|
||||||
|
"wärmer": "warmer",
|
||||||
|
"kälter": "colder",
|
||||||
|
"konstant": "steady",
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user