nearby
This commit is contained in:
+118
-1
@@ -207,5 +207,122 @@
|
||||
"no_data_title": "Für dieses Modell gibt es hier keine Daten",
|
||||
"no_data_body": "Das gewählte Wettermodell deckt {location} nicht ab - regionale Modelle liefern nur Daten in ihrem eigenen Gebiet.",
|
||||
"no_data_try_city": "Ort auf {city} ändern",
|
||||
"no_data_best_match": "Zu „Best match“ wechseln"
|
||||
"no_data_best_match": "Zu „Best match“ wechseln",
|
||||
"model_archive": "Reanalyse",
|
||||
"model_seasonal": "Saisonmodell",
|
||||
"nearby_cities_title": "Städte in der Nähe",
|
||||
"nearby_cities_subtitle": "Höchst- und Tiefstwerte für den gewählten Tag",
|
||||
"supporter_dialog_title": "Drizz.li Unterstützer",
|
||||
"supporter_dialog_desc": "Fügen Sie den Zugangsschlüssel aus Ihrer Unterstützer-E-Mail ein, um die Extras freizuschalten.",
|
||||
"supporter_extras_active": "Unterstützer-Extras sind aktiv",
|
||||
"supporter_lifetime": "Lebenslanger Zugang",
|
||||
"supporter_active_until": "Aktiv bis {date}",
|
||||
"supporter_access_key": "Zugangsschlüssel",
|
||||
"supporter_key_invalid": "Dieser Schlüssel ist ungültig oder abgelaufen.",
|
||||
"supporter_server_unreachable": "Der Server ist nicht erreichbar. Prüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
|
||||
"supporter_verifying": "Wird geprüft…",
|
||||
"supporter_unlock_button": "Freischalten",
|
||||
"supporter_remove_key": "Schlüssel von diesem Gerät entfernen",
|
||||
"supporter_no_key_yet": "Noch keinen Schlüssel?",
|
||||
"supporter_support_from": "Das Projekt ab {price} unterstützen",
|
||||
"supporter_checking": "Ihr Zugang wird geprüft…",
|
||||
"supporter_gate_title": "{feature} ist ein Unterstützer-Extra",
|
||||
"supporter_gate_body": "Drizz.li ist kostenlos und quelloffen. Mit einem Beitrag ab {price} halten Sie es am Laufen - als Dankeschön schalten Unterstützer die Extras frei.",
|
||||
"supporter_become": "Unterstützer werden",
|
||||
"supporter_have_key": "Ich habe einen Schlüssel",
|
||||
"supporter_key_expired": "Ihr gespeicherter Schlüssel ist ungültig oder abgelaufen.",
|
||||
"supporter_enter_key": "Geben Sie Ihren Zugangsschlüssel ein.",
|
||||
"supporter_perk_historical": "Historisches Wetter und Vergleich mit Klimanormalen",
|
||||
"supporter_perk_seasonal": "Saisonale Aussichten: kommende Monate gegen die Klimanormale",
|
||||
"supporter_perk_future": "Neue Unterstützer-Extras, sobald sie erscheinen",
|
||||
"action_close": "Schliessen",
|
||||
"action_done": "Fertig",
|
||||
"action_try_again": "Erneut versuchen",
|
||||
"action_reset_defaults": "Auf Standard zurücksetzen",
|
||||
"customize_meteograms": "Meteogramme anpassen",
|
||||
"customizer_intro": "Ziehen Sie Variablen zwischen den Diagrammen, um Ihr eigenes Layout zu bauen.",
|
||||
"customizer_chart_n": "Diagramm {number}",
|
||||
"customizer_delete_chart": "Diagramm {number} löschen",
|
||||
"customizer_add_chart": "+ Diagramm hinzufügen",
|
||||
"customizer_available": "Verfügbare Variablen",
|
||||
"customizer_drag": "{variable} ziehen",
|
||||
"customizer_remove": "{variable} entfernen",
|
||||
"customizer_drop_here": "Variablen hierher ziehen",
|
||||
"customizer_all_in_use": "Alle Variablen sind in Verwendung",
|
||||
"variables_aria": "Variablenauswahl",
|
||||
"variables_close": "Variablenauswahl schliessen",
|
||||
"variables_table_section": "Stundentabelle",
|
||||
"variables_move_up": "{variable} nach oben verschieben",
|
||||
"variables_move_down": "{variable} nach unten verschieben",
|
||||
"variables_charts_hint_before": "Meteogramm-Variablen konfigurieren Sie über die Schaltfläche",
|
||||
"variables_charts_hint_after": "über den Diagrammen.",
|
||||
"table_customize": "Variablen anpassen",
|
||||
"table_interval_aria": "Stundenintervall",
|
||||
"table_now": "Jetzt",
|
||||
"interval_toggle": "Zwischen 1- und 3-Stunden-Intervall wechseln",
|
||||
"charts_loading": "Diagramme werden geladen…",
|
||||
"chart_download": "Meteogramm als PNG-Bild herunterladen",
|
||||
"chart_credit_viz": "Visualisierung von",
|
||||
"chart_toggle_series": "{series} ein- oder ausblenden",
|
||||
"legend_show": "Legende anzeigen",
|
||||
"meteograms_none_before": "Keine Meteogramme konfiguriert,",
|
||||
"meteograms_none_action": "fügen Sie Variablen hinzu",
|
||||
"meteograms_none_historical": "Keine Meteogramme konfiguriert. Fügen Sie Variablen auf der 7-Tage-Seite hinzu.",
|
||||
"search_searching": "Wird gesucht…",
|
||||
"search_favorites": "Favoriten",
|
||||
"search_recent": "Zuletzt",
|
||||
"search_hint": "Tippen Sie los oder nutzen Sie GPS, um Ihren Standort zu bestimmen",
|
||||
"search_no_results": "Keine Orte gefunden",
|
||||
"search_remove_recent": "{location} aus den letzten Orten entfernen",
|
||||
"search_remove_recent_short": "Aus den letzten entfernen",
|
||||
"search_aria": "Ort suchen",
|
||||
"search_gps": "GPS-Standort verwenden",
|
||||
"model_automatic_selection": "Automatische Auswahl",
|
||||
"model_selector_aria": "Auswahl: {label}",
|
||||
"compare_models_heading": "Modelle",
|
||||
"compare_variables_heading": "Stündliche Wettervariablen",
|
||||
"compare_timeline_title": "Zeitleiste des Modellvergleichs",
|
||||
"compare_model_label": "Modell",
|
||||
"ensemble_trimmed": "Das Ensemble dieses Modells reicht nur etwa {days} Tage voraus, die Streuung ist auf den verfügbaren Zeitraum gekürzt.",
|
||||
"seasonal_explainer_before": "Eine saisonale Vorhersage zeigt, wie stark ein ganzer Monat voraussichtlich",
|
||||
"seasonal_explainer_strong": "von seiner Klimanormalen abweicht",
|
||||
"seasonal_explainer_after": "- nicht das Wetter an einem einzelnen Tag. Lesen Sie den Monatstrend und die Übereinstimmung des Ensembles, nicht die täglichen Ausschläge.",
|
||||
"seasonal_runs_to": "Diese Aussicht reicht bis {date}.",
|
||||
"seasonal_range_aria": "Zeitraum der Aussichten",
|
||||
"seasonal_no_normal": "keine Normale für diesen Monat",
|
||||
"normals_loading": "Normale wird geladen…",
|
||||
"normals_loading_period": "Normale 1991–2020 wird geladen…",
|
||||
"normal_band_legend": "Normalband = Mittel 1991–2020",
|
||||
"anomaly_vs_normal": "{value} gegenüber Normal",
|
||||
"stat_avg_temperature": "Durchschnittstemperatur",
|
||||
"stat_mean_temperature": "Mitteltemperatur",
|
||||
"stat_total_precipitation": "Niederschlagssumme",
|
||||
"stat_warmest_day": "Wärmster Tag",
|
||||
"stat_coldest_day": "Kältester Tag",
|
||||
"historical_daily_heading": "Täglich",
|
||||
"historical_daily_hint": "– Tag wählen für Stundenwerte",
|
||||
"historical_full_range": "– gesamter Zeitraum",
|
||||
"historical_from": "Von",
|
||||
"historical_to": "Bis",
|
||||
"historical_quick_ranges": "Schnellauswahl",
|
||||
"historical_last_days": "{days} Tage",
|
||||
"historical_month_last_year": "Monat, Vorjahr",
|
||||
"label_day": "Tag",
|
||||
"label_night": "Nacht",
|
||||
"daycards_past": "Letzte 3 Tage",
|
||||
"daycards_load_15": "15 Tage laden",
|
||||
"error_technical_details": "Technische Details",
|
||||
"err_network_title": "Der Wetterdienst ist nicht erreichbar",
|
||||
"err_network_hint": "Prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.",
|
||||
"err_nodata_title": "Keine Daten für diesen Ort mit dem gewählten Modell",
|
||||
"err_nodata_hint": "Regionale Wettermodelle decken nur ihr eigenes Gebiet ab - \"Best match\" wählt automatisch ein passendes Modell.",
|
||||
"err_rejected_title": "Der Wetterdienst hat die Anfrage abgelehnt",
|
||||
"err_rejected_hint": "Versuchen Sie andere Einstellungen oder stellen Sie das Modell auf \"Best match\" zurück.",
|
||||
"err_generic_title": "Die Wetterdaten konnten nicht geladen werden",
|
||||
"err_generic_hint": "Versuchen Sie es gleich noch einmal. Wenn es weiterhin auftritt, wechseln Sie zum Modell \"Best match\".",
|
||||
"maps_iframe_title": "Interaktive Karte von Open-Meteo",
|
||||
"page_maps_title": "Wetterkarte",
|
||||
"page_week_title": "Wetter",
|
||||
"search_favorite_add": "Zu Favoriten hinzufügen",
|
||||
"search_favorite_remove": "Aus Favoriten entfernen"
|
||||
}
|
||||
|
||||
+118
-1
@@ -207,5 +207,122 @@
|
||||
"no_data_title": "No forecast data for this model here",
|
||||
"no_data_body": "The selected weather model doesn't cover {location} - regional models only provide data inside their own area.",
|
||||
"no_data_try_city": "Change location to {city}",
|
||||
"no_data_best_match": "Switch to Best match"
|
||||
"no_data_best_match": "Switch to Best match",
|
||||
"model_archive": "Reanalysis",
|
||||
"model_seasonal": "Seasonal model",
|
||||
"nearby_cities_title": "Nearby cities",
|
||||
"nearby_cities_subtitle": "Highs and lows for the selected day",
|
||||
"supporter_dialog_title": "Drizz.li Supporter",
|
||||
"supporter_dialog_desc": "Paste the access key from your supporter email to unlock the extras.",
|
||||
"supporter_extras_active": "Supporter extras are active",
|
||||
"supporter_lifetime": "Lifetime access",
|
||||
"supporter_active_until": "Active until {date}",
|
||||
"supporter_access_key": "Access key",
|
||||
"supporter_key_invalid": "That key is not valid or has expired.",
|
||||
"supporter_server_unreachable": "Couldn't reach the server. Check your connection and try again.",
|
||||
"supporter_verifying": "Verifying…",
|
||||
"supporter_unlock_button": "Unlock",
|
||||
"supporter_remove_key": "Remove key from this device",
|
||||
"supporter_no_key_yet": "No key yet?",
|
||||
"supporter_support_from": "Support the project from {price}",
|
||||
"supporter_checking": "Checking your subscription…",
|
||||
"supporter_gate_title": "{feature} is a supporter extra",
|
||||
"supporter_gate_body": "Drizz.li is free and open source. Chip in from {price} to keep it running - as a thank-you, supporters unlock the extras.",
|
||||
"supporter_become": "Become a supporter",
|
||||
"supporter_have_key": "I have a key",
|
||||
"supporter_key_expired": "Your saved key is no longer valid or has expired.",
|
||||
"supporter_enter_key": "Enter your access key.",
|
||||
"supporter_perk_historical": "Historical weather & climate-normal comparisons",
|
||||
"supporter_perk_seasonal": "Seasonal outlook: months ahead vs the climate normal",
|
||||
"supporter_perk_future": "New supporter extras as they land",
|
||||
"action_close": "Close",
|
||||
"action_done": "Done",
|
||||
"action_try_again": "Try again",
|
||||
"action_reset_defaults": "Reset to defaults",
|
||||
"customize_meteograms": "Customize meteograms",
|
||||
"customizer_intro": "Drag variables between charts to build your own layout.",
|
||||
"customizer_chart_n": "Chart {number}",
|
||||
"customizer_delete_chart": "Delete chart {number}",
|
||||
"customizer_add_chart": "+ Add chart",
|
||||
"customizer_available": "Available variables",
|
||||
"customizer_drag": "Drag {variable}",
|
||||
"customizer_remove": "Remove {variable}",
|
||||
"customizer_drop_here": "Drop variables here",
|
||||
"customizer_all_in_use": "All variables are in use",
|
||||
"variables_aria": "Variable selection",
|
||||
"variables_close": "Close variable selection",
|
||||
"variables_table_section": "Hourly table",
|
||||
"variables_move_up": "Move {variable} up",
|
||||
"variables_move_down": "Move {variable} down",
|
||||
"variables_charts_hint_before": "Meteogram variables are configured with the",
|
||||
"variables_charts_hint_after": "button above the charts.",
|
||||
"table_customize": "Customize variables",
|
||||
"table_interval_aria": "Hourly interval",
|
||||
"table_now": "Now",
|
||||
"interval_toggle": "Toggle between 1-hour and 3-hour intervals",
|
||||
"charts_loading": "Loading charts...",
|
||||
"chart_download": "Download meteogram as PNG image",
|
||||
"chart_credit_viz": "visualisation by",
|
||||
"chart_toggle_series": "Toggle {series}",
|
||||
"legend_show": "Show legend",
|
||||
"meteograms_none_before": "No meteograms configured,",
|
||||
"meteograms_none_action": "add some variables",
|
||||
"meteograms_none_historical": "No meteograms configured. Add variables from the 7-day forecast page.",
|
||||
"search_searching": "Searching...",
|
||||
"search_favorites": "Favorites",
|
||||
"search_recent": "Recent",
|
||||
"search_hint": "Start typing to search or use GPS to detect your position",
|
||||
"search_no_results": "No locations found",
|
||||
"search_remove_recent": "Remove {location} from recent locations",
|
||||
"search_remove_recent_short": "Remove from recent",
|
||||
"search_aria": "Search location",
|
||||
"search_gps": "Use GPS location",
|
||||
"model_automatic_selection": "Automatic selection",
|
||||
"model_selector_aria": "{label} selection",
|
||||
"compare_models_heading": "Models",
|
||||
"compare_variables_heading": "Hourly Weather Variables",
|
||||
"compare_timeline_title": "Model Comparison Timeline",
|
||||
"compare_model_label": "Model",
|
||||
"ensemble_trimmed": "This model's ensemble only reaches about {days} days ahead, the spread is trimmed to its available range.",
|
||||
"seasonal_explainer_before": "A seasonal forecast shows how a whole month is likely to",
|
||||
"seasonal_explainer_strong": "depart from its climate normal",
|
||||
"seasonal_explainer_after": "- not the weather on any given day. Read the monthly trend and the ensemble agreement, not the daily wiggles.",
|
||||
"seasonal_runs_to": "This outlook runs to {date}.",
|
||||
"seasonal_range_aria": "Outlook range",
|
||||
"seasonal_no_normal": "no normal for this month",
|
||||
"normals_loading": "normal loading…",
|
||||
"normals_loading_period": "1991–2020 normal loading…",
|
||||
"normal_band_legend": "normal band = 1991–2020 mean",
|
||||
"anomaly_vs_normal": "{value} vs normal",
|
||||
"stat_avg_temperature": "Average temperature",
|
||||
"stat_mean_temperature": "Mean temperature",
|
||||
"stat_total_precipitation": "Total precipitation",
|
||||
"stat_warmest_day": "Warmest day",
|
||||
"stat_coldest_day": "Coldest day",
|
||||
"historical_daily_heading": "Daily",
|
||||
"historical_daily_hint": "– select a day for hourly detail",
|
||||
"historical_full_range": "– full range",
|
||||
"historical_from": "From",
|
||||
"historical_to": "To",
|
||||
"historical_quick_ranges": "Quick ranges",
|
||||
"historical_last_days": "{days} days",
|
||||
"historical_month_last_year": "Month, last year",
|
||||
"label_day": "day",
|
||||
"label_night": "night",
|
||||
"daycards_past": "Past 3 days",
|
||||
"daycards_load_15": "Load 15 days",
|
||||
"error_technical_details": "Technical details",
|
||||
"err_network_title": "Couldn't reach the weather service",
|
||||
"err_network_hint": "Check your internet connection and try again.",
|
||||
"err_nodata_title": "No data for this location with the selected model",
|
||||
"err_nodata_hint": "Regional weather models only cover their own area - \"Best match\" picks a suitable model automatically.",
|
||||
"err_rejected_title": "The weather service rejected the request",
|
||||
"err_rejected_hint": "Try different settings, or switch the model back to \"Best match\".",
|
||||
"err_generic_title": "Loading the weather data failed",
|
||||
"err_generic_hint": "Try again in a moment. If it keeps happening, switch the model to \"Best match\".",
|
||||
"maps_iframe_title": "Open-Meteo interactive map",
|
||||
"page_maps_title": "Weather map",
|
||||
"page_week_title": "Weather",
|
||||
"search_favorite_add": "Add to favorites",
|
||||
"search_favorite_remove": "Remove from favorites"
|
||||
}
|
||||
|
||||
+118
-1
@@ -207,5 +207,122 @@
|
||||
"no_data_title": "Este modelo no tiene datos aquí",
|
||||
"no_data_body": "El modelo seleccionado no cubre {location}: los modelos regionales solo ofrecen datos dentro de su propia área.",
|
||||
"no_data_try_city": "Cambiar la ubicación a {city}",
|
||||
"no_data_best_match": "Cambiar a «Best match»"
|
||||
"no_data_best_match": "Cambiar a «Best match»",
|
||||
"model_archive": "Reanálisis",
|
||||
"model_seasonal": "Modelo estacional",
|
||||
"nearby_cities_title": "Ciudades cercanas",
|
||||
"nearby_cities_subtitle": "Máximas y mínimas del día seleccionado",
|
||||
"supporter_dialog_title": "Colaborador de Drizz.li",
|
||||
"supporter_dialog_desc": "Pega la clave de acceso de tu correo de colaborador para desbloquear los extras.",
|
||||
"supporter_extras_active": "Los extras de colaborador están activos",
|
||||
"supporter_lifetime": "Acceso de por vida",
|
||||
"supporter_active_until": "Activo hasta el {date}",
|
||||
"supporter_access_key": "Clave de acceso",
|
||||
"supporter_key_invalid": "Esa clave no es válida o ha caducado.",
|
||||
"supporter_server_unreachable": "No se pudo conectar con el servidor. Comprueba tu conexión e inténtalo de nuevo.",
|
||||
"supporter_verifying": "Verificando…",
|
||||
"supporter_unlock_button": "Desbloquear",
|
||||
"supporter_remove_key": "Quitar la clave de este dispositivo",
|
||||
"supporter_no_key_yet": "¿Aún no tienes clave?",
|
||||
"supporter_support_from": "Apoya el proyecto desde {price}",
|
||||
"supporter_checking": "Comprobando tu suscripción…",
|
||||
"supporter_gate_title": "{feature} es un extra para colaboradores",
|
||||
"supporter_gate_body": "Drizz.li es gratuito y de código abierto. Aporta desde {price} para mantenerlo en marcha; como agradecimiento, los colaboradores desbloquean los extras.",
|
||||
"supporter_become": "Hazte colaborador",
|
||||
"supporter_have_key": "Ya tengo una clave",
|
||||
"supporter_key_expired": "Tu clave guardada ya no es válida o ha caducado.",
|
||||
"supporter_enter_key": "Introduce tu clave de acceso.",
|
||||
"supporter_perk_historical": "Clima histórico y comparación con las normales climáticas",
|
||||
"supporter_perk_seasonal": "Perspectiva estacional: los próximos meses frente a la normal climática",
|
||||
"supporter_perk_future": "Nuevos extras para colaboradores en cuanto lleguen",
|
||||
"action_close": "Cerrar",
|
||||
"action_done": "Listo",
|
||||
"action_try_again": "Reintentar",
|
||||
"action_reset_defaults": "Restablecer valores por defecto",
|
||||
"customize_meteograms": "Personalizar meteogramas",
|
||||
"customizer_intro": "Arrastra variables entre gráficos para crear tu propio diseño.",
|
||||
"customizer_chart_n": "Gráfico {number}",
|
||||
"customizer_delete_chart": "Eliminar el gráfico {number}",
|
||||
"customizer_add_chart": "+ Añadir gráfico",
|
||||
"customizer_available": "Variables disponibles",
|
||||
"customizer_drag": "Arrastrar {variable}",
|
||||
"customizer_remove": "Quitar {variable}",
|
||||
"customizer_drop_here": "Suelta variables aquí",
|
||||
"customizer_all_in_use": "Todas las variables están en uso",
|
||||
"variables_aria": "Selección de variables",
|
||||
"variables_close": "Cerrar la selección de variables",
|
||||
"variables_table_section": "Tabla horaria",
|
||||
"variables_move_up": "Subir {variable}",
|
||||
"variables_move_down": "Bajar {variable}",
|
||||
"variables_charts_hint_before": "Las variables del meteograma se configuran con el botón",
|
||||
"variables_charts_hint_after": "situado encima de los gráficos.",
|
||||
"table_customize": "Personalizar variables",
|
||||
"table_interval_aria": "Intervalo horario",
|
||||
"table_now": "Ahora",
|
||||
"interval_toggle": "Alternar entre intervalos de 1 y 3 horas",
|
||||
"charts_loading": "Cargando gráficos…",
|
||||
"chart_download": "Descargar el meteograma como imagen PNG",
|
||||
"chart_credit_viz": "visualización de",
|
||||
"chart_toggle_series": "Mostrar u ocultar {series}",
|
||||
"legend_show": "Mostrar leyenda",
|
||||
"meteograms_none_before": "No hay meteogramas configurados,",
|
||||
"meteograms_none_action": "añade algunas variables",
|
||||
"meteograms_none_historical": "No hay meteogramas configurados. Añade variables desde la página de 7 días.",
|
||||
"search_searching": "Buscando…",
|
||||
"search_favorites": "Favoritos",
|
||||
"search_recent": "Recientes",
|
||||
"search_hint": "Empieza a escribir o usa el GPS para detectar tu posición",
|
||||
"search_no_results": "No se encontraron ubicaciones",
|
||||
"search_remove_recent": "Quitar {location} de las ubicaciones recientes",
|
||||
"search_remove_recent_short": "Quitar de recientes",
|
||||
"search_aria": "Buscar ubicación",
|
||||
"search_gps": "Usar la ubicación GPS",
|
||||
"model_automatic_selection": "Selección automática",
|
||||
"model_selector_aria": "Selección de {label}",
|
||||
"compare_models_heading": "Modelos",
|
||||
"compare_variables_heading": "Variables meteorológicas horarias",
|
||||
"compare_timeline_title": "Cronología de comparación de modelos",
|
||||
"compare_model_label": "Modelo",
|
||||
"ensemble_trimmed": "El ensemble de este modelo solo llega unos {days} días, así que la dispersión se recorta a su rango disponible.",
|
||||
"seasonal_explainer_before": "Una previsión estacional muestra cuánto es probable que todo un mes",
|
||||
"seasonal_explainer_strong": "se desvíe de su normal climática",
|
||||
"seasonal_explainer_after": "- no el tiempo de un día concreto. Fíjate en la tendencia mensual y en el acuerdo del ensemble, no en los vaivenes diarios.",
|
||||
"seasonal_runs_to": "Esta perspectiva llega hasta el {date}.",
|
||||
"seasonal_range_aria": "Periodo de la perspectiva",
|
||||
"seasonal_no_normal": "sin normal para este mes",
|
||||
"normals_loading": "cargando la normal…",
|
||||
"normals_loading_period": "cargando la normal 1991–2020…",
|
||||
"normal_band_legend": "banda normal = media 1991–2020",
|
||||
"anomaly_vs_normal": "{value} respecto a la normal",
|
||||
"stat_avg_temperature": "Temperatura media",
|
||||
"stat_mean_temperature": "Temperatura media",
|
||||
"stat_total_precipitation": "Precipitación total",
|
||||
"stat_warmest_day": "Día más cálido",
|
||||
"stat_coldest_day": "Día más frío",
|
||||
"historical_daily_heading": "Diario",
|
||||
"historical_daily_hint": "– selecciona un día para el detalle horario",
|
||||
"historical_full_range": "– rango completo",
|
||||
"historical_from": "Desde",
|
||||
"historical_to": "Hasta",
|
||||
"historical_quick_ranges": "Rangos rápidos",
|
||||
"historical_last_days": "{days} días",
|
||||
"historical_month_last_year": "Este mes, el año pasado",
|
||||
"label_day": "día",
|
||||
"label_night": "noche",
|
||||
"daycards_past": "3 días pasados",
|
||||
"daycards_load_15": "Cargar 15 días",
|
||||
"error_technical_details": "Detalles técnicos",
|
||||
"err_network_title": "No se pudo conectar con el servicio meteorológico",
|
||||
"err_network_hint": "Comprueba tu conexión a internet e inténtalo de nuevo.",
|
||||
"err_nodata_title": "No hay datos para esta ubicación con el modelo seleccionado",
|
||||
"err_nodata_hint": "Los modelos regionales solo cubren su propia área - \"Best match\" elige automáticamente un modelo adecuado.",
|
||||
"err_rejected_title": "El servicio meteorológico rechazó la solicitud",
|
||||
"err_rejected_hint": "Prueba otros ajustes o vuelve al modelo \"Best match\".",
|
||||
"err_generic_title": "No se pudieron cargar los datos meteorológicos",
|
||||
"err_generic_hint": "Vuelve a intentarlo en un momento. Si sigue ocurriendo, cambia el modelo a \"Best match\".",
|
||||
"maps_iframe_title": "Mapa interactivo de Open-Meteo",
|
||||
"page_maps_title": "Mapa meteorológico",
|
||||
"page_week_title": "El tiempo",
|
||||
"search_favorite_add": "Añadir a favoritos",
|
||||
"search_favorite_remove": "Quitar de favoritos"
|
||||
}
|
||||
|
||||
+118
-1
@@ -207,5 +207,122 @@
|
||||
"no_data_title": "Ce modèle n'a pas de données ici",
|
||||
"no_data_body": "Le modèle sélectionné ne couvre pas {location} : les modèles régionaux ne fournissent des données que dans leur propre zone.",
|
||||
"no_data_try_city": "Changer de lieu pour {city}",
|
||||
"no_data_best_match": "Passer à « Best match »"
|
||||
"no_data_best_match": "Passer à « Best match »",
|
||||
"model_archive": "Réanalyse",
|
||||
"model_seasonal": "Modèle saisonnier",
|
||||
"nearby_cities_title": "Villes à proximité",
|
||||
"nearby_cities_subtitle": "Maximales et minimales du jour sélectionné",
|
||||
"supporter_dialog_title": "Soutien Drizz.li",
|
||||
"supporter_dialog_desc": "Collez la clé d'accès reçue par e-mail pour débloquer les bonus.",
|
||||
"supporter_extras_active": "Les bonus contributeur sont actifs",
|
||||
"supporter_lifetime": "Accès à vie",
|
||||
"supporter_active_until": "Actif jusqu'au {date}",
|
||||
"supporter_access_key": "Clé d'accès",
|
||||
"supporter_key_invalid": "Cette clé n'est pas valide ou a expiré.",
|
||||
"supporter_server_unreachable": "Serveur injoignable. Vérifiez votre connexion et réessayez.",
|
||||
"supporter_verifying": "Vérification…",
|
||||
"supporter_unlock_button": "Débloquer",
|
||||
"supporter_remove_key": "Supprimer la clé de cet appareil",
|
||||
"supporter_no_key_yet": "Pas encore de clé ?",
|
||||
"supporter_support_from": "Soutenez le projet à partir de {price}",
|
||||
"supporter_checking": "Vérification de votre abonnement…",
|
||||
"supporter_gate_title": "{feature} est un bonus contributeur",
|
||||
"supporter_gate_body": "Drizz.li est gratuit et open source. Contribuez à partir de {price} pour le faire vivre - en remerciement, les contributeurs débloquent les bonus.",
|
||||
"supporter_become": "Devenir contributeur",
|
||||
"supporter_have_key": "J'ai une clé",
|
||||
"supporter_key_expired": "Votre clé enregistrée n'est plus valide ou a expiré.",
|
||||
"supporter_enter_key": "Saisissez votre clé d'accès.",
|
||||
"supporter_perk_historical": "Météo historique et comparaison aux normales climatiques",
|
||||
"supporter_perk_seasonal": "Aperçu saisonnier : les mois à venir face à la normale climatique",
|
||||
"supporter_perk_future": "Les nouveaux bonus dès leur sortie",
|
||||
"action_close": "Fermer",
|
||||
"action_done": "Terminé",
|
||||
"action_try_again": "Réessayer",
|
||||
"action_reset_defaults": "Rétablir les valeurs par défaut",
|
||||
"customize_meteograms": "Personnaliser les météogrammes",
|
||||
"customizer_intro": "Faites glisser les variables d'un graphique à l'autre pour composer votre mise en page.",
|
||||
"customizer_chart_n": "Graphique {number}",
|
||||
"customizer_delete_chart": "Supprimer le graphique {number}",
|
||||
"customizer_add_chart": "+ Ajouter un graphique",
|
||||
"customizer_available": "Variables disponibles",
|
||||
"customizer_drag": "Déplacer {variable}",
|
||||
"customizer_remove": "Retirer {variable}",
|
||||
"customizer_drop_here": "Déposez des variables ici",
|
||||
"customizer_all_in_use": "Toutes les variables sont utilisées",
|
||||
"variables_aria": "Sélection des variables",
|
||||
"variables_close": "Fermer la sélection des variables",
|
||||
"variables_table_section": "Tableau horaire",
|
||||
"variables_move_up": "Déplacer {variable} vers le haut",
|
||||
"variables_move_down": "Déplacer {variable} vers le bas",
|
||||
"variables_charts_hint_before": "Les variables des météogrammes se règlent avec le bouton",
|
||||
"variables_charts_hint_after": "au-dessus des graphiques.",
|
||||
"table_customize": "Personnaliser les variables",
|
||||
"table_interval_aria": "Intervalle horaire",
|
||||
"table_now": "Maintenant",
|
||||
"interval_toggle": "Basculer entre les intervalles de 1 h et 3 h",
|
||||
"charts_loading": "Chargement des graphiques…",
|
||||
"chart_download": "Télécharger le météogramme en PNG",
|
||||
"chart_credit_viz": "visualisation par",
|
||||
"chart_toggle_series": "Afficher ou masquer {series}",
|
||||
"legend_show": "Afficher la légende",
|
||||
"meteograms_none_before": "Aucun météogramme configuré,",
|
||||
"meteograms_none_action": "ajoutez des variables",
|
||||
"meteograms_none_historical": "Aucun météogramme configuré. Ajoutez des variables depuis la page 7 jours.",
|
||||
"search_searching": "Recherche…",
|
||||
"search_favorites": "Favoris",
|
||||
"search_recent": "Récents",
|
||||
"search_hint": "Commencez à taper ou utilisez le GPS pour détecter votre position",
|
||||
"search_no_results": "Aucun lieu trouvé",
|
||||
"search_remove_recent": "Retirer {location} des lieux récents",
|
||||
"search_remove_recent_short": "Retirer des récents",
|
||||
"search_aria": "Rechercher un lieu",
|
||||
"search_gps": "Utiliser la position GPS",
|
||||
"model_automatic_selection": "Sélection automatique",
|
||||
"model_selector_aria": "Sélection : {label}",
|
||||
"compare_models_heading": "Modèles",
|
||||
"compare_variables_heading": "Variables météo horaires",
|
||||
"compare_timeline_title": "Chronologie de comparaison des modèles",
|
||||
"compare_model_label": "Modèle",
|
||||
"ensemble_trimmed": "L'ensemble de ce modèle ne va qu'à environ {days} jours, la dispersion est donc limitée à sa portée disponible.",
|
||||
"seasonal_explainer_before": "Une prévision saisonnière montre dans quelle mesure un mois entier devrait",
|
||||
"seasonal_explainer_strong": "s'écarter de sa normale climatique",
|
||||
"seasonal_explainer_after": "- et non le temps qu'il fera un jour donné. Lisez la tendance mensuelle et l'accord de l'ensemble, pas les variations quotidiennes.",
|
||||
"seasonal_runs_to": "Cet aperçu va jusqu'au {date}.",
|
||||
"seasonal_range_aria": "Période de l'aperçu",
|
||||
"seasonal_no_normal": "pas de normale pour ce mois",
|
||||
"normals_loading": "chargement de la normale…",
|
||||
"normals_loading_period": "chargement de la normale 1991–2020…",
|
||||
"normal_band_legend": "bande normale = moyenne 1991–2020",
|
||||
"anomaly_vs_normal": "{value} par rapport à la normale",
|
||||
"stat_avg_temperature": "Température moyenne",
|
||||
"stat_mean_temperature": "Température moyenne",
|
||||
"stat_total_precipitation": "Précipitations totales",
|
||||
"stat_warmest_day": "Jour le plus chaud",
|
||||
"stat_coldest_day": "Jour le plus froid",
|
||||
"historical_daily_heading": "Quotidien",
|
||||
"historical_daily_hint": "– sélectionnez un jour pour le détail horaire",
|
||||
"historical_full_range": "– période complète",
|
||||
"historical_from": "Du",
|
||||
"historical_to": "Au",
|
||||
"historical_quick_ranges": "Plages rapides",
|
||||
"historical_last_days": "{days} jours",
|
||||
"historical_month_last_year": "Ce mois, l'an dernier",
|
||||
"label_day": "jour",
|
||||
"label_night": "nuit",
|
||||
"daycards_past": "3 jours passés",
|
||||
"daycards_load_15": "Charger 15 jours",
|
||||
"error_technical_details": "Détails techniques",
|
||||
"err_network_title": "Service météo injoignable",
|
||||
"err_network_hint": "Vérifiez votre connexion internet et réessayez.",
|
||||
"err_nodata_title": "Aucune donnée pour ce lieu avec le modèle sélectionné",
|
||||
"err_nodata_hint": "Les modèles régionaux ne couvrent que leur propre zone - \"Best match\" choisit automatiquement un modèle adapté.",
|
||||
"err_rejected_title": "Le service météo a refusé la requête",
|
||||
"err_rejected_hint": "Essayez d'autres réglages ou revenez au modèle \"Best match\".",
|
||||
"err_generic_title": "Le chargement des données météo a échoué",
|
||||
"err_generic_hint": "Réessayez dans un instant. Si cela persiste, revenez au modèle \"Best match\".",
|
||||
"maps_iframe_title": "Carte interactive Open-Meteo",
|
||||
"page_maps_title": "Carte météo",
|
||||
"page_week_title": "Météo",
|
||||
"search_favorite_add": "Ajouter aux favoris",
|
||||
"search_favorite_remove": "Retirer des favoris"
|
||||
}
|
||||
|
||||
+118
-1
@@ -207,5 +207,122 @@
|
||||
"no_data_title": "Nessun dato per questo modello qui",
|
||||
"no_data_body": "Il modello selezionato non copre {location}: i modelli regionali forniscono dati solo nella propria area.",
|
||||
"no_data_try_city": "Cambia località in {city}",
|
||||
"no_data_best_match": "Passa a «Best match»"
|
||||
"no_data_best_match": "Passa a «Best match»",
|
||||
"model_archive": "Rianalisi",
|
||||
"model_seasonal": "Modello stagionale",
|
||||
"nearby_cities_title": "Città nelle vicinanze",
|
||||
"nearby_cities_subtitle": "Massime e minime del giorno selezionato",
|
||||
"supporter_dialog_title": "Sostenitore Drizz.li",
|
||||
"supporter_dialog_desc": "Incolla la chiave di accesso ricevuta via e-mail per sbloccare gli extra.",
|
||||
"supporter_extras_active": "Gli extra per sostenitori sono attivi",
|
||||
"supporter_lifetime": "Accesso a vita",
|
||||
"supporter_active_until": "Attivo fino al {date}",
|
||||
"supporter_access_key": "Chiave di accesso",
|
||||
"supporter_key_invalid": "Questa chiave non è valida o è scaduta.",
|
||||
"supporter_server_unreachable": "Server non raggiungibile. Controlla la connessione e riprova.",
|
||||
"supporter_verifying": "Verifica in corso…",
|
||||
"supporter_unlock_button": "Sblocca",
|
||||
"supporter_remove_key": "Rimuovi la chiave da questo dispositivo",
|
||||
"supporter_no_key_yet": "Non hai ancora una chiave?",
|
||||
"supporter_support_from": "Sostieni il progetto da {price}",
|
||||
"supporter_checking": "Verifica dell'abbonamento…",
|
||||
"supporter_gate_title": "{feature} è un extra per sostenitori",
|
||||
"supporter_gate_body": "Drizz.li è gratuito e open source. Contribuisci da {price} per tenerlo in vita - come ringraziamento, i sostenitori sbloccano gli extra.",
|
||||
"supporter_become": "Diventa sostenitore",
|
||||
"supporter_have_key": "Ho una chiave",
|
||||
"supporter_key_expired": "La chiave salvata non è più valida o è scaduta.",
|
||||
"supporter_enter_key": "Inserisci la tua chiave di accesso.",
|
||||
"supporter_perk_historical": "Meteo storico e confronto con le normali climatiche",
|
||||
"supporter_perk_seasonal": "Prospettive stagionali: i mesi a venire rispetto alla normale climatica",
|
||||
"supporter_perk_future": "Nuovi extra non appena arrivano",
|
||||
"action_close": "Chiudi",
|
||||
"action_done": "Fatto",
|
||||
"action_try_again": "Riprova",
|
||||
"action_reset_defaults": "Ripristina i valori predefiniti",
|
||||
"customize_meteograms": "Personalizza i meteogrammi",
|
||||
"customizer_intro": "Trascina le variabili tra i grafici per costruire il tuo layout.",
|
||||
"customizer_chart_n": "Grafico {number}",
|
||||
"customizer_delete_chart": "Elimina il grafico {number}",
|
||||
"customizer_add_chart": "+ Aggiungi grafico",
|
||||
"customizer_available": "Variabili disponibili",
|
||||
"customizer_drag": "Trascina {variable}",
|
||||
"customizer_remove": "Rimuovi {variable}",
|
||||
"customizer_drop_here": "Trascina qui le variabili",
|
||||
"customizer_all_in_use": "Tutte le variabili sono in uso",
|
||||
"variables_aria": "Selezione delle variabili",
|
||||
"variables_close": "Chiudi la selezione delle variabili",
|
||||
"variables_table_section": "Tabella oraria",
|
||||
"variables_move_up": "Sposta {variable} in alto",
|
||||
"variables_move_down": "Sposta {variable} in basso",
|
||||
"variables_charts_hint_before": "Le variabili dei meteogrammi si impostano con il pulsante",
|
||||
"variables_charts_hint_after": "sopra i grafici.",
|
||||
"table_customize": "Personalizza le variabili",
|
||||
"table_interval_aria": "Intervallo orario",
|
||||
"table_now": "Ora",
|
||||
"interval_toggle": "Alterna tra intervalli di 1 e 3 ore",
|
||||
"charts_loading": "Caricamento dei grafici…",
|
||||
"chart_download": "Scarica il meteogramma come immagine PNG",
|
||||
"chart_credit_viz": "visualizzazione di",
|
||||
"chart_toggle_series": "Mostra o nascondi {series}",
|
||||
"legend_show": "Mostra legenda",
|
||||
"meteograms_none_before": "Nessun meteogramma configurato,",
|
||||
"meteograms_none_action": "aggiungi qualche variabile",
|
||||
"meteograms_none_historical": "Nessun meteogramma configurato. Aggiungi variabili dalla pagina a 7 giorni.",
|
||||
"search_searching": "Ricerca in corso…",
|
||||
"search_favorites": "Preferiti",
|
||||
"search_recent": "Recenti",
|
||||
"search_hint": "Inizia a digitare o usa il GPS per rilevare la tua posizione",
|
||||
"search_no_results": "Nessuna località trovata",
|
||||
"search_remove_recent": "Rimuovi {location} dai luoghi recenti",
|
||||
"search_remove_recent_short": "Rimuovi dai recenti",
|
||||
"search_aria": "Cerca una località",
|
||||
"search_gps": "Usa la posizione GPS",
|
||||
"model_automatic_selection": "Selezione automatica",
|
||||
"model_selector_aria": "Selezione di {label}",
|
||||
"compare_models_heading": "Modelli",
|
||||
"compare_variables_heading": "Variabili meteo orarie",
|
||||
"compare_timeline_title": "Cronologia del confronto tra modelli",
|
||||
"compare_model_label": "Modello",
|
||||
"ensemble_trimmed": "L'ensemble di questo modello arriva solo a circa {days} giorni, quindi la dispersione è ridotta al periodo disponibile.",
|
||||
"seasonal_explainer_before": "Una previsione stagionale mostra quanto un intero mese potrebbe",
|
||||
"seasonal_explainer_strong": "discostarsi dalla sua normale climatica",
|
||||
"seasonal_explainer_after": "- non il tempo di un singolo giorno. Guarda la tendenza mensile e l'accordo dell'ensemble, non gli sbalzi giornalieri.",
|
||||
"seasonal_runs_to": "Queste prospettive arrivano fino al {date}.",
|
||||
"seasonal_range_aria": "Periodo delle prospettive",
|
||||
"seasonal_no_normal": "nessuna normale per questo mese",
|
||||
"normals_loading": "caricamento della normale…",
|
||||
"normals_loading_period": "caricamento della normale 1991–2020…",
|
||||
"normal_band_legend": "banda normale = media 1991–2020",
|
||||
"anomaly_vs_normal": "{value} rispetto alla normale",
|
||||
"stat_avg_temperature": "Temperatura media",
|
||||
"stat_mean_temperature": "Temperatura media",
|
||||
"stat_total_precipitation": "Precipitazioni totali",
|
||||
"stat_warmest_day": "Giorno più caldo",
|
||||
"stat_coldest_day": "Giorno più freddo",
|
||||
"historical_daily_heading": "Giornaliero",
|
||||
"historical_daily_hint": "– seleziona un giorno per il dettaglio orario",
|
||||
"historical_full_range": "– intervallo completo",
|
||||
"historical_from": "Dal",
|
||||
"historical_to": "Al",
|
||||
"historical_quick_ranges": "Intervalli rapidi",
|
||||
"historical_last_days": "{days} giorni",
|
||||
"historical_month_last_year": "Questo mese, l'anno scorso",
|
||||
"label_day": "giorno",
|
||||
"label_night": "notte",
|
||||
"daycards_past": "Ultimi 3 giorni",
|
||||
"daycards_load_15": "Carica 15 giorni",
|
||||
"error_technical_details": "Dettagli tecnici",
|
||||
"err_network_title": "Servizio meteo non raggiungibile",
|
||||
"err_network_hint": "Controlla la connessione a internet e riprova.",
|
||||
"err_nodata_title": "Nessun dato per questa località con il modello selezionato",
|
||||
"err_nodata_hint": "I modelli regionali coprono solo la propria area - \"Best match\" sceglie automaticamente un modello adatto.",
|
||||
"err_rejected_title": "Il servizio meteo ha rifiutato la richiesta",
|
||||
"err_rejected_hint": "Prova impostazioni diverse o torna al modello \"Best match\".",
|
||||
"err_generic_title": "Caricamento dei dati meteo non riuscito",
|
||||
"err_generic_hint": "Riprova tra un momento. Se continua, passa al modello \"Best match\".",
|
||||
"maps_iframe_title": "Mappa interattiva di Open-Meteo",
|
||||
"page_maps_title": "Mappa meteo",
|
||||
"page_week_title": "Meteo",
|
||||
"search_favorite_add": "Aggiungi ai preferiti",
|
||||
"search_favorite_remove": "Rimuovi dai preferiti"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Regenerates static/data/cities/ - the city lists the "nearby cities" section
|
||||
* on the week page picks from.
|
||||
*
|
||||
* Source: GeoNames (CC BY 4.0) via the cities500 mirror.
|
||||
*
|
||||
* The full list of towns down to 20k inhabitants is ~1 MB, far too much to ship
|
||||
* to a phone for one section, so it is cut into 10x10 degree tiles. A tile
|
||||
* holds every town within NEAR_RADIUS_KM of anywhere inside it, plus the larger
|
||||
* cities within FAR_RADIUS_KM - so one small fetch answers both "what is around
|
||||
* the corner" and "what is the nearest place you would recognise" (which is all
|
||||
* a location in, say, the Australian outback can offer). Tiles overlap by
|
||||
* design; duplicating a few rows is cheaper than a second round trip.
|
||||
*
|
||||
* The ids are GeoNames ids, which is what Open-Meteo's geocoding API returns,
|
||||
* so a row can be turned into a location route without another lookup.
|
||||
*
|
||||
* Run with: node scripts/build-cities.mjs
|
||||
*/
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const SOURCE = 'https://raw.githubusercontent.com/lmfmaier/cities-json/master/cities500.json';
|
||||
|
||||
const MIN_POPULATION = 20_000;
|
||||
/** every city of any size within this range of the tile */
|
||||
const NEAR_RADIUS_KM = 400;
|
||||
/** only cities above FAR_MIN_POPULATION out to here */
|
||||
const FAR_RADIUS_KM = 1500;
|
||||
const FAR_MIN_POPULATION = 300_000;
|
||||
const TILE_DEGREES = 10;
|
||||
|
||||
const outDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'static', 'data', 'cities');
|
||||
|
||||
// CITIES_SOURCE_FILE lets you build from an already-downloaded copy, which is
|
||||
// also the only way to run this behind a proxy that node doesn't pick up.
|
||||
const localSource = process.env.CITIES_SOURCE_FILE;
|
||||
const source = localSource
|
||||
? JSON.parse(await readFile(localSource, 'utf8'))
|
||||
: await fetch(SOURCE).then((res) => {
|
||||
if (!res.ok) throw new Error(`source fetch failed: ${res.status}`);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
const cities = source
|
||||
.map((c) => ({
|
||||
id: Number(c.id),
|
||||
name: c.name,
|
||||
country: c.country,
|
||||
lat: Number(c.lat),
|
||||
lon: Number(c.lon),
|
||||
pop: Number(c.pop ?? 0)
|
||||
}))
|
||||
.filter((c) => Number.isFinite(c.lat) && Number.isFinite(c.lon) && c.pop >= MIN_POPULATION)
|
||||
// biggest first, so a tile that gets truncated keeps the recognisable names
|
||||
.sort((a, b) => b.pop - a.pop);
|
||||
|
||||
const toRad = Math.PI / 180;
|
||||
|
||||
/** Distance from a point to the nearest point of a lat/lon rectangle. */
|
||||
function distanceToTileKm(lat, lon, latMin, latMax, lonMin, lonMax) {
|
||||
const clampedLat = Math.min(latMax, Math.max(latMin, lat));
|
||||
// longitude distance shrinks towards the poles, measured at the closest latitude
|
||||
let dLon = 0;
|
||||
if (lon < lonMin) dLon = lonMin - lon;
|
||||
else if (lon > lonMax) dLon = lon - lonMax;
|
||||
if (dLon > 180) dLon = 360 - dLon;
|
||||
|
||||
const dLat = Math.abs(clampedLat - lat);
|
||||
const lonKm = dLon * 111.32 * Math.cos(clampedLat * toRad);
|
||||
return Math.hypot(dLat * 111.32, lonKm);
|
||||
}
|
||||
|
||||
const tiles = new Map();
|
||||
for (let latIndex = 0; latIndex < 180 / TILE_DEGREES; latIndex++) {
|
||||
for (let lonIndex = 0; lonIndex < 360 / TILE_DEGREES; lonIndex++) {
|
||||
const latMin = -90 + latIndex * TILE_DEGREES;
|
||||
const lonMin = -180 + lonIndex * TILE_DEGREES;
|
||||
const bounds = [latMin, latMin + TILE_DEGREES, lonMin, lonMin + TILE_DEGREES];
|
||||
|
||||
const rows = [];
|
||||
for (const c of cities) {
|
||||
const dist = distanceToTileKm(c.lat, c.lon, ...bounds);
|
||||
const inRange =
|
||||
dist <= NEAR_RADIUS_KM || (c.pop >= FAR_MIN_POPULATION && dist <= FAR_RADIUS_KM);
|
||||
if (!inRange) continue;
|
||||
// tuples, not objects: same data, roughly half the bytes
|
||||
rows.push([
|
||||
c.id,
|
||||
c.name,
|
||||
c.country,
|
||||
+c.lat.toFixed(3),
|
||||
+c.lon.toFixed(3),
|
||||
Math.round(c.pop / 1000)
|
||||
]);
|
||||
}
|
||||
if (rows.length > 0) tiles.set(`${latIndex}_${lonIndex}`, rows);
|
||||
}
|
||||
}
|
||||
|
||||
await rm(outDir, { recursive: true, force: true });
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
let bytes = 0;
|
||||
for (const [key, rows] of tiles) {
|
||||
const json = JSON.stringify(rows);
|
||||
bytes += json.length;
|
||||
await writeFile(join(outDir, `${key}.json`), json, 'utf8');
|
||||
}
|
||||
// the index tells the client which tiles exist, so an empty ocean tile is a
|
||||
// no-op instead of a 404
|
||||
await writeFile(join(outDir, 'index.json'), JSON.stringify([...tiles.keys()]), 'utf8');
|
||||
|
||||
const largest = Math.max(...[...tiles.values()].map((r) => r.length));
|
||||
console.log(
|
||||
`wrote ${tiles.size} tiles (${(bytes / 1024 / 1024).toFixed(1)} MB total, ` +
|
||||
`avg ${Math.round(bytes / tiles.size / 1024)} KB, largest ${largest} cities) to ${outDir}`
|
||||
);
|
||||
@@ -18,6 +18,8 @@
|
||||
/>
|
||||
-->
|
||||
<script module lang="ts">
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
export interface ChartSeries {
|
||||
/** Series display name (used in legend and tooltip) */
|
||||
name: string;
|
||||
@@ -698,7 +700,11 @@
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillStyle = muted;
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillText('Weather data by Open-Meteo · visualisation by Drizz.li', width - 6, totalH - 5);
|
||||
ctx.fillText(
|
||||
`${m.footer_data_by()} Open-Meteo · ${m.chart_credit_viz()} Drizz.li`,
|
||||
width - 6,
|
||||
totalH - 5
|
||||
);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
@@ -1764,7 +1770,7 @@
|
||||
? 'opacity-40'
|
||||
: ''}"
|
||||
onclick={() => toggleSeries(s.name)}
|
||||
title="Toggle {s.name}"
|
||||
title={m.chart_toggle_series({ series: s.name })}
|
||||
>
|
||||
<span
|
||||
class="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
@@ -1783,12 +1789,13 @@
|
||||
<div
|
||||
class="px-2 pt-2 pb-1 text-center md:text-right text-[10px] leading-none text-muted-foreground/70"
|
||||
>
|
||||
Weather data by <a
|
||||
{m.footer_data_by()}
|
||||
<a
|
||||
class="font-medium underline-offset-2 hover:text-foreground hover:underline"
|
||||
href="https://open-meteo.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Open-Meteo</a
|
||||
>, visualisation by
|
||||
>, {m.chart_credit_viz()}
|
||||
<a
|
||||
class="font-medium underline-offset-2 hover:text-foreground hover:underline"
|
||||
href="https://drizz.li"
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────────────────
|
||||
@@ -93,7 +95,7 @@
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading charts...</span>
|
||||
<span class="sr-only">{m.charts_loading()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
</ChartToolbar>
|
||||
-->
|
||||
<script module lang="ts">
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
export type { ExportableChart } from './downloadChartsPng';
|
||||
</script>
|
||||
|
||||
@@ -87,7 +89,7 @@
|
||||
class="toolbar-btn"
|
||||
disabled={!hasCharts || downloading}
|
||||
onclick={handleDownload}
|
||||
title="Download meteogram as PNG image"
|
||||
title={m.chart_download()}
|
||||
>
|
||||
{#if downloading}
|
||||
<svg
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
export let label: string = 'Search location...';
|
||||
export let placeholder: string = 'Enter city name...';
|
||||
|
||||
@@ -161,8 +163,8 @@
|
||||
? 'text-amber-500'
|
||||
: 'text-muted-foreground/50'}"
|
||||
onclick={() => toggleFavorite(location)}
|
||||
aria-label={fav ? 'Remove from favorites' : 'Add to favorites'}
|
||||
title={fav ? 'Remove from favorites' : 'Add to favorites'}
|
||||
aria-label={fav ? m.search_favorite_remove() : m.search_favorite_add()}
|
||||
title={fav ? m.search_favorite_remove() : m.search_favorite_add()}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -182,8 +184,8 @@
|
||||
<button
|
||||
class="mr-1 flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/50 hover:bg-background hover:text-destructive"
|
||||
onclick={() => removeRecent(location)}
|
||||
aria-label="Remove {location.name} from recent locations"
|
||||
title="Remove from recent"
|
||||
aria-label={m.search_remove_recent({ location: location.name })}
|
||||
title={m.search_remove_recent_short()}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" d="M6 6l12 12M18 6L6 18" />
|
||||
@@ -231,7 +233,7 @@
|
||||
class="h-9"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-label="Search Location"
|
||||
aria-label={m.search_aria()}
|
||||
bind:value={searchQuery}
|
||||
bind:ref={searchInputEl}
|
||||
/>
|
||||
@@ -240,7 +242,7 @@
|
||||
variant="outline"
|
||||
size="default"
|
||||
class="h-9 px-2.5"
|
||||
title="Use GPS Location"
|
||||
title={m.search_gps()}
|
||||
onclick={() => (searchQuery = 'GPS')}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -266,7 +268,7 @@
|
||||
<div class="flex h-20 items-center justify-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary"></div>
|
||||
<span class="text-sm text-muted-foreground">Searching...</span>
|
||||
<span class="text-sm text-muted-foreground">{m.search_searching()}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:then results}
|
||||
@@ -276,7 +278,7 @@
|
||||
<div
|
||||
class="mb-1 px-1 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"
|
||||
>
|
||||
Favorites
|
||||
{m.search_favorites()}
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
{#each $storedFavoriteLocations as loc (locationKey(loc))}
|
||||
@@ -290,7 +292,7 @@
|
||||
? 'mt-3'
|
||||
: ''}"
|
||||
>
|
||||
Recent
|
||||
{m.search_recent()}
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
{#each recentToShow as loc (locationKey(loc))}
|
||||
@@ -316,7 +318,7 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs">
|
||||
Start typing to search or use GPS to detect your position
|
||||
{m.search_hint()}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -336,7 +338,7 @@
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<Alert.Root variant="destructive">
|
||||
<Alert.Description>No locations found</Alert.Description>
|
||||
<Alert.Description>{m.search_no_results()}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
{:catch error}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import SupporterIcon from './SupporterIcon.svelte';
|
||||
import UnlockDialog from './UnlockDialog.svelte';
|
||||
import { SIGNUP_URL, SUPPORTER_PERKS, getSupporterPrice } from './config';
|
||||
@@ -8,11 +10,11 @@
|
||||
|
||||
interface Props {
|
||||
/** Short feature name shown in the locked panel headline. */
|
||||
feature?: string;
|
||||
feature: string;
|
||||
children: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { feature = 'This page', children }: Props = $props();
|
||||
let { feature, children }: Props = $props();
|
||||
|
||||
let unlockOpen = $state(false);
|
||||
const price = getSupporterPrice();
|
||||
@@ -31,7 +33,7 @@
|
||||
<svg class="mr-2 h-4 w-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-width="2" d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
Checking your subscription…
|
||||
{m.supporter_checking()}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
@@ -43,14 +45,13 @@
|
||||
<SupporterIcon class="h-7 w-7" />
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-bold tracking-tight">{feature} is a supporter extra</h2>
|
||||
<h2 class="text-xl font-bold tracking-tight">{m.supporter_gate_title({ feature })}</h2>
|
||||
<p class="mx-auto mt-1.5 max-w-sm text-sm text-muted-foreground">
|
||||
Drizz.li is free and open source. Chip in from {price} to keep it running - as a thank-you, supporters
|
||||
unlock the extras.
|
||||
{m.supporter_gate_body({ price })}
|
||||
</p>
|
||||
|
||||
<ul class="mx-auto mt-5 grid max-w-sm gap-2 text-left">
|
||||
{#each SUPPORTER_PERKS as perk (perk)}
|
||||
{#each SUPPORTER_PERKS() as perk (perk)}
|
||||
<li class="flex items-start gap-2.5 text-sm">
|
||||
<svg
|
||||
class="mt-0.5 h-4 w-4 shrink-0 text-primary"
|
||||
@@ -73,20 +74,20 @@
|
||||
rel="noopener"
|
||||
class="inline-flex h-10 w-full items-center justify-center rounded-lg bg-primary px-5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 sm:w-auto"
|
||||
>
|
||||
Become a supporter
|
||||
{m.supporter_become()}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 w-full cursor-pointer items-center justify-center rounded-lg border border-border bg-background px-5 text-sm font-semibold text-foreground transition-colors hover:bg-muted sm:w-auto"
|
||||
onclick={() => (unlockOpen = true)}
|
||||
>
|
||||
I have a key
|
||||
{m.supporter_have_key()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if $supporterState.status === 'invalid'}
|
||||
<p class="mt-4 text-xs text-amber-600 dark:text-amber-400">
|
||||
Your saved key is no longer valid or has expired.
|
||||
{m.supporter_key_expired()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { SIGNUP_URL, getSupporterPrice } from './config';
|
||||
import {
|
||||
clearLicense,
|
||||
@@ -46,7 +48,7 @@
|
||||
// close shortly so the success state is visible for a beat
|
||||
setTimeout(() => (open = false), 700);
|
||||
} else {
|
||||
localError = result.error ?? 'That key is not valid or has expired.';
|
||||
localError = result.error ?? m.supporter_key_invalid();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +60,17 @@
|
||||
let expiresLabel = $derived.by(() => {
|
||||
const exp = $supporterState.expires;
|
||||
if ($supporterState.status !== 'valid') return null;
|
||||
if (!exp) return 'Lifetime access';
|
||||
return `Active until ${formatZoned(new Date(exp), 'UTC', 'd LLL yyyy')}`;
|
||||
if (!exp) return m.supporter_lifetime();
|
||||
return m.supporter_active_until({ date: formatZoned(new Date(exp), 'UTC', 'd LLL yyyy') });
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Drizz.li Supporter</Dialog.Title>
|
||||
<Dialog.Title>{m.supporter_dialog_title()}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Paste the access key from your supporter email to unlock the extras.
|
||||
{m.supporter_dialog_desc()}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
@@ -87,7 +89,7 @@
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-semibold">Supporter extras are active</p>
|
||||
<p class="font-semibold">{m.supporter_extras_active()}</p>
|
||||
{#if expiresLabel}<p class="text-xs opacity-80">{expiresLabel}</p>{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +97,7 @@
|
||||
|
||||
<form onsubmit={submit} class="grid gap-3">
|
||||
<div class="grid gap-1.5">
|
||||
<Label for="license-key">Access key</Label>
|
||||
<Label for="license-key">{m.supporter_access_key()}</Label>
|
||||
<Input
|
||||
id="license-key"
|
||||
bind:value={keyInput}
|
||||
@@ -110,12 +112,12 @@
|
||||
<p class="text-sm text-destructive">{localError}</p>
|
||||
{:else if $supporterState.status === 'error'}
|
||||
<p class="text-sm text-destructive">
|
||||
Couldn't reach the server. Check your connection and try again.
|
||||
{m.supporter_server_unreachable()}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<Button type="submit" disabled={submitting || !keyInput.trim()}>
|
||||
{#if submitting}Verifying…{:else}Unlock{/if}
|
||||
{#if submitting}{m.supporter_verifying()}{:else}{m.supporter_unlock_button()}{/if}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -126,18 +128,18 @@
|
||||
class="cursor-pointer text-center text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||
onclick={removeKey}
|
||||
>
|
||||
Remove key from this device
|
||||
{m.supporter_remove_key()}
|
||||
</button>
|
||||
{:else}
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
No key yet?
|
||||
{m.supporter_no_key_yet()}
|
||||
<a
|
||||
href={SIGNUP_URL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="font-semibold text-primary underline-offset-2 hover:underline"
|
||||
>
|
||||
Support the project from {price}
|
||||
{m.supporter_support_from({ price })}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
/**
|
||||
* Paywall configuration.
|
||||
*
|
||||
@@ -63,9 +65,10 @@ export function getSupporterPrice(): string {
|
||||
return currency === 'CHF' ? `${symbol} 3 / month` : `${symbol}3 / month`;
|
||||
}
|
||||
|
||||
/** Short, human list of what supporting unlocks (shown on the locked panel). */
|
||||
export const SUPPORTER_PERKS = [
|
||||
'Historical weather & climate-normal comparisons',
|
||||
'Seasonal outlook: months ahead vs the climate normal',
|
||||
'New supporter extras as they land'
|
||||
/** Short, human list of what supporting unlocks (shown on the locked panel).
|
||||
* A function, not a constant: the copy has to follow the active locale. */
|
||||
export const SUPPORTER_PERKS = (): string[] => [
|
||||
m.supporter_perk_historical(),
|
||||
m.supporter_perk_seasonal(),
|
||||
m.supporter_perk_future()
|
||||
];
|
||||
|
||||
@@ -14,6 +14,8 @@ import { derived, get, writable } from 'svelte/store';
|
||||
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { PAYWALL_API_BASE } from './config';
|
||||
|
||||
/** The supporter's access key, e.g. "DRZ-7Q2K-9F4M-XW3P". Empty when signed out. */
|
||||
@@ -99,7 +101,7 @@ export async function verifyKey(key: string): Promise<VerifyResult> {
|
||||
const trimmed = key.trim();
|
||||
if (!trimmed) {
|
||||
supporterState.set({ status: 'invalid' });
|
||||
return { valid: false, error: 'Enter your access key.' };
|
||||
return { valid: false, error: m.supporter_enter_key() };
|
||||
}
|
||||
|
||||
supporterState.set({ status: 'checking' });
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Picks a handful of cities around a location, for cross-referencing a forecast
|
||||
* against places you already have a feel for.
|
||||
*
|
||||
* Open-Meteo's geocoding API can only search by name - there is no radius or
|
||||
* reverse lookup - so the candidates come from a static GeoNames extract, cut
|
||||
* into 10x10 degree tiles (see scripts/build-cities.mjs). Looking somewhere up
|
||||
* costs one small tile fetch, which the browser then caches.
|
||||
*/
|
||||
import { base } from '$app/paths';
|
||||
|
||||
/** [geonames id, name, country code, latitude, longitude, population/1000] */
|
||||
type CityRow = [number, string, string, number, number, number];
|
||||
|
||||
export interface NearbyCity {
|
||||
id: number;
|
||||
name: string;
|
||||
countryCode: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
population: number;
|
||||
distanceKm: number;
|
||||
}
|
||||
|
||||
const EARTH_RADIUS_KM = 6371;
|
||||
const TILE_DEGREES = 10;
|
||||
|
||||
export function distanceKm(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const toRad = Math.PI / 180;
|
||||
const dLat = (lat2 - lat1) * toRad;
|
||||
const dLon = (lon2 - lon1) * toRad;
|
||||
const a =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1 * toRad) * Math.cos(lat2 * toRad) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * EARTH_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(a)));
|
||||
}
|
||||
|
||||
function tileKey(latitude: number, longitude: number): string {
|
||||
const lat = Math.min(89.999, Math.max(-90, latitude));
|
||||
const lon = ((((longitude + 180) % 360) + 360) % 360) - 180;
|
||||
return `${Math.floor((lat + 90) / TILE_DEGREES)}_${Math.floor((lon + 180) / TILE_DEGREES)}`;
|
||||
}
|
||||
|
||||
const tileCache = new Map<string, Promise<CityRow[]>>();
|
||||
let indexPromise: Promise<Set<string>> | null = null;
|
||||
|
||||
async function fetchJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${base}/data/cities/${path}`);
|
||||
if (!res.ok) throw new Error(`${path}: ${res.status}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/** Tiles exist only where there are cities, so the index turns an empty ocean
|
||||
* tile into a no-op instead of a 404. */
|
||||
function loadIndex(): Promise<Set<string>> {
|
||||
indexPromise ??= fetchJson<string[]>('index.json')
|
||||
.then((keys) => new Set(keys))
|
||||
.catch((err) => {
|
||||
indexPromise = null;
|
||||
throw err;
|
||||
});
|
||||
return indexPromise;
|
||||
}
|
||||
|
||||
async function loadTile(key: string): Promise<CityRow[]> {
|
||||
if (!(await loadIndex()).has(key)) return [];
|
||||
|
||||
let tile = tileCache.get(key);
|
||||
if (!tile) {
|
||||
tile = fetchJson<CityRow[]>(`${key}.json`).catch((err) => {
|
||||
// a failed fetch must not poison the cache: the next visit retries
|
||||
tileCache.delete(key);
|
||||
throw err;
|
||||
});
|
||||
tileCache.set(key, tile);
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Widening rings. Within 400 km the tile holds towns down to 20k; past that
|
||||
* only cities above 300k, which is exactly the bias a wide search wants - if
|
||||
* nothing is nearby, the answer should be a place people have heard of.
|
||||
*/
|
||||
const SEARCH_RADII_KM = [200, 400, 1500];
|
||||
|
||||
/**
|
||||
* Returns up to `count` cities around the given point, ordered by distance.
|
||||
*
|
||||
* Candidates are ranked by population damped by distance, so a town up the
|
||||
* valley can outrank a metropolis three hours away, and picks have to keep
|
||||
* their distance from each other - otherwise a place like New York fills the
|
||||
* whole list with its own boroughs.
|
||||
*/
|
||||
export async function findNearbyCities(
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
count = 10
|
||||
): Promise<NearbyCity[]> {
|
||||
const rows = await loadTile(tileKey(latitude, longitude));
|
||||
if (rows.length === 0) return [];
|
||||
|
||||
let best: NearbyCity[] = [];
|
||||
|
||||
for (const radius of SEARCH_RADII_KM) {
|
||||
const halfWeightKm = radius / 4;
|
||||
// far-apart picks in a wide search, tight ones when everything is close
|
||||
const minSeparationKm = Math.max(15, radius / 20);
|
||||
|
||||
const scored = rows
|
||||
.map(([id, name, countryCode, lat, lon, popK]) => {
|
||||
const dist = distanceKm(latitude, longitude, lat, lon);
|
||||
return {
|
||||
city: {
|
||||
id,
|
||||
name,
|
||||
countryCode,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
population: popK * 1000,
|
||||
distanceKm: dist
|
||||
},
|
||||
score: popK / (1 + (dist / halfWeightKm) ** 2)
|
||||
};
|
||||
})
|
||||
.filter(({ city }) => city.distanceKm >= minSeparationKm && city.distanceKm <= radius)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const picked: NearbyCity[] = [];
|
||||
for (const { city } of scored) {
|
||||
if (picked.length === count) break;
|
||||
const tooClose = picked.some(
|
||||
(p) => distanceKm(p.latitude, p.longitude, city.latitude, city.longitude) < minSeparationKm
|
||||
);
|
||||
if (!tooClose) picked.push(city);
|
||||
}
|
||||
|
||||
if (picked.length >= count) return picked.sort((a, b) => a.distanceKm - b.distanceKm);
|
||||
if (picked.length > best.length) best = picked;
|
||||
}
|
||||
|
||||
return best.sort((a, b) => a.distanceKm - b.distanceKm);
|
||||
}
|
||||
+85
-10
@@ -14,6 +14,7 @@ import { Unit } from '@openmeteo/sdk/unit';
|
||||
import { fetchWeatherApi } from 'openmeteo';
|
||||
|
||||
import { type DaylightBand, buildDaylightBands } from '$lib/charts/bands';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import type { VariableWithValues } from '@openmeteo/sdk/variable-with-values';
|
||||
import type { VariablesWithTime } from '@openmeteo/sdk/variables-with-time';
|
||||
@@ -313,8 +314,8 @@ export function humanizeWeatherError(err: unknown): FriendlyWeatherError {
|
||||
msg.includes('network request failed')
|
||||
) {
|
||||
return {
|
||||
title: "Couldn't reach the weather service",
|
||||
hint: 'Check your internet connection and try again.',
|
||||
title: m.err_network_title(),
|
||||
hint: m.err_network_hint(),
|
||||
detail: raw
|
||||
};
|
||||
}
|
||||
@@ -325,21 +326,21 @@ export function humanizeWeatherError(err: unknown): FriendlyWeatherError {
|
||||
msg.includes('coordinates')
|
||||
) {
|
||||
return {
|
||||
title: 'No data for this location with the selected model',
|
||||
hint: 'Regional weather models only cover their own area — "Best match" picks a suitable model automatically.',
|
||||
title: m.err_nodata_title(),
|
||||
hint: m.err_nodata_hint(),
|
||||
detail: raw
|
||||
};
|
||||
}
|
||||
if (msg.includes('invalid') || msg.includes('cannot be') || msg.includes('bad request')) {
|
||||
return {
|
||||
title: 'The weather service rejected the request',
|
||||
hint: 'Try different settings, or switch the model back to "Best match".',
|
||||
title: m.err_rejected_title(),
|
||||
hint: m.err_rejected_hint(),
|
||||
detail: raw
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Loading the weather data failed',
|
||||
hint: 'Try again in a moment. If it keeps happening, switch the model to "Best match".',
|
||||
title: m.err_generic_title(),
|
||||
hint: m.err_generic_hint(),
|
||||
detail: raw
|
||||
};
|
||||
}
|
||||
@@ -936,6 +937,8 @@ export interface HistoricalForecastParams extends WeatherLocation, WeatherUnitPa
|
||||
end_date: string;
|
||||
/** Hourly API variables to request; defaults to the core week set. */
|
||||
hourlyVariables?: string[];
|
||||
/** Reanalysis to read from; omitted lets the API pick. */
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface HistoricalForecastResult {
|
||||
@@ -995,7 +998,8 @@ export async function fetchHistoricalWeather(
|
||||
temperature_unit: params.temperature_unit ?? 'celsius',
|
||||
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
|
||||
precipitation_unit: params.precipitation_unit ?? 'mm',
|
||||
timezone: params.timezone
|
||||
timezone: params.timezone,
|
||||
models: params.model && params.model !== 'best_match' ? params.model : undefined
|
||||
};
|
||||
|
||||
const cleanParams: Record<string, string> = {};
|
||||
@@ -1201,6 +1205,8 @@ export interface SeasonalForecastParams extends WeatherLocation, WeatherUnitPara
|
||||
dailyVariables?: string[];
|
||||
/** Lead time in days; the API allows at most 216. */
|
||||
forecast_days?: number;
|
||||
/** Seasonal model; omitted lets the API pick. */
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface SeasonalForecastResult {
|
||||
@@ -1273,7 +1279,8 @@ export async function fetchSeasonalForecast(
|
||||
temperature_unit: params.temperature_unit ?? 'celsius',
|
||||
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
|
||||
precipitation_unit: params.precipitation_unit ?? 'mm',
|
||||
timezone: params.timezone
|
||||
timezone: params.timezone,
|
||||
models: params.model && params.model !== 'best_match' ? params.model : undefined
|
||||
};
|
||||
|
||||
const cleanParams: Record<string, string> = {};
|
||||
@@ -1372,3 +1379,71 @@ export async function fetchSeasonalForecast(
|
||||
timezone
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Nearby cities snapshot ─────────────────────────────────────────────────────
|
||||
|
||||
export interface NearbyDaily {
|
||||
/** local calendar date ("yyyy-MM-dd") -> that day's summary for this city */
|
||||
byDate: Record<string, { weatherCode: number; max: number; min: number; precipitation: number }>;
|
||||
}
|
||||
|
||||
export interface NearbySnapshotParams extends WeatherUnitParams {
|
||||
points: { latitude: number; longitude: number }[];
|
||||
past_days?: number;
|
||||
forecast_days?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a daily summary for several locations in one request - the forecast
|
||||
* API takes comma-separated coordinates and answers with one response per
|
||||
* point, in order.
|
||||
*
|
||||
* Deliberately runs on best_match: the nearby list can reach well past the
|
||||
* domain of whatever regional model the page is showing, and a row of dashes
|
||||
* is worse than a row from a model that covers everywhere.
|
||||
*/
|
||||
export async function fetchNearbyDaily(
|
||||
params: NearbySnapshotParams
|
||||
): Promise<(NearbyDaily | null)[]> {
|
||||
if (params.points.length === 0) return [];
|
||||
|
||||
const apiParams: Record<string, string> = {
|
||||
latitude: params.points.map((p) => p.latitude).join(','),
|
||||
longitude: params.points.map((p) => p.longitude).join(','),
|
||||
daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum',
|
||||
temperature_unit: params.temperature_unit ?? 'celsius',
|
||||
wind_speed_unit: params.wind_speed_unit ?? 'kmh',
|
||||
precipitation_unit: params.precipitation_unit ?? 'mm',
|
||||
past_days: String(params.past_days ?? 3),
|
||||
forecast_days: String(params.forecast_days ?? 16),
|
||||
timezone: 'auto'
|
||||
};
|
||||
|
||||
const responses = await fetchWeatherApi(FORECAST_URL, apiParams);
|
||||
|
||||
return params.points.map((_, i) => {
|
||||
const response = responses[i];
|
||||
const dailyBlock = response?.daily();
|
||||
if (!dailyBlock) return null;
|
||||
|
||||
const utcOffsetSeconds = response.utcOffsetSeconds();
|
||||
const codes = getValues(dailyBlock.variables(0)!);
|
||||
const max = getValues(dailyBlock.variables(1)!);
|
||||
const min = getValues(dailyBlock.variables(2)!);
|
||||
const precip = getValues(dailyBlock.variables(3)!);
|
||||
|
||||
// Same convention as the seasonal fetch: shift by the response's own
|
||||
// offset, then read the calendar date off the ISO string.
|
||||
const byDate: NearbyDaily['byDate'] = {};
|
||||
getTimestamps(dailyBlock).forEach((t, d) => {
|
||||
const key = new Date(t + utcOffsetSeconds * 1000).toISOString().slice(0, 10);
|
||||
byDate[key] = {
|
||||
weatherCode: codes[d],
|
||||
max: max[d],
|
||||
min: min[d],
|
||||
precipitation: precip[d]
|
||||
};
|
||||
});
|
||||
return { byDate };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ export const storedChartRange = persisted<ChartRangePref>('chart_range_v1', 'aut
|
||||
/** Selected ensemble model for the 14-day spread forecast. */
|
||||
export const storedEnsembleModel = persisted<string>('ensemble_model', 'ncep_gefs_seamless');
|
||||
|
||||
/** Reanalysis used on the historical page, and the seasonal model. */
|
||||
export const storedArchiveModel = persisted<string>('archive_model', 'best_match');
|
||||
export const storedSeasonalModel = persisted<string>('seasonal_model', 'best_match');
|
||||
|
||||
/** Measurement units, shared across every forecast page and persisted. */
|
||||
export interface UnitPrefs {
|
||||
temperature_unit: 'celsius' | 'fahrenheit';
|
||||
|
||||
@@ -13,7 +13,14 @@ export const geoLocationNameToRoute = (name: string) => {
|
||||
// selections navigate here directly, no geocoding id involved
|
||||
const COORD_ROUTE = /^(-?\d+(?:\.\d+)?)N(-?\d+(?:\.\d+)?)E$/i;
|
||||
|
||||
export function buildLocationRoute(location: GeoLocation): string {
|
||||
/** Only the fields the route is built from, so callers holding a partial record
|
||||
* (the nearby-cities list, for one) don't have to fake a whole GeoLocation. */
|
||||
export type RoutableLocation = Pick<
|
||||
GeoLocation,
|
||||
'id' | 'name' | 'latitude' | 'longitude' | 'feature_code' | 'population'
|
||||
>;
|
||||
|
||||
export function buildLocationRoute(location: RoutableLocation): string {
|
||||
// coordinate-only locations (GPS) have no real geocoding id
|
||||
if (location.feature_code === 'COORD' || !location.id) {
|
||||
return `${location.latitude.toFixed(4)}N${location.longitude.toFixed(4)}E`;
|
||||
|
||||
@@ -1,33 +1,11 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
import LocalizedContent from '$lib/components/localized-content.svelte';
|
||||
|
||||
import de from './content/de.svelte';
|
||||
import en from './content/en.svelte';
|
||||
import es from './content/es.svelte';
|
||||
import fr from './content/fr.svelte';
|
||||
import it from './content/it.svelte';
|
||||
</script>
|
||||
|
||||
<!-- TODO(vincent): replace the [bracketed] placeholders with your real postal
|
||||
address and country before deploying. An imprint without a serviceable
|
||||
address does not satisfy the imprint/contact requirements. -->
|
||||
<ProsePage title="Imprint" subtitle="Legal notice / provider identification.">
|
||||
<h2>Service provider</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2<br />
|
||||
CH-6442 Gersau<br />
|
||||
Switzerland
|
||||
</address>
|
||||
<p>
|
||||
Email: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
<p>Drizz.li is operated as a personal, independent project.</p>
|
||||
|
||||
<h2>Disclaimer</h2>
|
||||
<p>
|
||||
Weather data shown on this site is provided for general information only. Forecasts are
|
||||
inherently uncertain; do not rely on them where life, health or property is at stake - consult
|
||||
official warnings from your national weather service instead.
|
||||
</p>
|
||||
<p>
|
||||
Weather and geodata are retrieved from
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which is not
|
||||
affiliated with this site. External links are provided in good faith; their content is the
|
||||
responsibility of the respective operators.
|
||||
</p>
|
||||
</ProsePage>
|
||||
<LocalizedContent variants={{ en, de, es, fr, it }} />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Impressum" subtitle="Anbieterkennzeichnung.">
|
||||
<h2>Diensteanbieter</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2<br />
|
||||
CH-6442 Gersau<br />
|
||||
Switzerland
|
||||
</address>
|
||||
<p>
|
||||
E-Mail: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
<p>Drizz.li wird als persönliches, unabhängiges Projekt betrieben.</p>
|
||||
|
||||
<h2>Haftungsausschluss</h2>
|
||||
<p>
|
||||
Die hier gezeigten Wetterdaten dienen ausschliesslich der allgemeinen Information. Vorhersagen
|
||||
sind naturgemäss unsicher; verlassen Sie sich nicht auf sie, wenn Leben, Gesundheit oder
|
||||
Sachwerte auf dem Spiel stehen - massgeblich sind die amtlichen Warnungen Ihres nationalen
|
||||
Wetterdienstes.
|
||||
</p>
|
||||
<p>
|
||||
Wetter- und Geodaten stammen von
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, das nicht mit
|
||||
dieser Seite verbunden ist. Externe Links werden nach bestem Wissen gesetzt; für deren Inhalte
|
||||
sind die jeweiligen Betreiber verantwortlich.
|
||||
</p>
|
||||
<p class="text-muted-foreground">Massgeblich ist die englische Fassung dieses Impressums.</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Imprint" subtitle="Legal notice / provider identification.">
|
||||
<h2>Service provider</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2<br />
|
||||
CH-6442 Gersau<br />
|
||||
Switzerland
|
||||
</address>
|
||||
<p>
|
||||
Email: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
<p>Drizz.li is operated as a personal, independent project.</p>
|
||||
|
||||
<h2>Disclaimer</h2>
|
||||
<p>
|
||||
Weather data shown on this site is provided for general information only. Forecasts are
|
||||
inherently uncertain; do not rely on them where life, health or property is at stake - consult
|
||||
official warnings from your national weather service instead.
|
||||
</p>
|
||||
<p>
|
||||
Weather and geodata are retrieved from
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which is not
|
||||
affiliated with this site. External links are provided in good faith; their content is the
|
||||
responsibility of the respective operators.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Aviso legal" subtitle="Identificación del prestador del servicio.">
|
||||
<h2>Prestador del servicio</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2<br />
|
||||
CH-6442 Gersau<br />
|
||||
Switzerland
|
||||
</address>
|
||||
<p>
|
||||
Correo electrónico: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
<p>Drizz.li se gestiona como un proyecto personal e independiente.</p>
|
||||
|
||||
<h2>Descargo de responsabilidad</h2>
|
||||
<p>
|
||||
Los datos meteorológicos de este sitio son solo informativos. Los pronósticos son
|
||||
intrínsecamente inciertos; no confíes en ellos cuando estén en juego la vida, la salud o los
|
||||
bienes: consulta los avisos oficiales de tu servicio meteorológico nacional.
|
||||
</p>
|
||||
<p>
|
||||
Los datos meteorológicos y geográficos proceden de
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, que no está
|
||||
afiliado a este sitio. Los enlaces externos se ofrecen de buena fe; su contenido es
|
||||
responsabilidad de sus respectivos operadores.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
En caso de discrepancia prevalece la versión inglesa de este aviso legal.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Mentions légales" subtitle="Identification de l'éditeur.">
|
||||
<h2>Éditeur du service</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2<br />
|
||||
CH-6442 Gersau<br />
|
||||
Switzerland
|
||||
</address>
|
||||
<p>
|
||||
E-mail: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
<p>Drizz.li est exploité comme un projet personnel et indépendant.</p>
|
||||
|
||||
<h2>Avertissement</h2>
|
||||
<p>
|
||||
Les données météo présentées ici sont fournies à titre d'information générale. Les prévisions
|
||||
sont par nature incertaines ; ne vous y fiez pas lorsque des vies, la santé ou des biens sont en
|
||||
jeu - consultez les alertes officielles de votre service météorologique national.
|
||||
</p>
|
||||
<p>
|
||||
Les données météo et géographiques proviennent d'
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, qui n'est pas
|
||||
affilié à ce site. Les liens externes sont fournis de bonne foi ; leur contenu relève de la
|
||||
responsabilité de leurs exploitants.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
En cas de divergence, la version anglaise de ces mentions fait foi.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Note legali" subtitle="Identificazione del fornitore del servizio.">
|
||||
<h2>Fornitore del servizio</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2<br />
|
||||
CH-6442 Gersau<br />
|
||||
Switzerland
|
||||
</address>
|
||||
<p>
|
||||
E-mail: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
<p>Drizz.li è gestito come progetto personale e indipendente.</p>
|
||||
|
||||
<h2>Avvertenza</h2>
|
||||
<p>
|
||||
I dati meteo mostrati su questo sito hanno finalità puramente informative. Le previsioni sono
|
||||
per natura incerte: non farvi affidamento quando sono in gioco la vita, la salute o i beni -
|
||||
consulta gli avvisi ufficiali del tuo servizio meteorologico nazionale.
|
||||
</p>
|
||||
<p>
|
||||
I dati meteo e geografici provengono da
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, che non è
|
||||
affiliato a questo sito. I link esterni sono forniti in buona fede; i contenuti sono
|
||||
responsabilità dei rispettivi gestori.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
In caso di discrepanza prevale la versione inglese di queste note legali.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -1,147 +1,11 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
import LocalizedContent from '$lib/components/localized-content.svelte';
|
||||
|
||||
import de from './content/de.svelte';
|
||||
import en from './content/en.svelte';
|
||||
import es from './content/es.svelte';
|
||||
import fr from './content/fr.svelte';
|
||||
import it from './content/it.svelte';
|
||||
</script>
|
||||
|
||||
<!-- TODO(vincent): fill in the [bracketed] placeholders (address, hosting
|
||||
provider, supervisory authority) before deploying. -->
|
||||
<ProsePage title="Privacy policy" subtitle="Last updated: 1 August 2026">
|
||||
<p>
|
||||
Drizz.li is built to need as little of your data as possible: there are no user accounts, no
|
||||
cookies, no advertising and no analytics or tracking scripts. This page explains what little
|
||||
processing does happen - when you browse the site, and when you support the project with a
|
||||
contribution.
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
|
||||
Email:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</address>
|
||||
<p>
|
||||
This policy covers the websites drizz.li (the weather app) and support.drizz.li (the supporter
|
||||
signup page).
|
||||
</p>
|
||||
|
||||
<h2>2. Browsing the site</h2>
|
||||
<h3>Weather data requests</h3>
|
||||
<p>
|
||||
Drizz.li is a static site: when you open a forecast, your browser fetches the weather data
|
||||
directly from the open-data APIs of
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>
|
||||
(api.open-meteo.com, geocoding-api.open-meteo.com, archive-api.open-meteo.com, ensemble-api.open-meteo.com,
|
||||
and map tiles from maps.open-meteo.com / map-tiles.open-meteo.com / map-assets.open-meteo.com). Like
|
||||
any web request, this transmits your IP address and the requested location or search term to Open-Meteo.
|
||||
We do not receive or store any of this. See
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
>Open-Meteo's terms and privacy information</a
|
||||
>. Legal basis: our legitimate interest in delivering the content you request (Art. 6(1)(f)
|
||||
GDPR).
|
||||
</p>
|
||||
<h3>Hosting</h3>
|
||||
<p>
|
||||
The static site is served by [hosting provider, location]. The hosting infrastructure may keep
|
||||
short-lived technical server logs (IP address, requested URL, timestamp) for security and
|
||||
operations. Legal basis: legitimate interest in a secure, reliable service (Art. 6(1)(f) GDPR).
|
||||
</p>
|
||||
<h3>Settings on your device (local storage)</h3>
|
||||
<p>
|
||||
Your preferences - theme, measurement units, last searched location, and (for supporters) your
|
||||
access key and its last verification result - are stored only in your browser's local storage.
|
||||
They never leave your device except as described below for key verification, and you can clear
|
||||
them at any time via your browser.
|
||||
</p>
|
||||
|
||||
<h2>3. Supporting the project (contributions)</h2>
|
||||
<p>
|
||||
Supporter contributions unlock the supporter extras, so legally they are a paid agreement, not a
|
||||
pure gift - and this is what we process to handle them:
|
||||
</p>
|
||||
<h3>Signup form</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Email address</strong> (required) - to send you the transfer details and, after your contribution
|
||||
arrives, your access key.
|
||||
</li>
|
||||
<li><strong>Name</strong> (optional) - to help match your bank transfer.</li>
|
||||
<li>
|
||||
<strong>Currency and amount</strong> - shown based on your browser's locale/timezone, detected on
|
||||
your device (no geolocation request is made).
|
||||
</li>
|
||||
<li>
|
||||
<strong>IP address and technical anti-bot signals</strong> - kept with the signup request to prevent
|
||||
abuse and spam of the form.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Payment reference</strong> (e.g. DRZ-XXXXXX) - generated per request to match your transfer
|
||||
to your signup.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The transfer details (bank account and reference) are sent to you by email rather than shown on
|
||||
the page. Legal bases: performance of the agreement (Art. 6(1)(b) GDPR) and, for the anti-abuse
|
||||
measures, legitimate interest (Art. 6(1)(f) GDPR).
|
||||
</p>
|
||||
<h3>Bank transfer</h3>
|
||||
<p>
|
||||
Contributions are paid by ordinary bank transfer. Your bank and ours process the transfer data
|
||||
under their own responsibility; on our bank statement we see the usual transfer details (your
|
||||
name, account number, amount, reference). We use them only to match your contribution and are
|
||||
required to retain accounting records under statutory bookkeeping and tax law (Art. 6(1)(c)
|
||||
GDPR).
|
||||
</p>
|
||||
<h3>Access key and verification</h3>
|
||||
<p>
|
||||
After your transfer is matched (manually, usually within 24 hours), you receive an access key by
|
||||
email. When you paste it into Drizz.li, your browser sends the key to our verification endpoint
|
||||
(support.drizz.li) to check whether it is active; the response contains only the validity, tier
|
||||
and expiry. The subscriber list (key, email, expiry) is stored on our server for as long as your
|
||||
access is active.
|
||||
</p>
|
||||
<h3>Email</h3>
|
||||
<p>
|
||||
Transactional emails (transfer details, access key, renewals) are sent through our email
|
||||
provider, Strato (Strato AG, Germany), acting as a processor. We send no newsletters or
|
||||
marketing email.
|
||||
</p>
|
||||
|
||||
<h2>4. Retention</h2>
|
||||
<ul>
|
||||
<li>Signup requests that are never paid are deleted after 6 months at the latest.</li>
|
||||
<li>
|
||||
Supporter records (key, email, expiry) are kept while your access is active and deleted within
|
||||
12 months after it expires, unless you ask us to delete them sooner.
|
||||
</li>
|
||||
<li>
|
||||
Accounting records (bank statements showing your transfer) are kept for the statutory
|
||||
retention period (up to 10 years, depending on jurisdiction).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Recipients</h2>
|
||||
<p>
|
||||
We do not sell or share personal data with third parties for their own purposes. Recipients are
|
||||
limited to: Open-Meteo (weather requests made directly by your browser), our hosting provider,
|
||||
our email provider (Strato), and the banks involved in your transfer.
|
||||
</p>
|
||||
|
||||
<h2>6. Your rights</h2>
|
||||
<p>
|
||||
Under the GDPR you have the right to access, rectification, erasure, restriction of processing,
|
||||
data portability, and to object to processing based on legitimate interest. Where processing is
|
||||
based on consent, you may withdraw it at any time. To exercise any of these rights, email
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. You also have the
|
||||
right to lodge a complaint with a supervisory authority, in particular in the EU member state of
|
||||
your residence, or with [competent supervisory authority of the controller].
|
||||
</p>
|
||||
<p>
|
||||
For visitors from Switzerland: the corresponding rights under the Swiss Federal Act on Data
|
||||
Protection (FADP) apply equivalently.
|
||||
</p>
|
||||
|
||||
<h2>7. Changes</h2>
|
||||
<p>
|
||||
We may update this policy when the service changes; the date above reflects the latest revision.
|
||||
</p>
|
||||
</ProsePage>
|
||||
<LocalizedContent variants={{ en, de, es, fr, it }} />
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Datenschutzerklärung" subtitle="Zuletzt aktualisiert: 1. August 2026">
|
||||
<p>
|
||||
Drizz.li ist so gebaut, dass möglichst wenige Ihrer Daten nötig sind: keine Benutzerkonten,
|
||||
keine Cookies, keine Werbung, keine Analyse- oder Tracking-Skripte. Diese Seite erklärt, welche
|
||||
wenige Verarbeitung dennoch stattfindet - beim Besuch der Seite und wenn Sie das Projekt mit
|
||||
einem Beitrag unterstützen.
|
||||
</p>
|
||||
|
||||
<h2>1. Verantwortlicher</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
|
||||
E-Mail:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</address>
|
||||
<p>
|
||||
Diese Erklärung gilt für die Websites drizz.li (die Wetter-App) und support.drizz.li (die
|
||||
Unterstützer-Anmeldung).
|
||||
</p>
|
||||
|
||||
<h2>2. Besuch der Website</h2>
|
||||
<h3>Abruf der Wetterdaten</h3>
|
||||
<p>
|
||||
Drizz.li ist eine statische Seite: Wenn Sie eine Vorhersage öffnen, ruft Ihr Browser die
|
||||
Wetterdaten direkt bei den Open-Data-Schnittstellen von
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Wie bei jeder
|
||||
Webanfrage werden dabei Ihre IP-Adresse und der gewünschte Ort bzw. Suchbegriff an Open-Meteo
|
||||
übertragen. Wir erhalten und speichern davon nichts. Siehe
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
>Nutzungs- und Datenschutzhinweise von Open-Meteo</a
|
||||
>. Rechtsgrundlage: unser berechtigtes Interesse an der Auslieferung der angeforderten Inhalte
|
||||
(Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
<h3>Hosting</h3>
|
||||
<p>
|
||||
Die statische Seite wird von [Hosting-Anbieter, Standort] ausgeliefert. Die
|
||||
Hosting-Infrastruktur kann kurzlebige technische Server-Logs (IP-Adresse, angefragte URL,
|
||||
Zeitstempel) zu Sicherheits- und Betriebszwecken speichern. Rechtsgrundlage: berechtigtes
|
||||
Interesse an einem sicheren, zuverlässigen Dienst (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
<h3>Einstellungen auf Ihrem Gerät (Local Storage)</h3>
|
||||
<p>
|
||||
Ihre Einstellungen - Design, Masseinheiten, zuletzt gesuchter Ort und (für Unterstützer) Ihr
|
||||
Zugangsschlüssel samt letztem Prüfergebnis - werden ausschliesslich im Local Storage Ihres
|
||||
Browsers gespeichert. Sie verlassen Ihr Gerät nicht, ausser wie unten für die Schlüsselprüfung
|
||||
beschrieben, und Sie können sie jederzeit über Ihren Browser löschen.
|
||||
</p>
|
||||
|
||||
<h2>3. Das Projekt unterstützen (Beiträge)</h2>
|
||||
<p>
|
||||
Unterstützerbeiträge schalten die Extras frei, rechtlich sind sie daher eine entgeltliche
|
||||
Vereinbarung und keine reine Schenkung - das verarbeiten wir zur Abwicklung:
|
||||
</p>
|
||||
<h3>Anmeldeformular</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>E-Mail-Adresse</strong> (erforderlich) - um Ihnen die Überweisungsdaten und nach Eingang
|
||||
Ihres Beitrags den Zugangsschlüssel zu senden.
|
||||
</li>
|
||||
<li><strong>Name</strong> (optional) - zur leichteren Zuordnung Ihrer Überweisung.</li>
|
||||
<li>
|
||||
<strong>Währung und Betrag</strong> - anhand der Spracheinstellung/Zeitzone Ihres Browsers auf Ihrem
|
||||
Gerät ermittelt (keine Standortabfrage).
|
||||
</li>
|
||||
<li>
|
||||
<strong>IP-Adresse und technische Anti-Bot-Signale</strong> - zusammen mit der Anfrage gespeichert,
|
||||
um Missbrauch und Spam zu verhindern.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Zahlungsreferenz</strong> (z. B. DRZ-XXXXXX) - je Anfrage erzeugt, um Ihre Überweisung Ihrer
|
||||
Anmeldung zuzuordnen.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Die Überweisungsdaten (Bankverbindung und Referenz) senden wir Ihnen per E-Mail statt sie auf
|
||||
der Seite anzuzeigen. Rechtsgrundlagen: Erfüllung der Vereinbarung (Art. 6 Abs. 1 lit. b DSGVO)
|
||||
und für die Missbrauchsabwehr berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
<h3>Banküberweisung</h3>
|
||||
<p>
|
||||
Beiträge werden per gewöhnlicher Banküberweisung gezahlt. Ihre und unsere Bank verarbeiten die
|
||||
Überweisungsdaten in eigener Verantwortung; auf unserem Kontoauszug sehen wir die üblichen
|
||||
Angaben (Name, Kontonummer, Betrag, Referenz). Wir nutzen sie nur zur Zuordnung Ihres Beitrags
|
||||
und sind nach handels- und steuerrechtlichen Vorschriften zur Aufbewahrung verpflichtet (Art. 6
|
||||
Abs. 1 lit. c DSGVO).
|
||||
</p>
|
||||
<h3>Zugangsschlüssel und Prüfung</h3>
|
||||
<p>
|
||||
Nach Zuordnung Ihrer Überweisung (manuell, meist innerhalb von 24 Stunden) erhalten Sie den
|
||||
Zugangsschlüssel per E-Mail. Wenn Sie ihn in Drizz.li einfügen, sendet Ihr Browser den Schlüssel
|
||||
an unseren Prüfendpunkt (support.drizz.li), um seine Gültigkeit zu prüfen; die Antwort enthält
|
||||
nur Gültigkeit, Stufe und Ablauf. Die Abonnentenliste (Schlüssel, E-Mail, Ablauf) liegt auf
|
||||
unserem Server, solange Ihr Zugang aktiv ist.
|
||||
</p>
|
||||
<h3>E-Mail</h3>
|
||||
<p>
|
||||
Transaktions-E-Mails (Überweisungsdaten, Zugangsschlüssel, Verlängerungen) versenden wir über
|
||||
unseren E-Mail-Anbieter Strato (Strato AG, Deutschland) als Auftragsverarbeiter. Newsletter oder
|
||||
Werbe-E-Mails versenden wir nicht.
|
||||
</p>
|
||||
|
||||
<h2>4. Speicherdauer</h2>
|
||||
<ul>
|
||||
<li>Nicht bezahlte Anmeldungen werden spätestens nach 6 Monaten gelöscht.</li>
|
||||
<li>
|
||||
Unterstützerdaten (Schlüssel, E-Mail, Ablauf) werden für die Dauer Ihres Zugangs aufbewahrt
|
||||
und innerhalb von 12 Monaten nach dessen Ablauf gelöscht, sofern Sie keine frühere Löschung
|
||||
wünschen.
|
||||
</li>
|
||||
<li>
|
||||
Buchhaltungsunterlagen (Kontoauszüge mit Ihrer Überweisung) werden für die gesetzliche
|
||||
Aufbewahrungsfrist gespeichert (je nach Rechtsordnung bis zu 10 Jahre).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Empfänger</h2>
|
||||
<p>
|
||||
Wir verkaufen personenbezogene Daten nicht und geben sie nicht für eigene Zwecke Dritter weiter.
|
||||
Empfänger sind ausschliesslich: Open-Meteo (Wetterabrufe direkt durch Ihren Browser), unser
|
||||
Hosting-Anbieter, unser E-Mail-Anbieter (Strato) und die an der Überweisung beteiligten Banken.
|
||||
</p>
|
||||
|
||||
<h2>6. Ihre Rechte</h2>
|
||||
<p>
|
||||
Nach der DSGVO haben Sie das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der
|
||||
Verarbeitung, Datenübertragbarkeit sowie das Recht, einer auf berechtigtem Interesse beruhenden
|
||||
Verarbeitung zu widersprechen. Beruht eine Verarbeitung auf Einwilligung, können Sie diese
|
||||
jederzeit widerrufen. Zur Ausübung schreiben Sie an
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. Ausserdem haben
|
||||
Sie das Recht, sich bei einer Aufsichtsbehörde zu beschweren, insbesondere im EU-Mitgliedstaat
|
||||
Ihres Wohnsitzes, oder bei [zuständige Aufsichtsbehörde des Verantwortlichen].
|
||||
</p>
|
||||
<p>
|
||||
Für Besucherinnen und Besucher aus der Schweiz gelten die entsprechenden Rechte nach dem
|
||||
Bundesgesetz über den Datenschutz (DSG) sinngemäss.
|
||||
</p>
|
||||
|
||||
<h2>7. Änderungen</h2>
|
||||
<p>
|
||||
Wir aktualisieren diese Erklärung, wenn sich der Dienst ändert; das Datum oben nennt die letzte
|
||||
Fassung.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
Massgeblich ist die englische Fassung dieser Datenschutzerklärung.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,145 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Privacy policy" subtitle="Last updated: 1 August 2026">
|
||||
<p>
|
||||
Drizz.li is built to need as little of your data as possible: there are no user accounts, no
|
||||
cookies, no advertising and no analytics or tracking scripts. This page explains what little
|
||||
processing does happen - when you browse the site, and when you support the project with a
|
||||
contribution.
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
|
||||
Email:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</address>
|
||||
<p>
|
||||
This policy covers the websites drizz.li (the weather app) and support.drizz.li (the supporter
|
||||
signup page).
|
||||
</p>
|
||||
|
||||
<h2>2. Browsing the site</h2>
|
||||
<h3>Weather data requests</h3>
|
||||
<p>
|
||||
Drizz.li is a static site: when you open a forecast, your browser fetches the weather data
|
||||
directly from the open-data APIs of
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>
|
||||
(api.open-meteo.com, geocoding-api.open-meteo.com, archive-api.open-meteo.com, ensemble-api.open-meteo.com,
|
||||
and map tiles from maps.open-meteo.com / map-tiles.open-meteo.com / map-assets.open-meteo.com). Like
|
||||
any web request, this transmits your IP address and the requested location or search term to Open-Meteo.
|
||||
We do not receive or store any of this. See
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
>Open-Meteo's terms and privacy information</a
|
||||
>. Legal basis: our legitimate interest in delivering the content you request (Art. 6(1)(f)
|
||||
GDPR).
|
||||
</p>
|
||||
<h3>Hosting</h3>
|
||||
<p>
|
||||
The static site is served by [hosting provider, location]. The hosting infrastructure may keep
|
||||
short-lived technical server logs (IP address, requested URL, timestamp) for security and
|
||||
operations. Legal basis: legitimate interest in a secure, reliable service (Art. 6(1)(f) GDPR).
|
||||
</p>
|
||||
<h3>Settings on your device (local storage)</h3>
|
||||
<p>
|
||||
Your preferences - theme, measurement units, last searched location, and (for supporters) your
|
||||
access key and its last verification result - are stored only in your browser's local storage.
|
||||
They never leave your device except as described below for key verification, and you can clear
|
||||
them at any time via your browser.
|
||||
</p>
|
||||
|
||||
<h2>3. Supporting the project (contributions)</h2>
|
||||
<p>
|
||||
Supporter contributions unlock the supporter extras, so legally they are a paid agreement, not a
|
||||
pure gift - and this is what we process to handle them:
|
||||
</p>
|
||||
<h3>Signup form</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Email address</strong> (required) - to send you the transfer details and, after your contribution
|
||||
arrives, your access key.
|
||||
</li>
|
||||
<li><strong>Name</strong> (optional) - to help match your bank transfer.</li>
|
||||
<li>
|
||||
<strong>Currency and amount</strong> - shown based on your browser's locale/timezone, detected on
|
||||
your device (no geolocation request is made).
|
||||
</li>
|
||||
<li>
|
||||
<strong>IP address and technical anti-bot signals</strong> - kept with the signup request to prevent
|
||||
abuse and spam of the form.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Payment reference</strong> (e.g. DRZ-XXXXXX) - generated per request to match your transfer
|
||||
to your signup.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The transfer details (bank account and reference) are sent to you by email rather than shown on
|
||||
the page. Legal bases: performance of the agreement (Art. 6(1)(b) GDPR) and, for the anti-abuse
|
||||
measures, legitimate interest (Art. 6(1)(f) GDPR).
|
||||
</p>
|
||||
<h3>Bank transfer</h3>
|
||||
<p>
|
||||
Contributions are paid by ordinary bank transfer. Your bank and ours process the transfer data
|
||||
under their own responsibility; on our bank statement we see the usual transfer details (your
|
||||
name, account number, amount, reference). We use them only to match your contribution and are
|
||||
required to retain accounting records under statutory bookkeeping and tax law (Art. 6(1)(c)
|
||||
GDPR).
|
||||
</p>
|
||||
<h3>Access key and verification</h3>
|
||||
<p>
|
||||
After your transfer is matched (manually, usually within 24 hours), you receive an access key by
|
||||
email. When you paste it into Drizz.li, your browser sends the key to our verification endpoint
|
||||
(support.drizz.li) to check whether it is active; the response contains only the validity, tier
|
||||
and expiry. The subscriber list (key, email, expiry) is stored on our server for as long as your
|
||||
access is active.
|
||||
</p>
|
||||
<h3>Email</h3>
|
||||
<p>
|
||||
Transactional emails (transfer details, access key, renewals) are sent through our email
|
||||
provider, Strato (Strato AG, Germany), acting as a processor. We send no newsletters or
|
||||
marketing email.
|
||||
</p>
|
||||
|
||||
<h2>4. Retention</h2>
|
||||
<ul>
|
||||
<li>Signup requests that are never paid are deleted after 6 months at the latest.</li>
|
||||
<li>
|
||||
Supporter records (key, email, expiry) are kept while your access is active and deleted within
|
||||
12 months after it expires, unless you ask us to delete them sooner.
|
||||
</li>
|
||||
<li>
|
||||
Accounting records (bank statements showing your transfer) are kept for the statutory
|
||||
retention period (up to 10 years, depending on jurisdiction).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Recipients</h2>
|
||||
<p>
|
||||
We do not sell or share personal data with third parties for their own purposes. Recipients are
|
||||
limited to: Open-Meteo (weather requests made directly by your browser), our hosting provider,
|
||||
our email provider (Strato), and the banks involved in your transfer.
|
||||
</p>
|
||||
|
||||
<h2>6. Your rights</h2>
|
||||
<p>
|
||||
Under the GDPR you have the right to access, rectification, erasure, restriction of processing,
|
||||
data portability, and to object to processing based on legitimate interest. Where processing is
|
||||
based on consent, you may withdraw it at any time. To exercise any of these rights, email
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. You also have the
|
||||
right to lodge a complaint with a supervisory authority, in particular in the EU member state of
|
||||
your residence, or with [competent supervisory authority of the controller].
|
||||
</p>
|
||||
<p>
|
||||
For visitors from Switzerland: the corresponding rights under the Swiss Federal Act on Data
|
||||
Protection (FADP) apply equivalently.
|
||||
</p>
|
||||
|
||||
<h2>7. Changes</h2>
|
||||
<p>
|
||||
We may update this policy when the service changes; the date above reflects the latest revision.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Política de privacidad" subtitle="Última actualización: 1 de agosto de 2026">
|
||||
<p>
|
||||
Drizz.li está hecho para necesitar los mínimos datos posibles: sin cuentas, sin cookies, sin
|
||||
publicidad y sin scripts de analítica ni de seguimiento. Esta página explica el poco tratamiento
|
||||
que sí ocurre: al navegar por el sitio y al apoyar el proyecto con una aportación.
|
||||
</p>
|
||||
|
||||
<h2>1. Responsable</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
|
||||
Correo electrónico:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</address>
|
||||
<p>
|
||||
Esta política cubre los sitios drizz.li (la app meteorológica) y support.drizz.li (la página de
|
||||
registro de colaboradores).
|
||||
</p>
|
||||
|
||||
<h2>2. Navegación por el sitio</h2>
|
||||
<h3>Peticiones de datos meteorológicos</h3>
|
||||
<p>
|
||||
Drizz.li es un sitio estático: cuando abres un pronóstico, tu navegador obtiene los datos
|
||||
directamente de las API abiertas de
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Como en
|
||||
cualquier petición web, esto transmite tu dirección IP y la ubicación o término buscado a
|
||||
Open-Meteo. Nosotros no recibimos ni almacenamos nada de eso. Consulta
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
>las condiciones e información de privacidad de Open-Meteo</a
|
||||
>. Base jurídica: nuestro interés legítimo en entregar el contenido que solicitas (art. 6.1.f
|
||||
del RGPD).
|
||||
</p>
|
||||
<h3>Alojamiento</h3>
|
||||
<p>
|
||||
El sitio estático lo sirve [proveedor de alojamiento, ubicación]. La infraestructura puede
|
||||
conservar registros técnicos de corta duración (dirección IP, URL solicitada, marca de tiempo)
|
||||
por seguridad y operación. Base jurídica: interés legítimo en un servicio seguro y fiable (art.
|
||||
6.1.f del RGPD).
|
||||
</p>
|
||||
<h3>Ajustes en tu dispositivo (almacenamiento local)</h3>
|
||||
<p>
|
||||
Tus preferencias - tema, unidades, última ubicación buscada y (para colaboradores) tu clave de
|
||||
acceso y su última verificación - se guardan únicamente en el almacenamiento local de tu
|
||||
navegador. No salen de tu dispositivo salvo como se describe abajo para verificar la clave, y
|
||||
puedes borrarlas cuando quieras.
|
||||
</p>
|
||||
|
||||
<h2>3. Apoyar el proyecto (aportaciones)</h2>
|
||||
<p>
|
||||
Las aportaciones desbloquean los extras, así que legalmente son un acuerdo remunerado y no una
|
||||
donación pura; esto es lo que tratamos para gestionarlas:
|
||||
</p>
|
||||
<h3>Formulario de registro</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Dirección de correo</strong> (obligatoria) - para enviarte los datos de la transferencia
|
||||
y, cuando llegue tu aportación, tu clave de acceso.
|
||||
</li>
|
||||
<li><strong>Nombre</strong> (opcional) - para ayudar a casar tu transferencia.</li>
|
||||
<li>
|
||||
<strong>Moneda e importe</strong> - determinados por la configuración regional/zona horaria de tu
|
||||
navegador, en tu dispositivo (no se solicita geolocalización).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Dirección IP y señales técnicas anti-bots</strong> - guardadas con la solicitud para evitar
|
||||
abusos y spam.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Referencia de pago</strong> (p. ej. DRZ-XXXXXX) - generada por solicitud para casar tu transferencia
|
||||
con tu registro.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Los datos de la transferencia (cuenta y referencia) se te envían por correo en lugar de
|
||||
mostrarse en la página. Bases jurídicas: ejecución del contrato (art. 6.1.b del RGPD) y, para
|
||||
las medidas antiabuso, interés legítimo (art. 6.1.f del RGPD).
|
||||
</p>
|
||||
<h3>Transferencia bancaria</h3>
|
||||
<p>
|
||||
Las aportaciones se pagan por transferencia bancaria ordinaria. Tu banco y el nuestro tratan los
|
||||
datos bajo su propia responsabilidad; en nuestro extracto vemos los datos habituales (nombre,
|
||||
número de cuenta, importe, referencia). Solo los usamos para casar tu aportación y debemos
|
||||
conservar los registros contables por obligación legal (art. 6.1.c del RGPD).
|
||||
</p>
|
||||
<h3>Clave de acceso y verificación</h3>
|
||||
<p>
|
||||
Una vez casada tu transferencia (manualmente, normalmente en 24 horas), recibes una clave de
|
||||
acceso por correo. Al pegarla en Drizz.li, tu navegador envía la clave a nuestro punto de
|
||||
verificación (support.drizz.li) para comprobar si está activa; la respuesta contiene solo
|
||||
validez, nivel y caducidad. La lista de suscriptores (clave, correo, caducidad) se guarda en
|
||||
nuestro servidor mientras tu acceso esté activo.
|
||||
</p>
|
||||
<h3>Correo electrónico</h3>
|
||||
<p>
|
||||
Los correos transaccionales (datos de transferencia, clave de acceso, renovaciones) se envían a
|
||||
través de nuestro proveedor Strato (Strato AG, Alemania), que actúa como encargado del
|
||||
tratamiento. No enviamos boletines ni correo comercial.
|
||||
</p>
|
||||
|
||||
<h2>4. Conservación</h2>
|
||||
<ul>
|
||||
<li>Las solicitudes de registro que nunca se pagan se eliminan como máximo a los 6 meses.</li>
|
||||
<li>
|
||||
Los registros de colaborador (clave, correo, caducidad) se conservan mientras tu acceso esté
|
||||
activo y se eliminan en los 12 meses siguientes a su expiración, salvo que pidas borrarlos
|
||||
antes.
|
||||
</li>
|
||||
<li>
|
||||
Los registros contables (extractos que muestran tu transferencia) se conservan durante el
|
||||
plazo legal (hasta 10 años según la jurisdicción).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Destinatarios</h2>
|
||||
<p>
|
||||
No vendemos ni compartimos datos personales con terceros para sus propios fines. Los
|
||||
destinatarios se limitan a: Open-Meteo (peticiones meteorológicas hechas directamente por tu
|
||||
navegador), nuestro proveedor de alojamiento, nuestro proveedor de correo (Strato) y los bancos
|
||||
implicados en tu transferencia.
|
||||
</p>
|
||||
|
||||
<h2>6. Tus derechos</h2>
|
||||
<p>
|
||||
Conforme al RGPD tienes derecho de acceso, rectificación, supresión, limitación del tratamiento,
|
||||
portabilidad y oposición al tratamiento basado en el interés legítimo. Cuando el tratamiento se
|
||||
base en el consentimiento, puedes retirarlo en cualquier momento. Para ejercer estos derechos,
|
||||
escribe a
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. También tienes
|
||||
derecho a presentar una reclamación ante una autoridad de control, en particular en el Estado
|
||||
miembro de tu residencia, o ante [autoridad de control competente].
|
||||
</p>
|
||||
<p>
|
||||
Para visitantes desde Suiza, se aplican de forma equivalente los derechos correspondientes de la
|
||||
Ley Federal de Protección de Datos (LPD).
|
||||
</p>
|
||||
|
||||
<h2>7. Cambios</h2>
|
||||
<p>
|
||||
Podemos actualizar esta política cuando cambie el servicio; la fecha de arriba refleja la última
|
||||
revisión.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
En caso de discrepancia prevalece la versión inglesa de esta política.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Politique de confidentialité" subtitle="Dernière mise à jour : 1er août 2026">
|
||||
<p>
|
||||
Drizz.li est conçu pour avoir besoin du minimum de vos données : aucun compte, aucun cookie,
|
||||
aucune publicité, aucun script d'analyse ou de suivi. Cette page explique le peu de traitement
|
||||
qui a lieu - lorsque vous consultez le site et lorsque vous soutenez le projet par une
|
||||
contribution.
|
||||
</p>
|
||||
|
||||
<h2>1. Responsable du traitement</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
|
||||
E-mail:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</address>
|
||||
<p>
|
||||
Cette politique couvre les sites drizz.li (l'application météo) et support.drizz.li (la page
|
||||
d'inscription contributeur).
|
||||
</p>
|
||||
|
||||
<h2>2. Navigation sur le site</h2>
|
||||
<h3>Requêtes de données météo</h3>
|
||||
<p>
|
||||
Drizz.li est un site statique : lorsque vous ouvrez une prévision, votre navigateur récupère les
|
||||
données directement auprès des API ouvertes d'
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Comme pour toute
|
||||
requête web, cela transmet votre adresse IP et le lieu ou le terme recherché à Open-Meteo. Nous
|
||||
n'en recevons ni n'en conservons rien. Voir
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
>les conditions et informations de confidentialité d'Open-Meteo</a
|
||||
>. Base légale : notre intérêt légitime à fournir le contenu demandé (art. 6, par. 1, point f)
|
||||
du RGPD).
|
||||
</p>
|
||||
<h3>Hébergement</h3>
|
||||
<p>
|
||||
Le site statique est servi par [hébergeur, localisation]. L'infrastructure d'hébergement peut
|
||||
conserver de brefs journaux techniques (adresse IP, URL demandée, horodatage) à des fins de
|
||||
sécurité et d'exploitation. Base légale : intérêt légitime à un service sûr et fiable (art. 6,
|
||||
par. 1, point f) du RGPD).
|
||||
</p>
|
||||
<h3>Réglages sur votre appareil (stockage local)</h3>
|
||||
<p>
|
||||
Vos préférences - thème, unités, dernier lieu recherché et (pour les contributeurs) votre clé
|
||||
d'accès et son dernier résultat de vérification - sont stockées uniquement dans le stockage
|
||||
local de votre navigateur. Elles ne quittent pas votre appareil, sauf comme décrit ci-dessous
|
||||
pour la vérification de la clé, et vous pouvez les effacer à tout moment.
|
||||
</p>
|
||||
|
||||
<h2>3. Soutenir le projet (contributions)</h2>
|
||||
<p>
|
||||
Les contributions débloquent les bonus : juridiquement il s'agit donc d'un accord payant et non
|
||||
d'un simple don - voici ce que nous traitons pour les gérer :
|
||||
</p>
|
||||
<h3>Formulaire d'inscription</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Adresse e-mail</strong> (obligatoire) - pour vous envoyer les coordonnées bancaires puis,
|
||||
à réception de votre contribution, votre clé d'accès.
|
||||
</li>
|
||||
<li><strong>Nom</strong> (facultatif) - pour faciliter le rapprochement de votre virement.</li>
|
||||
<li>
|
||||
<strong>Devise et montant</strong> - déterminés d'après la langue/le fuseau horaire de votre navigateur,
|
||||
sur votre appareil (aucune demande de géolocalisation).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Adresse IP et signaux anti-robots techniques</strong> - conservés avec la demande pour prévenir
|
||||
les abus et le spam.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Référence de paiement</strong> (p. ex. DRZ-XXXXXX) - générée par demande pour rapprocher
|
||||
votre virement de votre inscription.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Les coordonnées bancaires (compte et référence) vous sont envoyées par e-mail plutôt
|
||||
qu'affichées sur la page. Bases légales : exécution du contrat (art. 6, par. 1, point b) du
|
||||
RGPD) et, pour les mesures anti-abus, intérêt légitime (art. 6, par. 1, point f) du RGPD).
|
||||
</p>
|
||||
<h3>Virement bancaire</h3>
|
||||
<p>
|
||||
Les contributions sont réglées par virement bancaire ordinaire. Votre banque et la nôtre
|
||||
traitent les données du virement sous leur propre responsabilité ; sur notre relevé figurent les
|
||||
informations habituelles (nom, numéro de compte, montant, référence). Nous ne les utilisons que
|
||||
pour rapprocher votre contribution et devons conserver les pièces comptables au titre des
|
||||
obligations légales (art. 6, par. 1, point c) du RGPD).
|
||||
</p>
|
||||
<h3>Clé d'accès et vérification</h3>
|
||||
<p>
|
||||
Une fois votre virement rapproché (manuellement, en général sous 24 heures), vous recevez une
|
||||
clé d'accès par e-mail. Lorsque vous la collez dans Drizz.li, votre navigateur envoie la clé à
|
||||
notre point de vérification (support.drizz.li) pour savoir si elle est active ; la réponse ne
|
||||
contient que la validité, le niveau et l'échéance. La liste des abonnés (clé, e-mail, échéance)
|
||||
est conservée sur notre serveur tant que votre accès est actif.
|
||||
</p>
|
||||
<h3>E-mail</h3>
|
||||
<p>
|
||||
Les e-mails transactionnels (coordonnées bancaires, clé d'accès, renouvellements) sont envoyés
|
||||
via notre prestataire Strato (Strato AG, Allemagne), agissant comme sous-traitant. Nous
|
||||
n'envoyons ni newsletter ni e-mail marketing.
|
||||
</p>
|
||||
|
||||
<h2>4. Conservation</h2>
|
||||
<ul>
|
||||
<li>Les demandes d'inscription jamais payées sont supprimées au plus tard après 6 mois.</li>
|
||||
<li>
|
||||
Les données contributeur (clé, e-mail, échéance) sont conservées tant que votre accès est
|
||||
actif et supprimées dans les 12 mois suivant son expiration, sauf demande de suppression
|
||||
anticipée.
|
||||
</li>
|
||||
<li>
|
||||
Les pièces comptables (relevés bancaires mentionnant votre virement) sont conservées pendant
|
||||
la durée légale (jusqu'à 10 ans selon la juridiction).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Destinataires</h2>
|
||||
<p>
|
||||
Nous ne vendons ni ne partageons de données personnelles avec des tiers pour leurs propres
|
||||
finalités. Les destinataires se limitent à : Open-Meteo (requêtes météo effectuées directement
|
||||
par votre navigateur), notre hébergeur, notre prestataire e-mail (Strato) et les banques
|
||||
impliquées dans votre virement.
|
||||
</p>
|
||||
|
||||
<h2>6. Vos droits</h2>
|
||||
<p>
|
||||
En vertu du RGPD, vous disposez d'un droit d'accès, de rectification, d'effacement, de
|
||||
limitation du traitement, de portabilité, et d'opposition au traitement fondé sur l'intérêt
|
||||
légitime. Lorsque le traitement repose sur le consentement, vous pouvez le retirer à tout
|
||||
moment. Pour exercer ces droits, écrivez à
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. Vous avez
|
||||
également le droit d'introduire une réclamation auprès d'une autorité de contrôle, notamment
|
||||
dans l'État membre de votre résidence, ou auprès de [autorité de contrôle compétente].
|
||||
</p>
|
||||
<p>
|
||||
Pour les visiteurs suisses, les droits correspondants de la loi fédérale sur la protection des
|
||||
données (LPD) s'appliquent de manière équivalente.
|
||||
</p>
|
||||
|
||||
<h2>7. Modifications</h2>
|
||||
<p>
|
||||
Nous pouvons mettre à jour cette politique lorsque le service évolue ; la date ci-dessus indique
|
||||
la dernière révision.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
En cas de divergence, la version anglaise de cette politique fait foi.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Informativa sulla privacy" subtitle="Ultimo aggiornamento: 1 agosto 2026">
|
||||
<p>
|
||||
Drizz.li è costruito per aver bisogno del minimo dei tuoi dati: nessun account, nessun cookie,
|
||||
nessuna pubblicità, nessuno script di analisi o tracciamento. Questa pagina spiega il poco
|
||||
trattamento che avviene comunque: quando navighi sul sito e quando sostieni il progetto con un
|
||||
contributo.
|
||||
</p>
|
||||
|
||||
<h2>1. Titolare del trattamento</h2>
|
||||
<address>
|
||||
Vincent van der Wal<br />
|
||||
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
|
||||
E-mail:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</address>
|
||||
<p>
|
||||
Questa informativa copre i siti drizz.li (l'app meteo) e support.drizz.li (la pagina di
|
||||
iscrizione per i sostenitori).
|
||||
</p>
|
||||
|
||||
<h2>2. Navigazione sul sito</h2>
|
||||
<h3>Richieste di dati meteo</h3>
|
||||
<p>
|
||||
Drizz.li è un sito statico: quando apri una previsione, il tuo browser scarica i dati
|
||||
direttamente dalle API aperte di
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Come per
|
||||
qualsiasi richiesta web, questo trasmette il tuo indirizzo IP e la località o il termine cercato
|
||||
a Open-Meteo. Noi non riceviamo né conserviamo nulla di tutto ciò. Vedi
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
>le condizioni e le informazioni sulla privacy di Open-Meteo</a
|
||||
>. Base giuridica: il nostro legittimo interesse a fornire il contenuto richiesto (art. 6, par.
|
||||
1, lett. f del GDPR).
|
||||
</p>
|
||||
<h3>Hosting</h3>
|
||||
<p>
|
||||
Il sito statico è servito da [fornitore di hosting, sede]. L'infrastruttura può conservare brevi
|
||||
log tecnici del server (indirizzo IP, URL richiesto, data e ora) per sicurezza e gestione. Base
|
||||
giuridica: legittimo interesse a un servizio sicuro e affidabile (art. 6, par. 1, lett. f del
|
||||
GDPR).
|
||||
</p>
|
||||
<h3>Impostazioni sul tuo dispositivo (archiviazione locale)</h3>
|
||||
<p>
|
||||
Le tue preferenze - tema, unità di misura, ultima località cercata e (per i sostenitori) la
|
||||
chiave di accesso e l'ultimo esito della verifica - sono salvate solo nell'archiviazione locale
|
||||
del browser. Non lasciano il tuo dispositivo, salvo quanto descritto sotto per la verifica della
|
||||
chiave, e puoi cancellarle quando vuoi.
|
||||
</p>
|
||||
|
||||
<h2>3. Sostenere il progetto (contributi)</h2>
|
||||
<p>
|
||||
I contributi sbloccano gli extra, quindi giuridicamente sono un accordo a pagamento e non una
|
||||
pura donazione: ecco cosa trattiamo per gestirli:
|
||||
</p>
|
||||
<h3>Modulo di iscrizione</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Indirizzo e-mail</strong> (obbligatorio) - per inviarti i dati del bonifico e, all'arrivo
|
||||
del contributo, la chiave di accesso.
|
||||
</li>
|
||||
<li><strong>Nome</strong> (facoltativo) - per facilitare l'abbinamento del bonifico.</li>
|
||||
<li>
|
||||
<strong>Valuta e importo</strong> - determinati dalle impostazioni locali/fuso orario del browser,
|
||||
sul tuo dispositivo (nessuna richiesta di geolocalizzazione).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Indirizzo IP e segnali tecnici anti-bot</strong> - conservati con la richiesta per prevenire
|
||||
abusi e spam.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Riferimento di pagamento</strong> (es. DRZ-XXXXXX) - generato per ogni richiesta per abbinare
|
||||
il bonifico all'iscrizione.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
I dati del bonifico (conto e riferimento) ti vengono inviati via e-mail anziché mostrati sulla
|
||||
pagina. Basi giuridiche: esecuzione del contratto (art. 6, par. 1, lett. b del GDPR) e, per le
|
||||
misure anti-abuso, legittimo interesse (art. 6, par. 1, lett. f del GDPR).
|
||||
</p>
|
||||
<h3>Bonifico bancario</h3>
|
||||
<p>
|
||||
I contributi si pagano con un normale bonifico. La tua banca e la nostra trattano i dati del
|
||||
bonifico sotto la propria responsabilità; sul nostro estratto conto vediamo i dati consueti
|
||||
(nome, numero di conto, importo, riferimento). Li usiamo solo per abbinare il contributo e siamo
|
||||
tenuti a conservare le scritture contabili per obbligo di legge (art. 6, par. 1, lett. c del
|
||||
GDPR).
|
||||
</p>
|
||||
<h3>Chiave di accesso e verifica</h3>
|
||||
<p>
|
||||
Dopo l'abbinamento del bonifico (manuale, di solito entro 24 ore) ricevi una chiave di accesso
|
||||
via e-mail. Quando la incolli in Drizz.li, il browser invia la chiave al nostro endpoint di
|
||||
verifica (support.drizz.li) per controllare se è attiva; la risposta contiene solo validità,
|
||||
livello e scadenza. L'elenco dei sostenitori (chiave, e-mail, scadenza) resta sul nostro server
|
||||
finché il tuo accesso è attivo.
|
||||
</p>
|
||||
<h3>E-mail</h3>
|
||||
<p>
|
||||
Le e-mail transazionali (dati del bonifico, chiave di accesso, rinnovi) sono inviate tramite il
|
||||
nostro fornitore Strato (Strato AG, Germania), che agisce come responsabile del trattamento. Non
|
||||
inviamo newsletter né e-mail di marketing.
|
||||
</p>
|
||||
|
||||
<h2>4. Conservazione</h2>
|
||||
<ul>
|
||||
<li>Le richieste di iscrizione mai pagate sono cancellate al più tardi dopo 6 mesi.</li>
|
||||
<li>
|
||||
I dati dei sostenitori (chiave, e-mail, scadenza) sono conservati finché l'accesso è attivo e
|
||||
cancellati entro 12 mesi dalla scadenza, salvo richiesta di cancellazione anticipata.
|
||||
</li>
|
||||
<li>
|
||||
Le scritture contabili (estratti conto con il tuo bonifico) sono conservate per il periodo di
|
||||
legge (fino a 10 anni a seconda della giurisdizione).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Destinatari</h2>
|
||||
<p>
|
||||
Non vendiamo né condividiamo dati personali con terzi per finalità proprie. I destinatari si
|
||||
limitano a: Open-Meteo (richieste meteo fatte direttamente dal tuo browser), il nostro fornitore
|
||||
di hosting, il nostro fornitore e-mail (Strato) e le banche coinvolte nel bonifico.
|
||||
</p>
|
||||
|
||||
<h2>6. I tuoi diritti</h2>
|
||||
<p>
|
||||
In base al GDPR hai diritto di accesso, rettifica, cancellazione, limitazione del trattamento,
|
||||
portabilità dei dati e opposizione al trattamento fondato sul legittimo interesse. Se il
|
||||
trattamento si basa sul consenso, puoi revocarlo in qualsiasi momento. Per esercitare questi
|
||||
diritti scrivi a
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. Hai inoltre il
|
||||
diritto di proporre reclamo a un'autorità di controllo, in particolare nello Stato membro di
|
||||
residenza, o a [autorità di controllo competente].
|
||||
</p>
|
||||
<p>
|
||||
Per i visitatori dalla Svizzera valgono in modo equivalente i corrispondenti diritti previsti
|
||||
dalla Legge federale sulla protezione dei dati (LPD).
|
||||
</p>
|
||||
|
||||
<h2>7. Modifiche</h2>
|
||||
<p>
|
||||
Possiamo aggiornare questa informativa quando il servizio cambia; la data in alto indica
|
||||
l'ultima revisione.
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
In caso di discrepanza prevale la versione inglese di questa informativa.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -1,82 +1,11 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
import LocalizedContent from '$lib/components/localized-content.svelte';
|
||||
|
||||
import de from './content/de.svelte';
|
||||
import en from './content/en.svelte';
|
||||
import es from './content/es.svelte';
|
||||
import fr from './content/fr.svelte';
|
||||
import it from './content/it.svelte';
|
||||
</script>
|
||||
|
||||
<!-- TODO(vincent): confirm the governing-law country in section 8. -->
|
||||
<ProsePage title="Supporter terms" subtitle="Last updated: 1 August 2026">
|
||||
<p>
|
||||
Drizz.li is free to use. These terms cover the optional supporter contribution: a small payment
|
||||
that helps keep the project running and, as a thank-you, unlocks the supporter extras. Although
|
||||
we call it a contribution, features are unlocked in return.
|
||||
</p>
|
||||
|
||||
<h2>1. What you get</h2>
|
||||
<p>
|
||||
A supporter contribution unlocks the supporter extras for the paid period: currently historical
|
||||
weather with climate-normal comparisons, plus new supporter features as they land. The free
|
||||
parts of Drizz.li stay free for everyone.
|
||||
</p>
|
||||
|
||||
<h2>2. How it works</h2>
|
||||
<ul>
|
||||
<li>
|
||||
You request the transfer details on the signup page; we email them to you together with a
|
||||
personal payment reference.
|
||||
</li>
|
||||
<li>
|
||||
You transfer the amount (from €3 / $3 / CHF 3 per month) by ordinary bank transfer, including
|
||||
the reference.
|
||||
</li>
|
||||
<li>
|
||||
Transfers are matched manually. Once your contribution arrives - usually within 24 hours, at
|
||||
most a few days - you receive a personal access key by email.
|
||||
</li>
|
||||
<li>
|
||||
You paste the key into Drizz.li once; it is stored on your device and verified automatically.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. No auto-renewal</h2>
|
||||
<p>
|
||||
Contributions are one-off and prepaid: nothing renews automatically and we never charge you.
|
||||
When your period ends, the extras simply lock again; contributing again with the same email
|
||||
extends your existing key.
|
||||
</p>
|
||||
|
||||
<h2>4. Withdrawal and refunds</h2>
|
||||
<p>
|
||||
If you are a consumer in the EU/EEA, you have a statutory 14-day right of withdrawal. Beyond
|
||||
that, we keep it simple: if you are unhappy for any reason within 14 days of receiving your
|
||||
access key, email
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a> and we will refund your
|
||||
contribution in full.
|
||||
</p>
|
||||
|
||||
<h2>5. Fair use</h2>
|
||||
<p>
|
||||
The access key is personal. Please don't publish or share it; keys that are clearly abused (e.g.
|
||||
shared publicly) may be revoked. If your key is revoked without cause, you are entitled to a
|
||||
pro-rata refund.
|
||||
</p>
|
||||
|
||||
<h2>6. Service and availability</h2>
|
||||
<p>
|
||||
Drizz.li is a personal open-source project, provided as-is. We work to keep it available and
|
||||
accurate, but we cannot guarantee uninterrupted availability or the correctness of forecasts -
|
||||
weather data is informational only (see the disclaimer in the imprint). If the supporter extras
|
||||
become permanently unavailable during a period you paid for, we will refund the remaining period
|
||||
on request.
|
||||
</p>
|
||||
|
||||
<h2>7. Data</h2>
|
||||
<p>
|
||||
How we handle your data (email, payment reference, bank transfer details) is described in the
|
||||
<a href="/legal/privacy/">privacy policy</a>.
|
||||
</p>
|
||||
|
||||
<h2>8. Governing law</h2>
|
||||
<p>
|
||||
These terms are governed by the law of [Switzerland], without prejudice to mandatory consumer
|
||||
protection provisions of your country of residence.
|
||||
</p>
|
||||
</ProsePage>
|
||||
<LocalizedContent variants={{ en, de, es, fr, it }} />
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Unterstützerbedingungen" subtitle="Zuletzt aktualisiert: 1. August 2026">
|
||||
<p>
|
||||
Drizz.li ist kostenlos nutzbar. Diese Bedingungen betreffen den freiwilligen
|
||||
Unterstützerbeitrag: eine kleine Zahlung, die das Projekt am Laufen hält und als Dankeschön die
|
||||
Unterstützer-Extras freischaltet. Auch wenn wir von einem Beitrag sprechen, werden dafür
|
||||
Funktionen freigeschaltet.
|
||||
</p>
|
||||
|
||||
<h2>1. Was Sie erhalten</h2>
|
||||
<p>
|
||||
Ein Unterstützerbeitrag schaltet die Extras für den bezahlten Zeitraum frei: derzeit
|
||||
historisches Wetter mit Vergleich zu den Klimanormalen sowie neue Funktionen, sobald sie
|
||||
erscheinen. Die kostenlosen Teile von Drizz.li bleiben für alle frei.
|
||||
</p>
|
||||
|
||||
<h2>2. Ablauf</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Sie fordern die Überweisungsdaten auf der Anmeldeseite an; wir senden sie Ihnen zusammen mit
|
||||
einer persönlichen Zahlungsreferenz per E-Mail.
|
||||
</li>
|
||||
<li>
|
||||
Sie überweisen den Betrag (ab 3 € / 3 $ / 3 CHF pro Monat) per gewöhnlicher Banküberweisung
|
||||
unter Angabe der Referenz.
|
||||
</li>
|
||||
<li>
|
||||
Überweisungen werden manuell zugeordnet. Sobald Ihr Beitrag eingeht - meist innerhalb von 24
|
||||
Stunden, höchstens einige Tage - erhalten Sie per E-Mail einen persönlichen Zugangsschlüssel.
|
||||
</li>
|
||||
<li>
|
||||
Sie fügen den Schlüssel einmal in Drizz.li ein; er wird auf Ihrem Gerät gespeichert und
|
||||
automatisch überprüft.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Keine automatische Verlängerung</h2>
|
||||
<p>
|
||||
Beiträge sind einmalig und im Voraus bezahlt: Nichts verlängert sich automatisch und wir
|
||||
belasten Sie nie. Läuft Ihr Zeitraum ab, sperren sich die Extras einfach wieder; ein erneuter
|
||||
Beitrag mit derselben E-Mail verlängert Ihren bestehenden Schlüssel.
|
||||
</p>
|
||||
|
||||
<h2>4. Widerruf und Erstattung</h2>
|
||||
<p>
|
||||
Als Verbraucherin oder Verbraucher in der EU/im EWR haben Sie ein gesetzliches 14-tägiges
|
||||
Widerrufsrecht. Darüber hinaus halten wir es einfach: Sind Sie innerhalb von 14 Tagen nach
|
||||
Erhalt Ihres Zugangsschlüssels aus irgendeinem Grund unzufrieden, schreiben Sie an
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
und wir erstatten Ihren Beitrag vollständig.
|
||||
</p>
|
||||
|
||||
<h2>5. Faire Nutzung</h2>
|
||||
<p>
|
||||
Der Zugangsschlüssel ist persönlich. Bitte veröffentlichen oder teilen Sie ihn nicht; eindeutig
|
||||
missbrauchte Schlüssel (z. B. öffentlich geteilt) können gesperrt werden. Wird Ihr Schlüssel
|
||||
ohne Grund gesperrt, haben Sie Anspruch auf eine anteilige Erstattung.
|
||||
</p>
|
||||
|
||||
<h2>6. Dienst und Verfügbarkeit</h2>
|
||||
<p>
|
||||
Drizz.li ist ein persönliches Open-Source-Projekt und wird ohne Gewähr bereitgestellt. Wir
|
||||
bemühen uns um Verfügbarkeit und Genauigkeit, können aber weder unterbrechungsfreien Betrieb
|
||||
noch die Richtigkeit der Vorhersagen garantieren - Wetterdaten sind rein informativ (siehe
|
||||
Haftungsausschluss im Impressum). Sollten die Unterstützer-Extras während eines bezahlten
|
||||
Zeitraums dauerhaft ausfallen, erstatten wir auf Anfrage den Restzeitraum.
|
||||
</p>
|
||||
|
||||
<h2>7. Daten</h2>
|
||||
<p>
|
||||
Wie wir Ihre Daten (E-Mail, Zahlungsreferenz, Überweisungsdaten) verarbeiten, steht in der
|
||||
<a href={href('/legal/privacy')}>Datenschutzerklärung</a>.
|
||||
</p>
|
||||
|
||||
<h2>8. Anwendbares Recht</h2>
|
||||
<p>
|
||||
Für diese Bedingungen gilt das Recht der [Schweiz], unbeschadet zwingender
|
||||
Verbraucherschutzvorschriften Ihres Wohnsitzlandes.
|
||||
</p>
|
||||
|
||||
<p class="text-muted-foreground">Massgeblich ist die englische Fassung dieser Bedingungen.</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Supporter terms" subtitle="Last updated: 1 August 2026">
|
||||
<p>
|
||||
Drizz.li is free to use. These terms cover the optional supporter contribution: a small payment
|
||||
that helps keep the project running and, as a thank-you, unlocks the supporter extras. Although
|
||||
we call it a contribution, features are unlocked in return.
|
||||
</p>
|
||||
|
||||
<h2>1. What you get</h2>
|
||||
<p>
|
||||
A supporter contribution unlocks the supporter extras for the paid period: currently historical
|
||||
weather with climate-normal comparisons, plus new supporter features as they land. The free
|
||||
parts of Drizz.li stay free for everyone.
|
||||
</p>
|
||||
|
||||
<h2>2. How it works</h2>
|
||||
<ul>
|
||||
<li>
|
||||
You request the transfer details on the signup page; we email them to you together with a
|
||||
personal payment reference.
|
||||
</li>
|
||||
<li>
|
||||
You transfer the amount (from €3 / $3 / CHF 3 per month) by ordinary bank transfer, including
|
||||
the reference.
|
||||
</li>
|
||||
<li>
|
||||
Transfers are matched manually. Once your contribution arrives - usually within 24 hours, at
|
||||
most a few days - you receive a personal access key by email.
|
||||
</li>
|
||||
<li>
|
||||
You paste the key into Drizz.li once; it is stored on your device and verified automatically.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. No auto-renewal</h2>
|
||||
<p>
|
||||
Contributions are one-off and prepaid: nothing renews automatically and we never charge you.
|
||||
When your period ends, the extras simply lock again; contributing again with the same email
|
||||
extends your existing key.
|
||||
</p>
|
||||
|
||||
<h2>4. Withdrawal and refunds</h2>
|
||||
<p>
|
||||
If you are a consumer in the EU/EEA, you have a statutory 14-day right of withdrawal. Beyond
|
||||
that, we keep it simple: if you are unhappy for any reason within 14 days of receiving your
|
||||
access key, email
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a> and we will refund your
|
||||
contribution in full.
|
||||
</p>
|
||||
|
||||
<h2>5. Fair use</h2>
|
||||
<p>
|
||||
The access key is personal. Please don't publish or share it; keys that are clearly abused (e.g.
|
||||
shared publicly) may be revoked. If your key is revoked without cause, you are entitled to a
|
||||
pro-rata refund.
|
||||
</p>
|
||||
|
||||
<h2>6. Service and availability</h2>
|
||||
<p>
|
||||
Drizz.li is a personal open-source project, provided as-is. We work to keep it available and
|
||||
accurate, but we cannot guarantee uninterrupted availability or the correctness of forecasts -
|
||||
weather data is informational only (see the disclaimer in the imprint). If the supporter extras
|
||||
become permanently unavailable during a period you paid for, we will refund the remaining period
|
||||
on request.
|
||||
</p>
|
||||
|
||||
<h2>7. Data</h2>
|
||||
<p>
|
||||
How we handle your data (email, payment reference, bank transfer details) is described in the
|
||||
<a href={href('/legal/privacy')}>privacy policy</a>.
|
||||
</p>
|
||||
|
||||
<h2>8. Governing law</h2>
|
||||
<p>
|
||||
These terms are governed by the law of [Switzerland], without prejudice to mandatory consumer
|
||||
protection provisions of your country of residence.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage
|
||||
title="Condiciones para colaboradores"
|
||||
subtitle="Última actualización: 1 de agosto de 2026"
|
||||
>
|
||||
<p>
|
||||
Drizz.li es gratuito. Estas condiciones cubren la aportación voluntaria de colaborador: un
|
||||
pequeño pago que ayuda a mantener el proyecto y que, como agradecimiento, desbloquea los extras.
|
||||
Aunque lo llamamos aportación, a cambio se desbloquean funciones.
|
||||
</p>
|
||||
|
||||
<h2>1. Qué obtienes</h2>
|
||||
<p>
|
||||
Una aportación desbloquea los extras durante el periodo pagado: actualmente el clima histórico
|
||||
con comparación frente a las normales climáticas, además de las nuevas funciones que vayan
|
||||
llegando. Las partes gratuitas de Drizz.li siguen siendo gratuitas para todos.
|
||||
</p>
|
||||
|
||||
<h2>2. Cómo funciona</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Solicitas los datos de la transferencia en la página de registro; te los enviamos por correo
|
||||
junto con una referencia de pago personal.
|
||||
</li>
|
||||
<li>
|
||||
Transfieres el importe (desde 3 € / 3 $ / 3 CHF al mes) mediante una transferencia bancaria
|
||||
ordinaria, incluyendo la referencia.
|
||||
</li>
|
||||
<li>
|
||||
Las transferencias se concilian manualmente. Cuando llega tu aportación - normalmente en 24
|
||||
horas, como mucho unos días - recibes por correo una clave de acceso personal.
|
||||
</li>
|
||||
<li>
|
||||
Pegas la clave en Drizz.li una vez; se guarda en tu dispositivo y se verifica automáticamente.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Sin renovación automática</h2>
|
||||
<p>
|
||||
Las aportaciones son únicas y prepagadas: nada se renueva solo y nunca te cobramos. Cuando
|
||||
termina tu periodo, los extras simplemente se bloquean de nuevo; volver a aportar con el mismo
|
||||
correo amplía tu clave existente.
|
||||
</p>
|
||||
|
||||
<h2>4. Desistimiento y reembolsos</h2>
|
||||
<p>
|
||||
Si eres consumidor en la UE/EEE, dispones de un derecho legal de desistimiento de 14 días. Más
|
||||
allá de eso, lo mantenemos sencillo: si no estás satisfecho por cualquier motivo en los 14 días
|
||||
siguientes a recibir tu clave, escribe a
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
y te reembolsaremos la aportación íntegra.
|
||||
</p>
|
||||
|
||||
<h2>5. Uso razonable</h2>
|
||||
<p>
|
||||
La clave de acceso es personal. No la publiques ni la compartas; las claves claramente abusadas
|
||||
(por ejemplo, compartidas públicamente) pueden revocarse. Si tu clave se revoca sin causa,
|
||||
tienes derecho a un reembolso proporcional.
|
||||
</p>
|
||||
|
||||
<h2>6. Servicio y disponibilidad</h2>
|
||||
<p>
|
||||
Drizz.li es un proyecto personal de código abierto, ofrecido tal cual. Trabajamos para
|
||||
mantenerlo disponible y preciso, pero no podemos garantizar disponibilidad ininterrumpida ni la
|
||||
exactitud de los pronósticos: los datos meteorológicos son solo informativos (véase el descargo
|
||||
del aviso legal). Si los extras dejan de estar disponibles de forma permanente durante un
|
||||
periodo que has pagado, reembolsaremos el periodo restante a petición.
|
||||
</p>
|
||||
|
||||
<h2>7. Datos</h2>
|
||||
<p>
|
||||
Cómo tratamos tus datos (correo, referencia de pago, datos de la transferencia) se describe en
|
||||
la
|
||||
<a href={href('/legal/privacy')}>política de privacidad</a>.
|
||||
</p>
|
||||
|
||||
<h2>8. Legislación aplicable</h2>
|
||||
<p>
|
||||
Estas condiciones se rigen por la legislación de [Suiza], sin perjuicio de las disposiciones
|
||||
imperativas de protección al consumidor de tu país de residencia.
|
||||
</p>
|
||||
|
||||
<p class="text-muted-foreground">
|
||||
En caso de discrepancia prevalece la versión inglesa de estas condiciones.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Conditions contributeur" subtitle="Dernière mise à jour : 1er août 2026">
|
||||
<p>
|
||||
Drizz.li est gratuit. Ces conditions portent sur la contribution facultative : un petit paiement
|
||||
qui aide à faire vivre le projet et qui, en remerciement, débloque les bonus. Même si nous
|
||||
parlons de contribution, des fonctionnalités sont débloquées en retour.
|
||||
</p>
|
||||
|
||||
<h2>1. Ce que vous obtenez</h2>
|
||||
<p>
|
||||
Une contribution débloque les bonus pour la période payée : actuellement la météo historique
|
||||
avec comparaison aux normales climatiques, ainsi que les nouvelles fonctions à venir. Les
|
||||
parties gratuites de Drizz.li le restent pour tout le monde.
|
||||
</p>
|
||||
|
||||
<h2>2. Fonctionnement</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Vous demandez les coordonnées bancaires sur la page d'inscription ; nous vous les envoyons par
|
||||
e-mail avec une référence de paiement personnelle.
|
||||
</li>
|
||||
<li>
|
||||
Vous virez le montant (à partir de 3 € / 3 $ / 3 CHF par mois) par virement bancaire
|
||||
ordinaire, en indiquant la référence.
|
||||
</li>
|
||||
<li>
|
||||
Les virements sont rapprochés manuellement. Dès réception de votre contribution - en général
|
||||
sous 24 heures, au plus quelques jours - vous recevez une clé d'accès personnelle par e-mail.
|
||||
</li>
|
||||
<li>
|
||||
Vous collez la clé dans Drizz.li une seule fois ; elle est stockée sur votre appareil et
|
||||
vérifiée automatiquement.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Pas de reconduction automatique</h2>
|
||||
<p>
|
||||
Les contributions sont ponctuelles et prépayées : rien ne se reconduit et nous ne vous prélevons
|
||||
jamais. À la fin de votre période, les bonus se reverrouillent simplement ; contribuer à nouveau
|
||||
avec la même adresse prolonge votre clé existante.
|
||||
</p>
|
||||
|
||||
<h2>4. Rétractation et remboursement</h2>
|
||||
<p>
|
||||
Si vous êtes consommateur dans l'UE/EEE, vous disposez d'un droit légal de rétractation de 14
|
||||
jours. Au-delà, nous faisons simple : si vous n'êtes pas satisfait, pour quelque raison que ce
|
||||
soit, dans les 14 jours suivant la réception de votre clé, écrivez à
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
et nous vous remboursons intégralement.
|
||||
</p>
|
||||
|
||||
<h2>5. Usage loyal</h2>
|
||||
<p>
|
||||
La clé d'accès est personnelle. Merci de ne pas la publier ni la partager ; les clés
|
||||
manifestement détournées (par exemple partagées publiquement) peuvent être révoquées. Si votre
|
||||
clé est révoquée sans motif, vous avez droit à un remboursement au prorata.
|
||||
</p>
|
||||
|
||||
<h2>6. Service et disponibilité</h2>
|
||||
<p>
|
||||
Drizz.li est un projet personnel open source, fourni en l'état. Nous nous efforçons de le
|
||||
maintenir disponible et exact, mais nous ne pouvons garantir ni une disponibilité ininterrompue
|
||||
ni l'exactitude des prévisions - les données météo sont purement informatives (voir
|
||||
l'avertissement dans les mentions légales). Si les bonus devenaient définitivement indisponibles
|
||||
pendant une période payée, nous rembourserions la période restante sur demande.
|
||||
</p>
|
||||
|
||||
<h2>7. Données</h2>
|
||||
<p>
|
||||
La façon dont nous traitons vos données (e-mail, référence de paiement, informations de
|
||||
virement) est décrite dans la
|
||||
<a href={href('/legal/privacy')}>politique de confidentialité</a>.
|
||||
</p>
|
||||
|
||||
<h2>8. Droit applicable</h2>
|
||||
<p>
|
||||
Ces conditions sont régies par le droit de la [Suisse], sans préjudice des dispositions
|
||||
impératives de protection des consommateurs de votre pays de résidence.
|
||||
</p>
|
||||
|
||||
<p class="text-muted-foreground">
|
||||
En cas de divergence, la version anglaise de ces conditions fait foi.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Condizioni per i sostenitori" subtitle="Ultimo aggiornamento: 1 agosto 2026">
|
||||
<p>
|
||||
Drizz.li è gratuito. Queste condizioni riguardano il contributo facoltativo di sostegno: un
|
||||
piccolo pagamento che aiuta a mantenere vivo il progetto e che, come ringraziamento, sblocca gli
|
||||
extra. Anche se lo chiamiamo contributo, in cambio vengono sbloccate delle funzioni.
|
||||
</p>
|
||||
|
||||
<h2>1. Cosa ottieni</h2>
|
||||
<p>
|
||||
Un contributo sblocca gli extra per il periodo pagato: attualmente il meteo storico con
|
||||
confronto rispetto alle normali climatiche, oltre alle nuove funzioni man mano che arrivano. Le
|
||||
parti gratuite di Drizz.li restano gratuite per tutti.
|
||||
</p>
|
||||
|
||||
<h2>2. Come funziona</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Richiedi i dati per il bonifico nella pagina di iscrizione; te li inviamo via e-mail insieme a
|
||||
un riferimento di pagamento personale.
|
||||
</li>
|
||||
<li>
|
||||
Effettui il bonifico (da 3 € / 3 $ / 3 CHF al mese) con un normale bonifico bancario,
|
||||
indicando il riferimento.
|
||||
</li>
|
||||
<li>
|
||||
I bonifici vengono abbinati manualmente. Appena il contributo arriva - di solito entro 24 ore,
|
||||
al massimo pochi giorni - ricevi via e-mail una chiave di accesso personale.
|
||||
</li>
|
||||
<li>
|
||||
Incolli la chiave in Drizz.li una volta sola; viene salvata sul tuo dispositivo e verificata
|
||||
automaticamente.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Nessun rinnovo automatico</h2>
|
||||
<p>
|
||||
I contributi sono una tantum e prepagati: nulla si rinnova da solo e non ti addebitiamo mai
|
||||
nulla. Alla fine del periodo gli extra si bloccano semplicemente di nuovo; un nuovo contributo
|
||||
con la stessa e-mail estende la chiave esistente.
|
||||
</p>
|
||||
|
||||
<h2>4. Recesso e rimborsi</h2>
|
||||
<p>
|
||||
Se sei un consumatore nell'UE/SEE, hai un diritto di recesso legale di 14 giorni. Oltre a
|
||||
questo, la teniamo semplice: se per qualsiasi motivo non sei soddisfatto entro 14 giorni dalla
|
||||
ricezione della chiave, scrivi a
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
e ti rimborseremo l'intero contributo.
|
||||
</p>
|
||||
|
||||
<h2>5. Uso corretto</h2>
|
||||
<p>
|
||||
La chiave di accesso è personale. Non pubblicarla né condividerla; le chiavi chiaramente abusate
|
||||
(ad esempio condivise pubblicamente) possono essere revocate. Se la tua chiave viene revocata
|
||||
senza motivo, hai diritto a un rimborso proporzionale.
|
||||
</p>
|
||||
|
||||
<h2>6. Servizio e disponibilità</h2>
|
||||
<p>
|
||||
Drizz.li è un progetto personale open source, fornito così com'è. Lavoriamo per mantenerlo
|
||||
disponibile e accurato, ma non possiamo garantire né la continuità del servizio né la
|
||||
correttezza delle previsioni: i dati meteo sono solo informativi (vedi l'avvertenza nelle note
|
||||
legali). Se gli extra dovessero diventare permanentemente non disponibili durante un periodo
|
||||
pagato, rimborseremo su richiesta il periodo residuo.
|
||||
</p>
|
||||
|
||||
<h2>7. Dati</h2>
|
||||
<p>
|
||||
Il trattamento dei tuoi dati (e-mail, riferimento di pagamento, dati del bonifico) è descritto
|
||||
nell'
|
||||
<a href={href('/legal/privacy')}>informativa sulla privacy</a>.
|
||||
</p>
|
||||
|
||||
<h2>8. Legge applicabile</h2>
|
||||
<p>
|
||||
Queste condizioni sono regolate dal diritto della [Svizzera], fatte salve le disposizioni
|
||||
imperative di tutela del consumatore del tuo paese di residenza.
|
||||
</p>
|
||||
|
||||
<p class="text-muted-foreground">
|
||||
In caso di discrepanza prevale la versione inglese di queste condizioni.
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -327,8 +327,7 @@
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
This model's ensemble only reaches about <strong>{validDays} days</strong> ahead, the spread is
|
||||
trimmed to its available range.
|
||||
{m.ensemble_trimmed({ days: validDays })}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -385,7 +384,9 @@
|
||||
{#snippet controls()}
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
|
||||
<Label for="show_legend" class="cursor-pointer text-base leading-none">Show legend</Label>
|
||||
<Label for="show_legend" class="cursor-pointer text-base leading-none"
|
||||
>{m.legend_show()}</Label
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ChartToolbar>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
groupRange,
|
||||
isColumnUnit
|
||||
} from '$lib/charts';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import {
|
||||
type DaylightBand,
|
||||
type ModelCompareResult,
|
||||
@@ -315,10 +316,10 @@
|
||||
<!-- Range / zoom controls, aligned with the title like the other pages -->
|
||||
<div class="lg:absolute lg:right-0 lg:top-20 z-40 flex flex-wrap items-center gap-3">
|
||||
<span class="hidden text-xs text-muted-foreground lg:inline">
|
||||
drag or
|
||||
{m.meteograms_zoom_hint()}
|
||||
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]">Ctrl</kbd
|
||||
>
|
||||
+ scroll to zoom
|
||||
{m.meteograms_zoom_hint_end()}
|
||||
</span>
|
||||
{#if zoomActive}
|
||||
<button
|
||||
@@ -336,13 +337,13 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v6h6M20 20v-6h-6" />
|
||||
<path stroke-linecap="round" d="M20 10a8 8 0 0 0-14-4M4 14a8 8 0 0 0 14 4" />
|
||||
</svg>
|
||||
Reset zoom
|
||||
{m.reset_zoom()}
|
||||
</button>
|
||||
{/if}
|
||||
<div
|
||||
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
|
||||
role="group"
|
||||
aria-label="Chart time range"
|
||||
aria-label={m.range_group_aria()}
|
||||
>
|
||||
{#each rangePresets as preset (preset.label)}
|
||||
<button
|
||||
@@ -426,7 +427,9 @@
|
||||
{#snippet controls()}
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
|
||||
<Label for="show_legend" class="cursor-pointer text-base leading-none">Show legend</Label>
|
||||
<Label for="show_legend" class="cursor-pointer text-base leading-none"
|
||||
>{m.legend_show()}</Label
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ChartToolbar>
|
||||
@@ -436,7 +439,9 @@
|
||||
|
||||
<div class="mt-4 md:mt-8">
|
||||
<div class="flex">
|
||||
<a href="#models"><h2 id="models" class="text-2xl md:text-3xl">Models</h2></a>
|
||||
<a href="#models"
|
||||
><h2 id="models" class="text-2xl md:text-3xl">{m.compare_models_heading()}</h2></a
|
||||
>
|
||||
{#if params.models && params.models.length > 0}
|
||||
<div transition:fade={{ duration: 200 }} class="relative mt-1.25">
|
||||
<div
|
||||
@@ -487,7 +492,7 @@
|
||||
<div class="flex">
|
||||
<a href="#hourly_weather_variables"
|
||||
><h2 id="hourly_weather_variables" class="text-2xl md:text-3xl">
|
||||
Hourly Weather Variables
|
||||
{m.compare_variables_heading()}
|
||||
</h2></a
|
||||
>
|
||||
{#if params.hourly && params.hourly.length > 0}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { formatZoned, getZonedHour } from '$lib/utils/date';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { getWeatherIconName } from '../../utils/weather-codes';
|
||||
|
||||
interface Props {
|
||||
@@ -69,14 +71,14 @@
|
||||
{#if displayModels.length > 0}
|
||||
<div class="mt-8">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold">Model Comparison Timeline</h3>
|
||||
<h3 class="text-xl font-bold">{m.compare_timeline_title()}</h3>
|
||||
<div class="flex items-center gap-1.5 text-[13px] font-semibold">
|
||||
<span class="select-none text-muted-foreground">3h</span>
|
||||
<button
|
||||
class="relative h-6 w-11 cursor-pointer rounded-full border transition-colors
|
||||
{hourlyInterval === 1 ? 'border-primary/40 bg-primary' : 'border-border bg-muted'}"
|
||||
onclick={() => (hourlyInterval = hourlyInterval === 1 ? 3 : 1)}
|
||||
title="Toggle between 1-hour and 3-hour intervals"
|
||||
title={m.interval_toggle()}
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.75 size-4.5 rounded-full bg-white shadow-sm transition-[left] duration-200
|
||||
@@ -96,7 +98,7 @@
|
||||
<th
|
||||
class="sticky left-0 z-20 w-32 border-b border-r border-border bg-muted/95 p-2 text-left text-xs font-bold"
|
||||
>
|
||||
Model
|
||||
{m.compare_model_label()}
|
||||
</th>
|
||||
{#each filteredIndices as idx, i (idx)}
|
||||
{@const ts = timestamps[idx]}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { get } from 'svelte/store';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { reportPageReady } from '$lib/stores/page-transition.svelte';
|
||||
import {
|
||||
storedArchiveModel,
|
||||
storedChartLayout,
|
||||
storedLocation,
|
||||
storedUnits,
|
||||
@@ -13,6 +15,7 @@
|
||||
|
||||
import { ChartContainer } from '$lib/components/charts';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import PaywallGate from '$lib/paywall/PaywallGate.svelte';
|
||||
import { isSupporter } from '$lib/paywall/supporter';
|
||||
import {
|
||||
@@ -22,8 +25,10 @@
|
||||
fetchHistoricalWeather
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { defaultParameters } from '../../options';
|
||||
import { useHeroActions } from '../../hero.svelte';
|
||||
import { archiveModelGroups, defaultParameters } from '../../options';
|
||||
import HourlyTable from '../../week/[location]/HourlyTable.svelte';
|
||||
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
|
||||
import { neededHourlyApiVars } from '../../week/[location]/variables';
|
||||
import DateRangeControls from './DateRangeControls.svelte';
|
||||
import HistoricalDaily from './HistoricalDaily.svelte';
|
||||
@@ -37,6 +42,8 @@
|
||||
// the page cross-fade waits for this before revealing the new page
|
||||
reportPageReady(() => result != null);
|
||||
|
||||
useHeroActions(heroActions);
|
||||
|
||||
let location = $derived(data.location);
|
||||
$effect(() => {
|
||||
storedLocation.set(data.location);
|
||||
@@ -79,6 +86,11 @@
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
let archiveModel = $state('best_match');
|
||||
onMount(() => {
|
||||
archiveModel = get(storedArchiveModel);
|
||||
});
|
||||
|
||||
function onRangeChange(s: string, e: string) {
|
||||
startDate = s;
|
||||
endDate = e;
|
||||
@@ -101,6 +113,8 @@
|
||||
const s = startDate;
|
||||
const e = endDate;
|
||||
const vars = hourlyVars;
|
||||
const model = archiveModel;
|
||||
void model;
|
||||
if (!mounted || !$isSupporter || !loc || !s || !e) return;
|
||||
|
||||
const version = ++requestVersion;
|
||||
@@ -203,12 +217,27 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- the reanalysis picker rides in the layout's location row -->
|
||||
{#snippet heroActions()}
|
||||
<div class="lg:absolute lg:right-0 lg:top-0 flex w-full min-w-0 items-center gap-3 sm:w-auto">
|
||||
<ModelSelector
|
||||
selectedModel={archiveModel}
|
||||
groups={archiveModelGroups}
|
||||
label={m.model_archive()}
|
||||
onModelChange={(model) => {
|
||||
archiveModel = model;
|
||||
storedArchiveModel.set(model);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<svelte:head>
|
||||
<title>Drizz.li | Historical weather</title>
|
||||
<title>Drizz.li | {m.page_historical_subtitle()}</title>
|
||||
<meta name="description" content="Past weather and climate-normal comparisons for any location" />
|
||||
</svelte:head>
|
||||
|
||||
<PaywallGate feature="Historical weather">
|
||||
<PaywallGate feature={m.page_historical_subtitle()}>
|
||||
<DateRangeControls
|
||||
start={startDate}
|
||||
end={endDate}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
interface Props {
|
||||
start: string;
|
||||
end: string;
|
||||
@@ -34,10 +36,10 @@
|
||||
}
|
||||
|
||||
const presets = [
|
||||
{ label: '7 days', apply: () => applyLastDays(7) },
|
||||
{ label: '30 days', apply: () => applyLastDays(30) },
|
||||
{ label: '90 days', apply: () => applyLastDays(90) },
|
||||
{ label: 'Month, last year', apply: applyThisMonthLastYear }
|
||||
{ label: () => m.historical_last_days({ days: 7 }), apply: () => applyLastDays(7) },
|
||||
{ label: () => m.historical_last_days({ days: 30 }), apply: () => applyLastDays(30) },
|
||||
{ label: () => m.historical_last_days({ days: 90 }), apply: () => applyLastDays(90) },
|
||||
{ label: () => m.historical_month_last_year(), apply: applyThisMonthLastYear }
|
||||
];
|
||||
|
||||
// ─── Manual inputs ────────────────────────────────────────────────────────────
|
||||
@@ -65,7 +67,9 @@
|
||||
>
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div class="grid gap-1">
|
||||
<label for="hist-start" class="text-xs font-semibold text-muted-foreground">From</label>
|
||||
<label for="hist-start" class="text-xs font-semibold text-muted-foreground"
|
||||
>{m.historical_from()}</label
|
||||
>
|
||||
<input
|
||||
id="hist-start"
|
||||
type="date"
|
||||
@@ -77,7 +81,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-1">
|
||||
<label for="hist-end" class="text-xs font-semibold text-muted-foreground">To</label>
|
||||
<label for="hist-end" class="text-xs font-semibold text-muted-foreground"
|
||||
>{m.historical_to()}</label
|
||||
>
|
||||
<input
|
||||
id="hist-end"
|
||||
type="date"
|
||||
@@ -93,15 +99,15 @@
|
||||
<div
|
||||
class="inline-flex flex-wrap items-center gap-0.5 rounded-lg bg-muted p-0.5 text-xs font-semibold"
|
||||
role="group"
|
||||
aria-label="Quick ranges"
|
||||
aria-label={m.historical_quick_ranges()}
|
||||
>
|
||||
{#each presets as preset (preset.label)}
|
||||
{#each presets as preset (preset.label())}
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer rounded-md px-2.5 py-1.5 whitespace-nowrap text-muted-foreground transition-colors hover:bg-background hover:text-foreground hover:shadow-sm"
|
||||
onclick={preset.apply}
|
||||
>
|
||||
{preset.label}
|
||||
{preset.label()}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { formatZoned, isSameDayInZone } from '$lib/utils/date';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import {
|
||||
type ClimateNormals,
|
||||
type HistoricalDailyData,
|
||||
@@ -104,7 +105,7 @@
|
||||
<!-- ─── KPI tiles ────────────────────────────────────────────────────────────── -->
|
||||
<div class="grid grid-cols-2 gap-2.5 lg:grid-cols-4">
|
||||
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
|
||||
<p class="text-xs font-medium text-muted-foreground">Average temperature</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{m.stat_avg_temperature()}</p>
|
||||
<p class="mt-1 text-2xl font-bold tabular-nums">
|
||||
{fmtTemp(stats.avg)}<span class="text-base font-semibold text-muted-foreground"
|
||||
>{tempUnit.replace('°', '')}</span
|
||||
@@ -118,15 +119,15 @@
|
||||
class:dark:text-red-400={stats.tempAnomaly >= 0}
|
||||
class:dark:text-blue-400={stats.tempAnomaly < 0}
|
||||
>
|
||||
{fmtSigned(stats.tempAnomaly)} vs normal
|
||||
{m.anomaly_vs_normal({ value: fmtSigned(stats.tempAnomaly) })}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">1991–2020 normal loading…</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">{m.normals_loading_period()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
|
||||
<p class="text-xs font-medium text-muted-foreground">Total precipitation</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{m.stat_total_precipitation()}</p>
|
||||
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtPrecip(stats.totalPrecip)}</p>
|
||||
{#if stats.normalPrecip != null}
|
||||
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
|
||||
@@ -140,7 +141,7 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
|
||||
<p class="text-xs font-medium text-muted-foreground">Warmest day</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{m.stat_warmest_day()}</p>
|
||||
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtTemp(stats.warm.t)}</p>
|
||||
{#if stats.warm.i >= 0}
|
||||
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
|
||||
@@ -150,7 +151,7 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
|
||||
<p class="text-xs font-medium text-muted-foreground">Coldest day</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{m.stat_coldest_day()}</p>
|
||||
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtTemp(stats.cold.t)}</p>
|
||||
{#if stats.cold.i >= 0}
|
||||
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
|
||||
@@ -166,12 +167,12 @@
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-border/70 bg-muted/40 px-4 py-2.5">
|
||||
<h3 class="text-base font-bold">
|
||||
Daily <span class="font-semibold text-muted-foreground">– select a day for hourly detail</span
|
||||
>
|
||||
{m.historical_daily_heading()}
|
||||
<span class="font-semibold text-muted-foreground">{m.historical_daily_hint()}</span>
|
||||
</h3>
|
||||
{#if normals}
|
||||
<span class="hidden text-xs text-muted-foreground sm:inline">
|
||||
normal band = 1991–2020 mean
|
||||
{m.normal_band_legend()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -242,7 +243,7 @@
|
||||
<span
|
||||
class="mt-0.5 inline-block h-1.5 w-6 rounded-full"
|
||||
style="background:{anomalyColor(anomaly)}"
|
||||
title="{fmtSigned(anomaly)} vs normal"
|
||||
title={m.anomaly_vs_normal({ value: fmtSigned(anomaly) })}
|
||||
></span>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { ChartContainer, downloadChartsPng } from '$lib/components/charts';
|
||||
|
||||
import { CanvasChart, groupRange } from '$lib/charts';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { getWeatherIconName } from '../../utils/weather-codes';
|
||||
import { type FetchedHourly, type WeatherUnits } from '../../week/[location]/types';
|
||||
@@ -121,22 +122,23 @@
|
||||
<section class="mt-8" transition:fade={{ duration: 200 }}>
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 class="text-lg font-bold">
|
||||
Meteograms <span class="font-semibold text-muted-foreground">– full range</span>
|
||||
{m.meteograms_heading()}
|
||||
<span class="font-semibold text-muted-foreground">{m.historical_full_range()}</span>
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="hidden text-xs text-muted-foreground md:inline">
|
||||
drag or
|
||||
{m.meteograms_zoom_hint()}
|
||||
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
|
||||
>Ctrl</kbd
|
||||
>
|
||||
+ scroll to zoom
|
||||
{m.meteograms_zoom_hint_end()}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={liveCharts.length === 0 || downloadingPng}
|
||||
onclick={downloadPng}
|
||||
title="Download meteogram as PNG image"
|
||||
title={m.chart_download()}
|
||||
>
|
||||
{#if downloadingPng}Rendering…{:else}PNG{/if}
|
||||
</button>
|
||||
@@ -148,7 +150,7 @@
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
|
||||
onclick={resetZoom}
|
||||
>
|
||||
Reset zoom
|
||||
{m.reset_zoom()}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -158,7 +160,7 @@
|
||||
<div
|
||||
class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
No meteograms configured. Add variables from the 7-day forecast page.
|
||||
{m.meteograms_none_historical()}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="-mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border">
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { mapsDomainForModel } from '$lib/utils/maps-domain';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
// Hash piping, both directions:
|
||||
// - inbound: a #zoom/lat/lng(/bearing/pitch) hash on OUR url seeds the
|
||||
// map, so positions can be bookmarked/shared via drizzli links
|
||||
@@ -74,7 +76,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Weather Map | Open-Meteo.com</title>
|
||||
<title>{m.page_maps_title()} | Open-Meteo.com</title>
|
||||
<link rel="canonical" href="https://open-meteo.com/weather/maps" />
|
||||
<meta name="description" content="Interactive weather map powered by Open-Meteo" />
|
||||
</svelte:head>
|
||||
@@ -88,7 +90,7 @@
|
||||
<iframe
|
||||
bind:this={iframeEl}
|
||||
src={iframeSrc}
|
||||
title="Open-Meteo Interactive Map"
|
||||
title={m.maps_iframe_title()}
|
||||
loading="lazy"
|
||||
allowfullscreen
|
||||
allow="cross-origin-isolated"
|
||||
|
||||
@@ -513,3 +513,48 @@ export function inDomainCity(model: string): { slug: string; label: string } | n
|
||||
const hit = IN_DOMAIN_CITY.find(([prefix]) => model.startsWith(prefix));
|
||||
return hit ? hit[1] : null;
|
||||
}
|
||||
|
||||
/** Reanalyses the archive API accepts (verified against archive-api.open-meteo.com). */
|
||||
export const archiveModelGroups: WeatherModelGroup[] = [
|
||||
{
|
||||
value: 'auto',
|
||||
label: 'Automatic',
|
||||
models: [{ value: 'best_match', label: 'Best match', resolution: 'varies', update: 'daily' }]
|
||||
},
|
||||
{
|
||||
value: 'era5',
|
||||
label: 'ECMWF reanalysis',
|
||||
models: [
|
||||
{ value: 'era5_seamless', label: 'ERA5 seamless', resolution: '9-25 km', update: 'daily' },
|
||||
{ value: 'era5', label: 'ERA5', resolution: '25 km', update: 'daily' },
|
||||
{ value: 'era5_land', label: 'ERA5-Land', resolution: '9 km', update: 'daily' },
|
||||
{ value: 'ecmwf_ifs', label: 'ECMWF IFS', resolution: '9 km', update: 'daily' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 'regional',
|
||||
label: 'Regional reanalysis',
|
||||
models: [{ value: 'cerra', label: 'CERRA (Europe)', resolution: '5 km', update: 'daily' }]
|
||||
}
|
||||
];
|
||||
|
||||
/** Seasonal models the seasonal API accepts. */
|
||||
export const seasonalModelGroups: WeatherModelGroup[] = [
|
||||
{
|
||||
value: 'auto',
|
||||
label: 'Automatic',
|
||||
models: [{ value: 'best_match', label: 'Best match', resolution: 'varies', update: 'monthly' }]
|
||||
},
|
||||
{
|
||||
value: 'ecmwf',
|
||||
label: 'ECMWF',
|
||||
models: [
|
||||
{
|
||||
value: 'ecmwf_seasonal_seamless',
|
||||
label: 'ECMWF SEAS5',
|
||||
resolution: '36 km',
|
||||
update: 'monthly'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { reportPageReady } from '$lib/stores/page-transition.svelte';
|
||||
import { storedLocation, storedUnits } from '$lib/stores/settings';
|
||||
import { storedLocation, storedSeasonalModel, storedUnits } from '$lib/stores/settings';
|
||||
|
||||
import { ChartContainer, ChartToolbar } from '$lib/components/charts';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import PaywallGate from '$lib/paywall/PaywallGate.svelte';
|
||||
import { isSupporter } from '$lib/paywall/supporter';
|
||||
import {
|
||||
@@ -19,7 +21,8 @@
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { useHeroActions } from '../../hero.svelte';
|
||||
import { defaultParameters } from '../../options';
|
||||
import { defaultParameters, seasonalModelGroups } from '../../options';
|
||||
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
|
||||
import { getPrecipUnit } from '../../week/[location]/types';
|
||||
import SeasonalCharts from './SeasonalCharts.svelte';
|
||||
import SeasonalMonths from './SeasonalMonths.svelte';
|
||||
@@ -79,7 +82,9 @@
|
||||
let result = $state<SeasonalForecastResult | null>(null);
|
||||
let normals = $state<ClimateNormals | null>(null);
|
||||
|
||||
let seasonalModel = $state('best_match');
|
||||
onMount(() => {
|
||||
seasonalModel = get(storedSeasonalModel);
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
@@ -89,6 +94,7 @@
|
||||
const loc = location;
|
||||
const tempUnit = params.temperature_unit;
|
||||
const precipUnit = params.precipitation_unit;
|
||||
const model = seasonalModel;
|
||||
if (!mounted || !$isSupporter || !loc) return;
|
||||
|
||||
const version = ++requestVersion;
|
||||
@@ -99,6 +105,7 @@
|
||||
latitude: loc.latitude!,
|
||||
longitude: loc.longitude!,
|
||||
dailyVariables: SEASONAL_VARS,
|
||||
model,
|
||||
temperature_unit: tempUnit as 'celsius' | 'fahrenheit',
|
||||
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
|
||||
precipitation_unit: precipUnit as 'mm' | 'inch',
|
||||
@@ -173,7 +180,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Drizz.li | Seasonal forecast</title>
|
||||
<title>Drizz.li | {m.page_seasonal_subtitle()}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Multi-month seasonal outlook: monthly temperature and precipitation trends against the 1991-2020 climate normal"
|
||||
@@ -182,12 +189,13 @@
|
||||
|
||||
<!-- the range buttons ride in the layout's location row (see weather/+layout) -->
|
||||
{#snippet heroActions()}
|
||||
<div class="flex w-full flex-wrap items-center gap-3 sm:w-auto">
|
||||
{#if result}
|
||||
<!-- range buttons reslice the already-fetched horizon (no refetch) -->
|
||||
<div
|
||||
class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto"
|
||||
role="group"
|
||||
aria-label="Outlook range"
|
||||
aria-label={m.seasonal_range_aria()}
|
||||
>
|
||||
{#each RANGES as range, i (range.label)}
|
||||
{@const disabled = range.days !== Infinity && range.days > horizonDays}
|
||||
@@ -208,6 +216,16 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ModelSelector
|
||||
selectedModel={seasonalModel}
|
||||
groups={seasonalModelGroups}
|
||||
label={m.model_seasonal()}
|
||||
onModelChange={(model) => {
|
||||
seasonalModel = model;
|
||||
storedSeasonalModel.set(model);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- What a seasonal forecast is (and is not): without this the daily-looking
|
||||
@@ -226,17 +244,16 @@
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
</svg>
|
||||
<p class="text-muted-foreground">
|
||||
A seasonal forecast shows how a whole month is likely to
|
||||
<strong class="font-semibold text-foreground">depart from its climate normal</strong> - not the
|
||||
weather on any given day. Read the monthly trend and the ensemble agreement, not the daily
|
||||
wiggles.
|
||||
{m.seasonal_explainer_before()}
|
||||
<strong class="font-semibold text-foreground">{m.seasonal_explainer_strong()}</strong>
|
||||
{m.seasonal_explainer_after()}
|
||||
{#if lastDayLabel}
|
||||
This outlook runs to <strong class="font-semibold text-foreground">{lastDayLabel}</strong>.
|
||||
{m.seasonal_runs_to({ date: lastDayLabel })}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PaywallGate feature="The seasonal outlook">
|
||||
<PaywallGate feature={m.page_seasonal_subtitle()}>
|
||||
{#if loadError}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
||||
@@ -265,7 +282,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
|
||||
<Label for="show_legend" class="cursor-pointer text-base leading-none"
|
||||
>Show legend</Label
|
||||
>{m.legend_show()}</Label
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { getColor } from '../../utils/colors';
|
||||
import { type WeatherUnits, getPrecipUnit, getTempUnit } from '../../week/[location]/types';
|
||||
|
||||
@@ -50,7 +52,7 @@
|
||||
<!-- headline: mean temperature and its departure from the 1991-2020 normal -->
|
||||
<div class="mt-2.5 flex items-end gap-3">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-muted-foreground">Mean temperature</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{m.stat_mean_temperature()}</p>
|
||||
<p class="text-2xl leading-tight font-bold tabular-nums">
|
||||
{fmtTemp(month.tMean)}<span class="text-base font-semibold text-muted-foreground"
|
||||
>{tempUnit.replace('°', '')}</span
|
||||
@@ -70,9 +72,9 @@
|
||||
</p>
|
||||
<p class="text-[11px] text-muted-foreground">{anomalyLabel(month.anomaly)}</p>
|
||||
{:else if normals}
|
||||
<p class="text-[11px] text-muted-foreground">no normal for this month</p>
|
||||
<p class="text-[11px] text-muted-foreground">{m.seasonal_no_normal()}</p>
|
||||
{:else}
|
||||
<p class="text-[11px] text-muted-foreground">normal loading…</p>
|
||||
<p class="text-[11px] text-muted-foreground">{m.normals_loading()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@
|
||||
style="background:{getColor(month.tMax, units.temperature_unit)}"
|
||||
></span>
|
||||
<span class="font-semibold tabular-nums">{fmtTemp(month.tMax)}</span>
|
||||
<span class="text-muted-foreground">day</span>
|
||||
<span class="text-muted-foreground">{m.label_day()}</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -93,14 +95,14 @@
|
||||
style="background:{getColor(month.tMin, units.temperature_unit)}"
|
||||
></span>
|
||||
<span class="font-semibold tabular-nums">{fmtTemp(month.tMin)}</span>
|
||||
<span class="text-muted-foreground">night</span>
|
||||
<span class="text-muted-foreground">{m.label_night()}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- precipitation against its own normal, as a share bar -->
|
||||
<div class="mt-3 border-t border-border/60 pt-2.5">
|
||||
<div class="flex items-baseline justify-between gap-2 text-xs">
|
||||
<span class="font-medium text-muted-foreground">Precipitation</span>
|
||||
<span class="font-medium text-muted-foreground">{m.var_precipitation()}</span>
|
||||
<span class="font-semibold tabular-nums">
|
||||
{fmtPrecip(month.precip)}
|
||||
{precipUnit}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
import HourlyTable from './HourlyTable.svelte';
|
||||
import MeteogramCharts from './MeteogramCharts.svelte';
|
||||
import ModelSelector from './ModelSelector.svelte';
|
||||
import NearbyCities from './NearbyCities.svelte';
|
||||
import VariableSidebar from './VariableSidebar.svelte';
|
||||
import { neededHourlyApiVars } from './variables';
|
||||
|
||||
@@ -326,7 +327,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Drizz.li | Weather</title>
|
||||
<title>Drizz.li | {m.page_week_title()}</title>
|
||||
<link rel="canonical" href="https://drizz.li/weather/week" />
|
||||
<meta name="description" content="7-day weather forecast with detailed hourly data" />
|
||||
</svelte:head>
|
||||
@@ -362,7 +363,7 @@
|
||||
class="cursor-pointer rounded-md border border-destructive/40 bg-background px-3 py-1 text-xs font-semibold text-destructive transition-colors hover:bg-destructive/10"
|
||||
onclick={() => retryNonce++}
|
||||
>
|
||||
Try again
|
||||
{m.action_try_again()}
|
||||
</button>
|
||||
{#if suggestedCity}
|
||||
<a
|
||||
@@ -383,7 +384,7 @@
|
||||
</div>
|
||||
{#if loadError.detail}
|
||||
<details class="mt-2 text-xs text-destructive/70">
|
||||
<summary class="cursor-pointer select-none">Technical details</summary>
|
||||
<summary class="cursor-pointer select-none">{m.error_technical_details()}</summary>
|
||||
<p class="mt-1 font-mono break-all">{loadError.detail}</p>
|
||||
</details>
|
||||
{/if}
|
||||
@@ -535,6 +536,15 @@
|
||||
<ChartContainer loading chartCount={enabledChartCount || 1} {chartHeight} />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if selectedDayKey}
|
||||
<NearbyCities
|
||||
latitude={location.latitude}
|
||||
longitude={location.longitude}
|
||||
{selectedDayKey}
|
||||
units={$storedUnits}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,15 +152,15 @@
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-border px-5 py-4">
|
||||
<div>
|
||||
<h2 class="text-base font-bold">Customize meteograms</h2>
|
||||
<h2 class="text-base font-bold">{m.customize_meteograms()}</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Drag variables between charts to build your own layout.
|
||||
{m.customizer_intro()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onclick={onClose}
|
||||
aria-label="Close"
|
||||
aria-label={m.action_close()}
|
||||
>
|
||||
<svg
|
||||
class="h-4.5 w-4.5"
|
||||
@@ -212,12 +212,12 @@
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-[11px] font-bold tracking-wider text-primary uppercase"
|
||||
>Chart {i + 1}</span
|
||||
>{m.customizer_chart_n({ number: i + 1 })}</span
|
||||
>
|
||||
<button
|
||||
class="cursor-pointer rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
onclick={() => deletePanel(panel.id)}
|
||||
aria-label="Delete chart {i + 1}"
|
||||
aria-label={m.customizer_delete_chart({ number: i + 1 })}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -238,7 +238,7 @@
|
||||
data-chip
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Drag {def.label}"
|
||||
aria-label={m.customizer_drag({ variable: def.label })}
|
||||
class="flex cursor-grab touch-none items-center gap-1.5 rounded-lg border border-border bg-background py-1.5 pr-1 pl-2.5 text-sm shadow-sm select-none active:cursor-grabbing {dragKey ===
|
||||
key
|
||||
? 'opacity-30'
|
||||
@@ -257,7 +257,7 @@
|
||||
class="ml-0.5 flex h-5 w-5 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onclick={() => removeVar(key)}
|
||||
aria-label="Remove {def.label}"
|
||||
aria-label={m.customizer_remove({ variable: def.label })}
|
||||
>
|
||||
<svg
|
||||
class="h-3.5 w-3.5"
|
||||
@@ -274,7 +274,7 @@
|
||||
{/each}
|
||||
{#if panel.variables.length === 0}
|
||||
<span class="self-center text-xs text-muted-foreground italic"
|
||||
>Drop variables here</span
|
||||
>{m.customizer_drop_here()}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -285,12 +285,12 @@
|
||||
class="w-full cursor-pointer rounded-xl border-2 border-dashed border-border py-2.5 text-sm font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground"
|
||||
onclick={addPanel}
|
||||
>
|
||||
+ Add chart
|
||||
{m.customizer_add_chart()}
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-2 text-[11px] font-bold tracking-wider text-muted-foreground uppercase">
|
||||
Available variables
|
||||
{m.customizer_available()}
|
||||
</h3>
|
||||
<div
|
||||
data-zone="pool"
|
||||
@@ -304,7 +304,7 @@
|
||||
data-chip
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Drag {def.label}"
|
||||
aria-label={m.customizer_drag({ variable: def.label })}
|
||||
class="flex cursor-grab touch-none items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 py-1.5 text-sm text-muted-foreground shadow-sm select-none active:cursor-grabbing {dragKey ===
|
||||
def.key
|
||||
? 'opacity-30'
|
||||
@@ -321,7 +321,7 @@
|
||||
{/each}
|
||||
{#if availableVars.length === 0}
|
||||
<span class="self-center text-xs text-muted-foreground italic"
|
||||
>All variables are in use</span
|
||||
>{m.customizer_all_in_use()}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -333,13 +333,13 @@
|
||||
class="cursor-pointer text-xs font-medium text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline"
|
||||
onclick={resetLayout}
|
||||
>
|
||||
Reset to defaults
|
||||
{m.action_reset_defaults()}
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer rounded-lg bg-primary px-4 py-1.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90"
|
||||
onclick={onClose}
|
||||
>
|
||||
Done
|
||||
{m.action_done()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import { formatZoned, getRelativeDayLabel, isSameDayInZone } from '$lib/utils/date';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { getTempStyle } from '../../utils/colors';
|
||||
import { getWeatherIconName } from '../../utils/weather-codes';
|
||||
import { precipIsSignificant, sunIsSignificant, windIsSignificant } from './significance';
|
||||
@@ -118,7 +120,7 @@
|
||||
type="button"
|
||||
class="mr-4 flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
onclick={onExtendPast}
|
||||
aria-label="Load recent past days"
|
||||
aria-label={m.strip_past_aria()}
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
@@ -135,7 +137,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14 13l-3 3 3 3" />
|
||||
</svg>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold">
|
||||
Past<br />3 days
|
||||
{m.daycards_past()}
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -317,7 +319,7 @@
|
||||
type="button"
|
||||
class="day-card-btn flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 self-stretch rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground md:hidden"
|
||||
onclick={onExtend}
|
||||
aria-label="Load the longer-range forecast"
|
||||
aria-label={m.strip_extend_aria()}
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
@@ -334,7 +336,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
|
||||
</svg>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold">
|
||||
Load<br />15 days
|
||||
{m.daycards_load_15()}
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -349,7 +351,7 @@
|
||||
type="button"
|
||||
class="mt-2 mb-3 hidden w-20 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 self-stretch rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground md:mt-5 md:mb-11 md:flex md:w-24"
|
||||
onclick={onExtend}
|
||||
aria-label="Load the longer-range forecast"
|
||||
aria-label={m.strip_extend_aria()}
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
@@ -365,7 +367,8 @@
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
|
||||
</svg>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold"> Load<br />15 days </span>
|
||||
<span class="text-center text-[11px] leading-tight font-semibold">{m.daycards_load_15()}</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -451,7 +451,7 @@
|
||||
<button
|
||||
class="inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 text-[13px] font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground"
|
||||
onclick={onCustomize}
|
||||
aria-label="Customize variables"
|
||||
aria-label={m.table_customize()}
|
||||
>
|
||||
<!-- sliders icon -->
|
||||
<svg
|
||||
@@ -466,13 +466,13 @@
|
||||
d="M4 6h9m5 0h2M4 12h2m5 0h9M4 18h9m5 0h2M13 4.5v3M6 10.5v3M18 16.5v3"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Variables</span>
|
||||
<span class="hidden sm:inline">{m.hourly_variables()}</span>
|
||||
</button>
|
||||
{/if}
|
||||
<div
|
||||
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-[13px] font-semibold"
|
||||
role="group"
|
||||
aria-label="Hourly interval"
|
||||
aria-label={m.table_interval_aria()}
|
||||
>
|
||||
{#each [3, 1] as interval (interval)}
|
||||
<button
|
||||
@@ -584,7 +584,7 @@
|
||||
class="absolute bottom-0.5 z-15 -translate-x-1/2 rounded-full bg-red-500 px-1.5 py-px text-[9px] font-bold tracking-wide whitespace-nowrap text-white uppercase shadow-sm"
|
||||
style="left:{nowPercent}%"
|
||||
>
|
||||
Now
|
||||
{m.table_now()}
|
||||
</span>
|
||||
{/if}
|
||||
<!-- Hour labels -->
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="hidden text-xs text-muted-foreground md:inline">
|
||||
drag or
|
||||
{m.meteograms_zoom_hint()}
|
||||
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
|
||||
>Ctrl</kbd
|
||||
>
|
||||
@@ -273,7 +273,7 @@
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={liveCharts.length === 0 || downloadingPng}
|
||||
onclick={downloadPng}
|
||||
title="Download meteogram as PNG image"
|
||||
title={m.chart_download()}
|
||||
>
|
||||
{#if downloadingPng}
|
||||
<svg
|
||||
@@ -330,9 +330,10 @@
|
||||
<div
|
||||
class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
No meteograms configured, <button
|
||||
{m.meteograms_none_before()}
|
||||
<button
|
||||
class="cursor-pointer font-semibold text-primary underline-offset-2 hover:underline"
|
||||
onclick={() => (customizerOpen = true)}>add some variables</button
|
||||
onclick={() => (customizerOpen = true)}>{m.meteograms_none_action()}</button
|
||||
>.
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}}
|
||||
>
|
||||
<Select.Trigger
|
||||
aria-label="{label} selection"
|
||||
aria-label={m.model_selector_aria({ label })}
|
||||
class="group h-auto min-h-12 min-w-0 flex-1 cursor-pointer gap-2.5 rounded-xl border-2 border-primary/35 bg-card py-1.5 ps-2.5 shadow-sm transition-colors hover:border-primary/70 hover:shadow-md data-[size=default]:h-auto data-[state=open]:border-primary sm:min-h-14 sm:gap-3 sm:py-2 sm:min-w-72 sm:flex-none"
|
||||
>
|
||||
<div
|
||||
@@ -99,7 +99,9 @@
|
||||
{mo.resolution}{mo.update ? ` · updated ${mo.update}` : ''}
|
||||
</span>
|
||||
{:else if mo.value === 'best_match'}
|
||||
<span class="text-[11px] text-muted-foreground">Automatic selection</span>
|
||||
<span class="text-[11px] text-muted-foreground"
|
||||
>{m.model_automatic_selection()}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Select.Item>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
import { type NearbyCity, findNearbyCities } from '$lib/services/nearby-cities';
|
||||
import { type NearbyDaily, fetchNearbyDaily } from '$lib/services/weather';
|
||||
|
||||
import { getWeatherIconName } from '../../utils/weather-codes';
|
||||
|
||||
import type { UnitPrefs } from '$lib/stores/settings';
|
||||
|
||||
interface Props {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
/** "yyyy-MM-dd" of the day the rest of the page is showing */
|
||||
selectedDayKey: string;
|
||||
units: UnitPrefs;
|
||||
}
|
||||
|
||||
let { latitude, longitude, selectedDayKey, units }: Props = $props();
|
||||
|
||||
const COUNT = 10;
|
||||
|
||||
let cities = $state<NearbyCity[]>([]);
|
||||
let daily = $state<(NearbyDaily | null)[]>([]);
|
||||
let failed = $state(false);
|
||||
|
||||
// Country names come free and localized from the platform; the flag images
|
||||
// are the same set the header uses.
|
||||
let countryNames = $derived(new Intl.DisplayNames([getLocale()], { type: 'region' }));
|
||||
const countryName = (code: string) => {
|
||||
try {
|
||||
return countryNames.of(code) ?? code;
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
};
|
||||
|
||||
// One request covers the whole strip's date range, so clicking through the
|
||||
// days re-reads what is already here instead of refetching ten cities.
|
||||
$effect(() => {
|
||||
const lat = latitude;
|
||||
const lon = longitude;
|
||||
const unitPrefs = { ...units };
|
||||
let cancelled = false;
|
||||
|
||||
cities = [];
|
||||
daily = [];
|
||||
failed = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const found = await findNearbyCities(lat, lon, COUNT);
|
||||
if (cancelled) return;
|
||||
cities = found;
|
||||
|
||||
const snapshots = await fetchNearbyDaily({ points: found, ...unitPrefs });
|
||||
if (cancelled) return;
|
||||
daily = snapshots;
|
||||
} catch {
|
||||
if (!cancelled) failed = true;
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
const dayFor = (index: number) => daily[index]?.byDate[selectedDayKey];
|
||||
|
||||
const temp = (value: number | undefined) =>
|
||||
value == null || !Number.isFinite(value) ? '–' : `${Math.round(value)}°`;
|
||||
|
||||
const distance = (km: number) =>
|
||||
units.wind_speed_unit === 'mph' ? `${Math.round(km * 0.621371)} mi` : `${Math.round(km)} km`;
|
||||
</script>
|
||||
|
||||
{#if cities.length > 0 && !failed}
|
||||
<section class="mt-8" in:fade={{ duration: 200 }}>
|
||||
<div class="mb-3 flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h2 class="text-lg font-semibold">{m.nearby_cities_title()}</h2>
|
||||
<p class="text-xs text-muted-foreground">{m.nearby_cities_subtitle()}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="-mx-3 grid grid-cols-2 gap-px overflow-hidden border-y border-border/70 bg-border/70 sm:grid-cols-3 md:mx-0 md:rounded-2xl md:border lg:grid-cols-5"
|
||||
>
|
||||
{#each cities as city, i (city.id)}
|
||||
{@const day = dayFor(i)}
|
||||
<a
|
||||
href={href('/weather/week/[location]', {
|
||||
location: buildLocationRoute({
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
latitude: city.latitude,
|
||||
longitude: city.longitude,
|
||||
population: city.population,
|
||||
feature_code: 'PPL'
|
||||
})
|
||||
})}
|
||||
class="group flex items-center gap-2 bg-card px-3 py-2.5 transition-colors hover:bg-muted/60"
|
||||
title="{city.name}, {countryName(city.countryCode)}"
|
||||
>
|
||||
<svg class="shrink-0 fill-foreground/80" width="34px" height="34px">
|
||||
{#if day}
|
||||
<use
|
||||
xlink:href="/images/weather-icons/{getWeatherIconName(
|
||||
day.weatherCode,
|
||||
true
|
||||
)}.svg#Layer_1"
|
||||
></use>
|
||||
{/if}
|
||||
</svg>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium group-hover:underline">{city.name}</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
{distance(city.distanceKm)} · {countryName(city.countryCode)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right text-sm tabular-nums">
|
||||
<div class="font-semibold">{temp(day?.max)}</div>
|
||||
<div class="text-[11px] text-muted-foreground">{temp(day?.min)}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
@@ -12,6 +12,8 @@
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -81,16 +83,16 @@
|
||||
<aside
|
||||
class="absolute inset-y-0 right-0 flex w-80 max-w-[90vw] flex-col overflow-y-auto border-l border-border bg-card shadow-xl"
|
||||
transition:fly={{ x: 320, duration: 200, opacity: 1 }}
|
||||
aria-label="Variable selection"
|
||||
aria-label={m.variables_aria()}
|
||||
>
|
||||
<div
|
||||
class="sticky top-0 flex items-center justify-between border-b border-border bg-card px-5 py-4"
|
||||
>
|
||||
<h2 class="text-base font-bold">Variables</h2>
|
||||
<h2 class="text-base font-bold">{m.hourly_variables()}</h2>
|
||||
<button
|
||||
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onclick={onClose}
|
||||
aria-label="Close variable selection"
|
||||
aria-label={m.variables_close()}
|
||||
>
|
||||
<svg
|
||||
class="h-4.5 w-4.5"
|
||||
@@ -107,7 +109,7 @@
|
||||
<div class="flex flex-1 flex-col gap-6 px-5 py-4">
|
||||
<section>
|
||||
<h3 class="mb-2 text-[11px] font-bold tracking-wider text-primary uppercase">
|
||||
Hourly table
|
||||
{m.variables_table_section()}
|
||||
</h3>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each tableRowOrder as key, i (key)}
|
||||
@@ -129,7 +131,7 @@
|
||||
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-30"
|
||||
onclick={() => moveRow(key, -1)}
|
||||
disabled={i === 0}
|
||||
aria-label="Move {tableVariableLabels[key] ?? key} up"
|
||||
aria-label={m.variables_move_up({ variable: tableVariableLabels[key] ?? key })}
|
||||
>
|
||||
<svg
|
||||
class="h-3.5 w-3.5"
|
||||
@@ -145,7 +147,9 @@
|
||||
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-30"
|
||||
onclick={() => moveRow(key, 1)}
|
||||
disabled={i === tableRowOrder.length - 1}
|
||||
aria-label="Move {tableVariableLabels[key] ?? key} down"
|
||||
aria-label={m.variables_move_down({
|
||||
variable: tableVariableLabels[key] ?? key
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
class="h-3.5 w-3.5"
|
||||
@@ -164,8 +168,9 @@
|
||||
</section>
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Meteogram variables are configured with the <span class="font-semibold">Customize</span>
|
||||
button above the charts.
|
||||
{m.variables_charts_hint_before()}
|
||||
<span class="font-semibold">{m.meteograms_customize()}</span>
|
||||
{m.variables_charts_hint_after()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -174,7 +179,7 @@
|
||||
class="cursor-pointer text-xs font-medium text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline"
|
||||
onclick={resetDefaults}
|
||||
>
|
||||
Reset to defaults
|
||||
{m.action_reset_defaults()}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[[5856195,"Honolulu","US",21.307,-157.858,351]]
|
||||
@@ -0,0 +1 @@
|
||||
[[5856195,"Honolulu","US",21.307,-157.858,351],[7315245,"East Honolulu","US",21.289,-157.717,50],[5852275,"Pearl City","US",21.397,-157.975,48],[13100439,"Makakilo / Kapolei / Honokai Hale","US",21.337,-158.097,46],[10173805,"Joint Base Pearl Harbor Hickam","US",21.349,-157.947,42],[13100433,"Aliamanu / Salt Lakes / Foster Village","US",21.36,-157.918,39],[5847486,"Kailua","US",21.402,-157.741,39],[5854686,"Waipahu","US",21.387,-158.009,38],[5848189,"Kaneohe","US",21.4,-157.799,35],[9409823,"Makiki / Lower Punchbowl / Tantalus","US",21.318,-157.831,31],[5851030,"Mililani Town","US",21.45,-158.015,28],[5847411,"Kahului","US",20.889,-156.473,26],[5851096,"Mō‘ili‘ili","US",21.295,-157.83,25],[9409821,"Manoa","US",21.316,-157.804,23],[5855070,"‘Ewa Gentry","US",21.34,-158.03,23],[5849297,"Kīhei","US",20.765,-156.446,21],[9534958,"Kalihi Valley","US",21.364,-157.843,21]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[3646738,"Caracas","VE",10.488,-66.879,3000],[3663517,"Manaus","BR",-3.102,-60.025,2220],[3492908,"Santo Domingo","DO",18.472,-69.892,2202],[3633009,"Maracaibo","VE",10.642,-71.611,1753],[3625549,"Valencia","VE",10.162,-68,1619],[3405870,"Belém","BR",-1.456,-48.504,1500],[3648522,"Barquisimeto","VE",10.065,-69.357,1241],[3718426,"Port-au-Prince","HT",18.543,-72.339,1235],[3492914,"Santiago de los Caballeros","DO",19.45,-70.691,1200],[3645528,"Ciudad Guayana","VE",8.351,-62.641,978],[3648559,"Barcelona","VE",10.138,-64.688,815],[3685533,"Cúcuta","CO",7.907,-72.505,777],[7874116,"Santo Domingo Oeste","DO",18.5,-70,701],[8601412,"Santo Domingo Este","DO",18.485,-69.848,700],[3778045,"Maturín","VE",9.746,-63.183,647],[3688465,"Bucaramanga","CO",7.125,-73.119,581],[3396016,"Macapá","BR",0.039,-51.066,513],[3666304,"Valledupar","CO",10.465,-73.253,490],[3632998,"Maracay","VE",10.25,-67.595,465],[3407669,"Ananindeua","BR",-1.366,-48.372,434],[3664980,"Boa Vista","BR",2.82,-60.673,420],[4568127,"San Juan","PR",18.466,-66.106,418],[3645532,"Ciudad Bolívar","VE",8.124,-63.547,413],[3644768,"Cumaná","VE",10.464,-64.179,406],[3648546,"Barinas","VE",8.621,-70.231,397],[3726786,"Delmas","HT",18.545,-72.3,383],[3629672,"Puerto La Cruz","VE",10.214,-64.633,370],[3630297,"Petare","VE",10.477,-66.808,365],[3647651,"Cabimas","VE",10.399,-71.452,352],[3625829,"Turmero","VE",10.229,-67.474,345],[3718420,"Port-de-Paix","HT",19.94,-72.83,306],[3632308,"Mérida","VE",8.579,-71.169,300],[3378644,"Georgetown","GY",6.804,-58.155,235],[3646382,"Carúpano","VE",10.665,-63.254,167],[3625710,"Upata","VE",8.016,-62.406,113],[3625929,"Tucupita","VE",9.063,-62.052,100],[3374036,"Bridgetown","BB",13.107,-59.62,99],[3570675,"Fort-de-France","MQ",14.604,-61.074,90],[3574810,"Chaguanas","TT",10.517,-61.417,67],[3629606,"Punta de Mata","VE",9.691,-63.609,61],[3574116,"Mon Repos","TT",10.28,-61.446,56],[3573738,"San Fernando","TT",10.28,-61.468,55],[3578959,"Les Abymes","GP",16.273,-61.505,54],[3573890,"Port of Spain","TT",10.667,-61.519,49],[3646534,"Caripito","VE",10.111,-63.1,43],[3646581,"Cariaco","VE",10.497,-63.553,42],[3570428,"Le Lamentin","MQ",14.613,-61,41],[3639725,"Güiria","VE",10.577,-62.298,40],[3625892,"Tumeremo","VE",7.299,-61.505,38],[3626352,"Temblador","VE",9.005,-62.645,36],[3573840,"Rio Claro","TT",10.306,-61.176,36],[3575051,"Arima","TT",10.637,-61.282,35],[3579767,"Baie-Mahault","GP",16.267,-61.585,31],[3578978,"Le Gosier","GP",16.206,-61.493,29],[3639922,"Guasipati","VE",7.477,-61.897,28],[3574194,"Marabella","TT",10.306,-61.447,27],[3574636,"Diego Martin","TT",10.721,-61.566,25],[3576686,"Gros Islet","LC",14.067,-60.95,25],[3578682,"Petit-Bourg","GP",16.192,-61.591,25],[3577887,"Kingstown","VC",13.155,-61.227,25],[3576022,"Saint John’s","AG",17.121,-61.843,24],[3748746,"Calliaqua","VC",13.129,-61.192,24],[3570412,"Le Robert","MQ",14.678,-60.942,24],[3647535,"Caicara","VE",9.818,-63.613,24],[3578466,"Sainte-Anne","GP",16.226,-61.381,23],[3578967,"Le Moule","GP",16.332,-61.347,23],[3574309,"Laventille","TT",10.649,-61.499,21],[3646316,"Casanay","VE",10.504,-63.417,21],[3569926,"Sainte-Marie","MQ",14.784,-60.992,20],[3578447,"Sainte-Rose","GP",16.333,-61.697,20],[3576812,"Castries","LC",13.996,-61.006,20]]
|
||||
@@ -0,0 +1 @@
|
||||
[[3405870,"Belém","BR",-1.456,-48.504,1500],[3645528,"Ciudad Guayana","VE",8.351,-62.641,978],[3388368,"São Luís","BR",-2.53,-44.303,917],[3778045,"Maturín","VE",9.746,-63.183,647],[3396016,"Macapá","BR",0.039,-51.066,513],[3407669,"Ananindeua","BR",-1.366,-48.372,434],[3664980,"Boa Vista","BR",2.82,-60.673,420],[3645532,"Ciudad Bolívar","VE",8.124,-63.547,413]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2253354,"Dakar","SN",14.694,-17.444,2647],[2377450,"Nouakchott","MR",18.086,-15.979,1185],[3388368,"São Luís","BR",-2.53,-44.303,917],[2246678,"Pikine","SN",14.765,-17.391,874],[2411989,"Serekunda","GM",13.438,-16.678,340],[2595778,"Guédiawaye","SN",14.774,-17.402,330],[2244802,"Thiès","SN",14.789,-16.926,318]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2460596,"Bamako","ML",12.609,-7.975,4228],[2253354,"Dakar","SN",14.694,-17.444,2647],[2422465,"Conakry","GN",9.538,-13.677,1928],[2422488,"Camayenne","GN",9.535,-13.688,1871],[2274895,"Monrovia","LR",6.301,-10.797,1543],[2377450,"Nouakchott","MR",18.086,-15.979,1185],[2244322,"Touba","SN",14.85,-15.883,1121],[2246678,"Pikine","SN",14.765,-17.391,874],[2409306,"Freetown","SL",8.487,-13.236,803],[2374775,"Bissau","GW",11.864,-15.598,440],[2515270,"Las Palmas de Gran Canaria","ES",28.102,-15.416,384],[2411989,"Serekunda","GM",13.438,-16.678,340],[2595778,"Guédiawaye","SN",14.774,-17.402,330],[2244802,"Thiès","SN",14.789,-16.926,318],[2246512,"Rufisque","SN",14.715,-17.273,295],[2248477,"Mbour","SN",14.422,-16.964,284],[2246452,"Saint-Louis","SN",16.018,-16.49,254],[2244799,"Thiès Nones","SN",14.783,-16.967,252],[2377457,"Nouadhibou","MR",20.942,-17.038,146],[3374333,"Praia","CV",14.932,-23.513,138],[2244386,"Tivaouane","SN",14.952,-16.815,103],[2413753,"Brikama","GM",13.271,-16.649,97],[2252132,"Dougnane","SN",14.959,-16.87,70],[2254154,"Bargny","SN",14.697,-17.23,69],[3374462,"Mindelo","CV",16.89,-24.98,69],[2411880,"Sukuta","GM",13.41,-16.708,56],[12640502,"Talinding","GM",13.426,-16.673,49],[2247381,"Nguékhokh","SN",14.513,-17.004,48],[2595640,"Diamniadio","SN",14.721,-17.182,48],[2251084,"Joal","SN",14.173,-16.849,46],[2413525,"Faji Kunda","GM",13.418,-16.667,46],[2413920,"Bakau","GM",13.478,-16.682,43],[2245709,"Sébikhotane","SN",14.745,-17.138,43],[2595222,"Sali","SN",14.441,-17.019,42],[2247298,"Niakoul Rab","SN",14.784,-17.274,42],[2248515,"Mboro","SN",15.141,-16.887,41],[2251911,"Fatick","SN",14.332,-16.405,39],[7670740,"Bambey","SN",14.701,-16.459,37],[2413876,"Banjul","GM",13.453,-16.578,37],[11101616,"N’diareme limamoulaye","SN",14.781,-17.384,35],[2413696,"Busumbala","GM",13.339,-16.688,29],[2412749,"Lamin","GM",13.352,-16.434,29],[2411700,"Welingara","GM",13.404,-16.674,28],[2413745,"Brufut","GM",13.384,-16.751,28],[2248409,"Mékhé","SN",15.11,-16.622,28],[2412186,"Sanchaba","GM",13.422,-16.71,25],[8521418,"Espargos","CV",16.755,-22.945,25],[2246608,"Poukham Un","SN",14.358,-16.412,24],[2246606,"Pout","SN",14.771,-17.061,24],[2250677,"Kayar","SN",14.919,-17.12,24],[2413419,"Gunjur","GM",13.202,-16.734,23],[2412609,"Manjai Kunda","GM",13.445,-16.703,22],[3374888,"Assomada","CV",15.1,-23.683,21],[2413565,"Dippa Kunda","GM",13.44,-16.687,21],[12640501,"New Jeshwang","GM",13.447,-16.665,21],[2249901,"Khombole","SN",14.764,-16.692,20],[2413915,"Bakoti","GM",13.43,-16.698,20],[2244786,"Thiadiaye","SN",14.42,-16.703,20]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[5856195,"Honolulu","US",21.307,-157.858,351],[7315245,"East Honolulu","US",21.289,-157.717,50],[5852275,"Pearl City","US",21.397,-157.975,48],[13100439,"Makakilo / Kapolei / Honokai Hale","US",21.337,-158.097,46],[5855927,"Hilo","US",19.73,-155.091,43],[10173805,"Joint Base Pearl Harbor Hickam","US",21.349,-157.947,42],[13100433,"Aliamanu / Salt Lakes / Foster Village","US",21.36,-157.918,39],[5847486,"Kailua","US",21.402,-157.741,39],[5854686,"Waipahu","US",21.387,-158.009,38],[5848189,"Kaneohe","US",21.4,-157.799,35],[9409823,"Makiki / Lower Punchbowl / Tantalus","US",21.318,-157.831,31],[5851030,"Mililani Town","US",21.45,-158.015,28],[5847411,"Kahului","US",20.889,-156.473,26],[5851096,"Mō‘ili‘ili","US",21.295,-157.83,25],[9409821,"Manoa","US",21.316,-157.804,23],[5855070,"‘Ewa Gentry","US",21.34,-158.03,23],[5849297,"Kīhei","US",20.765,-156.446,21],[9534958,"Kalihi Valley","US",21.364,-157.843,21]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[1275339,"Mumbai","IN",19.073,72.883,12692],[1174872,"Karachi","PK",24.861,67.01,11624],[1279233,"Ahmedabad","IN",23.026,72.587,6358],[105343,"Jeddah","SA",21.49,39.186,4697],[1255364,"Surat","IN",21.196,72.83,4591],[108410,"Riyadh","SA",24.688,46.722,4206],[344979,"Addis Ababa","ET",9.025,38.747,3860],[292223,"Dubai","AE",25.077,55.309,3790],[1259229,"Pune","IN",18.52,73.855,3124],[53654,"Mogadishu","SO",2.037,45.344,2587],[71137,"Sanaa","YE",15.355,44.206,1937],[1176734,"Hyderabad","PK",25.397,68.377,1921],[1254661,"Thāne","IN",19.197,72.964,1841],[1253573,"Vadodara","IN",22.299,73.208,1822],[292968,"Abu Dhabi","AE",24.451,54.397,1807],[292672,"Sharjah","AE",25.334,55.412,1800],[7626690,"Pimpri-Chinchwad","IN",18.619,73.804,1728],[104515,"Makkah","SA",21.427,39.826,1579],[1167528,"Quetta","PK",30.184,67.001,1566],[418863,"Isfahan","IR",32.652,51.675,1547],[1261731,"Nashik","IN",19.997,73.791,1486],[1258847,"Rājkot","IN",22.292,70.793,1391],[99532,"Basrah","IQ",30.509,47.78,1327],[109223,"Madinah","SA",24.469,39.614,1300],[1259652,"Pimpri","IN",18.623,73.807,1285],[1268295,"Kalyān","IN",19.244,73.136,1262],[110336,"Dammam","SA",26.434,50.103,1253],[115019,"Shiraz","IR",29.61,52.531,1250],[1272423,"Dombivali","IN",19.217,73.083,1247],[1253133,"Virār","IN",19.456,72.811,1222],[415189,"Aden","YE",12.78,45.039,1080],[101760,"Sulţānah","SA",24.493,39.586,947],[70225,"Taiz","YE",13.58,44.021,941],[1275901,"Bhiwandi","IN",19.3,73.059,874],[292913,"Al Ain City","AE",24.192,55.761,847],[144448,"Ahvaz","IR",31.319,48.684,841],[1276014,"Bhayandar","IN",19.302,72.851,809],[287286,"Muscat","OM",23.584,58.408,797],[74477,"Ibb","YE",13.967,44.183,772],[107304,"Buraydah","SA",26.326,43.975,745],[79415,"Al Ḩudaydah","YE",14.798,42.955,735],[107968,"Ta’if","SA",21.27,40.416,689],[285839,"Al Aḩmadī","KW",29.077,48.084,637],[223817,"Djibouti","DJ",11.589,43.145,627],[1275248,"Borivli","IN",19.235,72.86,610],[106281,"Ha'il","SA",27.522,41.691,606],[1276032,"Bhavnagar","IN",21.763,72.153,606],[1269317,"Jamnagar","IN",22.473,70.067,601],[64021,"Borama","SO",9.936,43.183,598],[78754,"Mukalla","YE",14.542,49.124,595],[128234,"Kerman","IR",30.283,57.079,578],[343300,"Asmara","ER",15.338,38.932,564],[1164408,"Sukkur","PK",27.703,68.859,564],[98854,"Nasiriyah","IQ",31.058,46.257,558],[1159301,"Zahedan","IR",29.496,60.863,552],[1138336,"Kandahār","AF",31.613,65.71,523],[1167460,"Rahim Yar Khan","PK",28.42,70.303,517],[1253894,"Ulhasnagar","IN",19.217,73.15,517],[1275610,"Bilimora","IN",20.77,72.961,511],[103630,"Najrān","SA",17.493,44.128,506],[292932,"Ajman City","AE",25.402,55.479,490],[377039,"Port Sudan","SD",19.617,37.216,490],[333795,"Jijiga","ET",9.35,42.8,483],[98860,"Najaf","IQ",32.026,44.346,483],[111822,"Yazd","IR",31.897,54.367,478],[57289,"Hargeysa","SO",9.56,44.065,478],[288967,"Seeb","OM",23.67,58.189,471],[336014,"Gonder","ET",12.6,37.467,466],[331180,"Mek'ele","ET",13.497,39.475,458],[330186,"Nazrēt","ET",8.55,39.267,457],[109353,"Al Kharj","SA",24.155,47.335,425],[343137,"Awasa","ET",7.062,38.476,422],[1163965,"Tando Allahyar","PK",25.46,68.717,422],[372753,"Kassala","SD",15.451,36.4,401],[105072,"Khamis Mushait","SA",18.3,42.733,388],[288764,"Bawshar","OM",23.578,58.4,383],[145459,"Abadan","IR",30.339,48.304,370],[1172128,"Larkana","PK",27.559,68.212,364],[1169116,"Nawabshah","PK",26.239,68.404,363],[141681,"Bandar Abbas","IR",27.186,56.281,352],[291074,"Ras Al Khaimah City","AE",25.79,55.943,352],[342884,"Bahir Dar","ET",11.594,37.391,350],[290030,"Doha","QA",25.285,51.531,345],[338832,"Dire Dawa","ET",9.593,41.866,343],[127319,"Khorramshahr","IR",30.441,48.184,331],[99608,"Al ‘Amārah","IQ",31.836,47.145,323],[1268773,"Jūnāgadh","IN",21.52,70.46,319],[99762,"Ad Dīwānīyah","IQ",31.993,44.926,319],[99131,"Al Kūt","IQ",32.513,45.818,315],[287830,"‘Ibrī","OM",23.226,56.516,163],[286621,"Şalālah","OM",17.015,54.092,163],[289011,"Rustaq","OM",23.391,57.424,120],[64013,"Bosaso","SO",11.284,49.182,74],[286987,"Nizwá","OM",22.933,57.533,72],[286245,"Sur","OM",22.567,59.529,71],[400769,"Laascaanood","SO",8.477,47.36,60],[58933,"Garoowe","SO",8.402,48.483,57],[288899,"Bahlā’","OM",22.979,57.305,54],[286293,"Sufālat Samā’il","OM",23.317,58.017,48],[77726,"Ataq","YE",14.538,46.832,37],[287814,"Izkī","OM",22.933,57.767,36],[62691,"Ceerigaabo","SO",10.616,47.368,34],[287832,"Ibrā’","OM",22.691,58.533,25],[288721,"Bidbid","OM",23.408,58.128,21]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[5856195,"Honolulu","US",21.307,-157.858,351]]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[1668341,"Taipei","TW",25.053,121.526,7872],[1790645,"Xiamen","CN",24.48,118.082,4617],[12908892,"New Taipei City","TW",25.062,121.457,4004],[1795940,"Shantou","CN",23.355,116.679,3839],[1810821,"Fuzhou","CN",26.061,119.306,3740],[1799397,"Ningbo","CN",29.878,121.549,3731],[1798425,"Puyang","CN",29.457,119.889,3590],[1692192,"Quezon City","PH",14.649,121.051,3084],[1668399,"Taichung","TW",24.147,120.684,2850],[1673820,"Kaohsiung","TW",22.616,120.313,2738],[1791388,"Wenzhou","CN",27.999,120.667,2650],[1795855,"Shaoxing","CN",30.002,120.579,2300],[1797121,"Jieyang","CN",23.542,116.366,1899],[1668355,"Tainan","TW",22.991,120.213,1857],[1715348,"Davao","PH",7.073,125.613,1849],[1815395,"Chaozhou","CN",23.654,116.623,1751],[1720151,"Caloocan City","PH",14.65,120.968,1713],[1701668,"Manila","PH",14.604,120.982,1600],[1798449,"Putian","CN",25.439,119.01,1539],[8400694,"Taizhou","CN",28.663,121.433,1486],[1814870,"Yiwu","CN",29.315,120.077,1481],[1797353,"Quanzhou","CN",24.914,118.586,1469],[1805528,"Jinhua","CN",29.107,119.644,1464],[1806602,"Cixi","CN",30.176,121.246,1458],[1797658,"Jinjiang","CN",24.82,118.574,1416],[1684308,"Taguig","PH",14.524,121.079,1308],[1723510,"Budta","PH",7.204,124.44,1274],[1978681,"Malingao","PH",7.161,124.475,1122],[1802276,"Longyan","CN",25.075,117.018,1025],[1679432,"Zamboanga","PH",6.91,122.074,1019],[1717512,"Cebu City","PH",10.317,123.891,965],[1797063,"Rui’an","CN",27.776,120.659,927],[1730501,"Antipolo","PH",14.626,121.123,914],[1886762,"Zhoushan","CN",29.989,122.205,883],[1802940,"Puning","CN",23.311,116.169,875],[7290466,"Pasig City","PH",14.587,121.061,853],[1817720,"Shangyu","CN",30.016,120.871,770],[1721080,"Cagayan de Oro","PH",8.482,124.647,742],[1858421,"Kumamoto","JP",32.806,130.692,739],[1680102,"Valenzuela","PH",14.7,120.967,725],[1713022,"General Santos","PH",6.113,125.172,722],[1694781,"Paranaque City","PH",14.482,121.017,703],[1707174,"Las Piñas","PH",14.451,120.983,616],[1796663,"Sanming","CN",26.249,117.619,602],[1860827,"Kagoshima","JP",31.567,130.55,595],[1785018,"Zhangzhou","CN",24.513,117.656,590],[1720681,"Calamba","PH",14.212,121.165,575],[1670029,"Banqiao","TW",25.014,121.467,551],[11209896,"Kotō","JP",32.779,130.745,544],[1809412,"Guli","CN",28.882,120.033,536],[1703417,"Makati City","PH",14.55,121.033,510],[1707267,"Lapu-Lapu City","PH",10.31,123.949,498],[1730737,"Angeles City","PH",15.15,120.583,483],[1710914,"Imus","PH",14.43,120.937,482],[1854487,"Ōita","JP",33.233,131.6,478],[1667905,"Taoyuan","TW",24.99,121.319,476],[1711005,"Iloilo","PH",10.697,122.564,474],[1700925,"Marikina City","PH",14.648,121.113,471],[1799846,"Nanping","CN",26.645,118.174,468],[1701966,"Mandaluyong City","PH",14.583,121.041,466],[1729564,"Bacolod City","PH",10.667,122.95,455],[1701500,"Mansilingan","PH",10.631,122.979,454],[1675151,"Hsinchu","TW",24.804,120.969,454],[1636544,"Manado","ID",1.482,124.849,452],[1803245,"Lishui","CN",28.46,119.91,451],[1715430,"Dasmariñas","PH",14.329,120.937,442],[1734052,"Sandakan","MY",5.84,118.118,439],[1806097,"Ningde","CN",26.662,119.523,429],[13118244,"Mandaluyong","PH",14.578,121.035,425],[1694611,"Pasay","PH",14.537,121.001,417],[1856177,"Nagasaki","JP",32.75,129.883,409],[2082600,"Jayapura","ID",-2.534,140.718,404],[6696918,"Taoyuan City","TW",24.994,121.297,402],[1682812,"Tarlac City","PH",15.48,120.598,402],[1856717,"Miyazaki","JP",31.917,131.417,401],[1716771,"Cotabato","PH",7.224,124.246,383],[1734199,"Tawau","MY",4.245,117.891,373],[1703320,"Malabon","PH",14.673,120.94,366],[1678228,"Keelung","TW",25.131,121.741,362],[1689395,"San Jose del Monte","PH",14.814,121.045,358],[1729524,"Bacoor","PH",14.459,120.939,357],[1673816,"Fengshan","TW",22.627,120.361,356],[1688749,"San Pedro","PH",14.36,121.047,349],[1721906,"Cabanatuan City","PH",15.486,120.966,344],[1711084,"Iligan","PH",8.229,124.243,343],[1701947,"Mandaue City","PH",10.324,123.922,331],[1856035,"Naha","JP",26.213,127.679,318],[1711082,"Iligan City","PH",8.25,124.4,312],[1722186,"Butuan","PH",8.949,125.544,310],[1721281,"Cabuyao","PH",14.273,121.126,309],[1858088,"Kurume","JP",33.317,130.517,304],[1688830,"San Pablo","PH",14.068,121.326,300],[1725115,"Biñan","PH",14.343,121.081,300]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2082600,"Jayapura","ID",-2.534,140.718,404],[1856035,"Naha","JP",26.213,127.679,318],[7828758,"Saipan","MP",15.212,145.755,48],[4043909,"Dededo Village","GU",13.518,144.839,45],[4038794,"Yigo Village","GU",13.536,144.889,21]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2113779,"Majuro","MH",7.09,171.38,25]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2113779,"Majuro","MH",7.09,171.38,25]]
|
||||
@@ -0,0 +1 @@
|
||||
[[3981609,"Tijuana","MX",32.503,-117.004,1923],[5391811,"San Diego","US",32.716,-117.165,1404],[3996069,"Mexicali","MX",32.628,-115.454,1033],[4004898,"Hermosillo","MX",29.089,-110.967,812],[4012176,"Culiacán","MX",24.802,-107.394,808],[4006702,"Ensenada","MX",31.871,-116.601,444],[3996322,"Mazatlán","MX",23.222,-106.419,382],[4013704,"Ciudad Obregón","MX",27.486,-109.941,329]]
|
||||
@@ -0,0 +1 @@
|
||||
[[3530597,"Mexico City","MX",19.428,-99.128,12294],[3981609,"Tijuana","MX",32.503,-117.004,1923],[3526683,"Iztapalapa","MX",19.355,-99.062,1835],[3521081,"Puebla","MX",19.048,-98.207,1692],[5308655,"Phoenix","US",33.448,-112.074,1650],[3529612,"Ecatepec de Morelos","MX",19.605,-99.061,1645],[3991164,"Santiago de Querétaro","MX",20.588,-100.388,1594],[3998655,"León de los Aldama","MX",21.129,-101.674,1580],[4013708,"Ciudad Juárez","MX",31.72,-106.461,1512],[3979770,"Zapopan","MX",20.721,-103.387,1476],[5391811,"San Diego","US",32.716,-117.165,1404],[4005539,"Guadalajara","MX",20.677,-103.347,1386],[3514674,"Gustavo Adolfo Madero","MX",19.494,-99.111,1186],[3995465,"Monterrey","MX",25.684,-100.317,1136],[3530589,"Ciudad Nezahualcoyotl","MX",19.401,-99.015,1077],[3996069,"Mexicali","MX",32.628,-115.454,1033],[4014338,"Chihuahua","MX",28.635,-106.089,926],[3522790,"Naucalpan de Juárez","MX",19.479,-99.24,834],[4004898,"Hermosillo","MX",29.089,-110.967,812],[4012176,"Culiacán","MX",24.802,-107.394,808],[3995402,"Morelia","MX",19.701,-101.184,743],[3981254,"Torreón","MX",25.544,-103.419,735],[3985606,"San Luis Potosí","MX",22.152,-100.971,723],[4019233,"Aguascalientes","MX",21.883,-102.284,722],[3988086,"Saltillo","MX",25.426,-100.98,710],[5520993,"El Paso","US",31.759,-106.487,679],[4005492,"Guadalupe","MX",25.677,-100.256,674],[3533462,"Acapulco de Juárez","MX",16.849,-99.909,659],[3515431,"Tlalnepantla","MX",19.54,-99.195,653],[3981461,"Tlaquepaque","MX",20.641,-103.293,650],[3530139,"Coyoacán","MX",19.347,-99.162,614],[3520339,"Reynosa","MX",26.08,-98.285,589],[3515428,"Tlalpan","MX",19.295,-99.162,575],[3515807,"Cuautitlán Izcalli","MX",19.644,-99.216,555],[5318313,"Tucson","US",32.222,-110.926,543],[3827409,"Cuauhtémoc","MX",19.445,-99.146,532],[4011743,"Victoria de Durango","MX",24.02,-104.658,519],[3523466,"Heroica Matamoros","MX",25.88,-97.504,511],[3515302,"Toluca","MX",19.288,-99.653,489],[3532624,"Ciudad López Mateos","MX",19.558,-99.257,489],[5304391,"Mesa","US",33.422,-111.823,472],[4018390,"Ciudad Apodaca","MX",25.782,-100.188,467],[4005867,"Ciudad General Escobedo","MX",25.797,-100.318,455],[4006702,"Ensenada","MX",31.871,-116.601,444],[3514450,"Xochimilco","MX",19.255,-99.104,442],[3514783,"Veracruz","MX",19.181,-96.143,428],[3526617,"Xalapa de Enríquez","MX",19.531,-96.916,425],[3522551,"Nuevo Laredo","MX",27.476,-99.516,416],[3532497,"Azcapotzalco","MX",19.487,-99.186,415],[3985241,"San Nicolás de los Garza","MX",25.742,-100.302,412],[3981369,"Tonalá","MX",20.623,-103.242,409],[3827598,"Ojo de Agua","MX",19.68,-99.01,386],[3514519,"Xico","MX",19.27,-98.951,384],[3526700,"Iztacalco","MX",19.395,-99.098,384],[3996322,"Mazatlán","MX",23.222,-106.419,382],[4004330,"Irapuato","MX",20.677,-101.356,381],[3827408,"Miguel Hidalgo","MX",19.434,-99.2,373],[3827406,"Benito Juarez","MX",19.398,-99.158,355],[4014875,"Celaya","MX",20.524,-100.816,340],[3529947,"Cuernavaca","MX",18.926,-99.231,339],[3981941,"Tepic","MX",21.507,-104.893,333],[3530580,"Ciudad Victoria","MX",23.741,-99.144,332],[3982912,"Soledad de Graciano Sánchez","MX",22.189,-100.938,332],[4013704,"Ciudad Obregón","MX",27.486,-109.941,329],[3526682,"Ixtapaluca","MX",19.316,-98.883,322],[3516355,"Tampico","MX",22.285,-97.878,309],[4003757,"Ciudad Benito Juárez","MX",25.647,-100.096,308],[3515463,"Tláhuac","MX",19.287,-99.005,305],[3984583,"Santa Catarina","MX",25.673,-100.458,304],[3985710,"Cabo San Lucas","MX",22.891,-109.912,203],[3986172,"San José del Cabo","MX",23.054,-109.704,136],[11121204,"Colonia del Sol","MX",22.913,-109.927,48]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[5856195,"Honolulu","US",21.307,-157.858,351]]
|
||||
@@ -0,0 +1 @@
|
||||
[[5856195,"Honolulu","US",21.307,-157.858,351],[7315245,"East Honolulu","US",21.289,-157.717,50],[5852275,"Pearl City","US",21.397,-157.975,48],[13100439,"Makakilo / Kapolei / Honokai Hale","US",21.337,-158.097,46],[10173805,"Joint Base Pearl Harbor Hickam","US",21.349,-157.947,42],[13100433,"Aliamanu / Salt Lakes / Foster Village","US",21.36,-157.918,39],[5847486,"Kailua","US",21.402,-157.741,39],[5854686,"Waipahu","US",21.387,-158.009,38],[5848189,"Kaneohe","US",21.4,-157.799,35],[9409823,"Makiki / Lower Punchbowl / Tantalus","US",21.318,-157.831,31],[5851030,"Mililani Town","US",21.45,-158.015,28],[5847411,"Kahului","US",20.889,-156.473,26],[5851096,"Mō‘ili‘ili","US",21.295,-157.83,25],[9409821,"Manoa","US",21.316,-157.804,23],[5855070,"‘Ewa Gentry","US",21.34,-158.03,23],[5849297,"Kīhei","US",20.765,-156.446,21],[9534958,"Kalihi Valley","US",21.364,-157.843,21]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
[[3646738,"Caracas","VE",10.488,-66.879,3000],[3492908,"Santo Domingo","DO",18.472,-69.892,2202],[3625549,"Valencia","VE",10.162,-68,1619],[3648522,"Barquisimeto","VE",10.065,-69.357,1241],[3718426,"Port-au-Prince","HT",18.543,-72.339,1235],[3492914,"Santiago de los Caballeros","DO",19.45,-70.691,1200],[3645528,"Ciudad Guayana","VE",8.351,-62.641,978],[3648559,"Barcelona","VE",10.138,-64.688,815],[7874116,"Santo Domingo Oeste","DO",18.5,-70,701],[8601412,"Santo Domingo Este","DO",18.485,-69.848,700],[3778045,"Maturín","VE",9.746,-63.183,647],[3632998,"Maracay","VE",10.25,-67.595,465],[4568127,"San Juan","PR",18.466,-66.106,418],[3645532,"Ciudad Bolívar","VE",8.124,-63.547,413],[3644768,"Cumaná","VE",10.464,-64.179,406],[3726786,"Delmas","HT",18.545,-72.3,383],[3629672,"Puerto La Cruz","VE",10.214,-64.633,370],[3630297,"Petare","VE",10.477,-66.808,365],[3625829,"Turmero","VE",10.229,-67.474,345],[3718420,"Port-de-Paix","HT",19.94,-72.83,306],[3576022,"Saint John’s","AG",17.121,-61.843,24]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2253354,"Dakar","SN",14.694,-17.444,2647],[2377450,"Nouakchott","MR",18.086,-15.979,1185],[2246678,"Pikine","SN",14.765,-17.391,874],[2515270,"Las Palmas de Gran Canaria","ES",28.102,-15.416,384],[2595778,"Guédiawaye","SN",14.774,-17.402,330],[2244802,"Thiès","SN",14.789,-16.926,318]]
|
||||
@@ -0,0 +1 @@
|
||||
[[2553604,"Casablanca","MA",33.588,-7.611,3666],[2253354,"Dakar","SN",14.694,-17.444,2647],[2422465,"Conakry","GN",9.538,-13.677,1928],[2422488,"Camayenne","GN",9.535,-13.688,1871],[2538475,"Rabat","MA",34.013,-6.833,1656],[2377450,"Nouakchott","MR",18.086,-15.979,1185],[2244322,"Touba","SN",14.85,-15.883,1121],[2542997,"Marrakesh","MA",31.634,-8,996],[2537763,"Salé","MA",34.053,-6.798,972],[2246678,"Pikine","SN",14.765,-17.391,874],[2409306,"Freetown","SL",8.487,-13.236,803],[2561668,"Agadir","MA",30.42,-9.598,698],[2542715,"Meknes","MA",33.894,-5.547,568],[2267057,"Lisbon","PT",38.725,-9.15,518],[2544571,"Kenitra","MA",34.261,-6.58,471],[2374775,"Bissau","GW",11.864,-15.598,440],[2515270,"Las Palmas de Gran Canaria","ES",28.102,-15.416,384],[2529013,"Temara","MA",33.929,-6.907,342],[2411989,"Serekunda","GM",13.438,-16.678,340],[2537881,"Safi","MA",32.299,-9.237,337],[2595778,"Guédiawaye","SN",14.774,-17.402,330],[2244802,"Thiès","SN",14.789,-16.926,318],[2511174,"Santa Cruz de Tenerife","ES",28.468,-16.255,211],[2511401,"La Laguna","ES",28.485,-16.32,151],[2377457,"Nouadhibou","MR",20.942,-17.038,146],[2521582,"Arona","ES",28.1,-16.681,79],[3374462,"Mindelo","CV",16.89,-24.98,69],[2522437,"Adeje","ES",28.123,-16.726,43],[2515692,"La Orotava","ES",28.391,-16.523,42],[2517111,"Granadilla de Abona","ES",28.119,-16.576,40],[6692471,"Los Realejos","ES",28.385,-16.583,36],[2511994,"Realejo Alto","ES",28.376,-16.586,36],[2512196,"Puerto de la Cruz","ES",28.417,-16.551,32],[8521418,"Espargos","CV",16.755,-22.945,25],[2520283,"Candelaria","ES",28.355,-16.373,24],[2516474,"Icod de los Vinos","ES",28.372,-16.712,24],[2510725,"Tacoronte","ES",28.477,-16.41,24],[2516860,"Guía de Isora","ES",28.212,-16.779,21],[2514651,"Los Llanos de Aridane","ES",28.659,-17.918,20]]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user