diff --git a/messages/de.json b/messages/de.json index 84e1fb1..8ae4807 100644 --- a/messages/de.json +++ b/messages/de.json @@ -335,5 +335,33 @@ "cadence_varies": "variiert", "model_group_automatic": "Automatisch", "model_group_reanalysis": "ECMWF-Reanalyse", - "model_group_regional_reanalysis": "Regionale Reanalyse" + "model_group_regional_reanalysis": "Regionale Reanalyse", + "wmo_0": "Klarer Himmel", + "wmo_1": "Überwiegend klar", + "wmo_2": "Teils bewölkt", + "wmo_3": "Bedeckt", + "wmo_45": "Nebel", + "wmo_48": "Gefrierender Nebel", + "wmo_51": "Leichter Nieselregen", + "wmo_53": "Mäßiger Nieselregen", + "wmo_55": "Starker Nieselregen", + "wmo_56": "Leichter gefrierender Nieselregen", + "wmo_57": "Starker gefrierender Nieselregen", + "wmo_61": "Leichter Regen", + "wmo_63": "Mäßiger Regen", + "wmo_65": "Starker Regen", + "wmo_66": "Leichter gefrierender Regen", + "wmo_67": "Starker gefrierender Regen", + "wmo_71": "Leichter Schneefall", + "wmo_73": "Mäßiger Schneefall", + "wmo_75": "Starker Schneefall", + "wmo_77": "Schneegriesel", + "wmo_80": "Leichte Regenschauer", + "wmo_81": "Mäßige Regenschauer", + "wmo_82": "Heftige Regenschauer", + "wmo_85": "Leichte Schneeschauer", + "wmo_86": "Starke Schneeschauer", + "wmo_95": "Gewitter", + "wmo_96": "Gewitter mit leichtem Hagel", + "wmo_99": "Gewitter mit starkem Hagel" } diff --git a/messages/en.json b/messages/en.json index a8b8526..536c18c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -335,5 +335,33 @@ "cadence_varies": "varies", "model_group_automatic": "Automatic", "model_group_reanalysis": "ECMWF reanalysis", - "model_group_regional_reanalysis": "Regional reanalysis" + "model_group_regional_reanalysis": "Regional reanalysis", + "wmo_0": "Clear sky", + "wmo_1": "Mainly clear", + "wmo_2": "Partly cloudy", + "wmo_3": "Overcast", + "wmo_45": "Fog", + "wmo_48": "Depositing rime fog", + "wmo_51": "Light drizzle", + "wmo_53": "Moderate drizzle", + "wmo_55": "Dense drizzle", + "wmo_56": "Light freezing drizzle", + "wmo_57": "Dense freezing drizzle", + "wmo_61": "Slight rain", + "wmo_63": "Moderate rain", + "wmo_65": "Heavy rain", + "wmo_66": "Light freezing rain", + "wmo_67": "Heavy freezing rain", + "wmo_71": "Slight snowfall", + "wmo_73": "Moderate snowfall", + "wmo_75": "Heavy snowfall", + "wmo_77": "Snow grains", + "wmo_80": "Slight rain showers", + "wmo_81": "Moderate rain showers", + "wmo_82": "Violent rain showers", + "wmo_85": "Slight snow showers", + "wmo_86": "Heavy snow showers", + "wmo_95": "Thunderstorm", + "wmo_96": "Thunderstorm with slight hail", + "wmo_99": "Thunderstorm with heavy hail" } diff --git a/messages/es.json b/messages/es.json index 7b4924c..fc4aff5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -335,5 +335,33 @@ "cadence_varies": "variable", "model_group_automatic": "Automático", "model_group_reanalysis": "Reanálisis del ECMWF", - "model_group_regional_reanalysis": "Reanálisis regional" + "model_group_regional_reanalysis": "Reanálisis regional", + "wmo_0": "Cielo despejado", + "wmo_1": "Mayormente despejado", + "wmo_2": "Parcialmente nublado", + "wmo_3": "Cubierto", + "wmo_45": "Niebla", + "wmo_48": "Niebla engelante", + "wmo_51": "Llovizna débil", + "wmo_53": "Llovizna moderada", + "wmo_55": "Llovizna intensa", + "wmo_56": "Llovizna engelante débil", + "wmo_57": "Llovizna engelante intensa", + "wmo_61": "Lluvia débil", + "wmo_63": "Lluvia moderada", + "wmo_65": "Lluvia intensa", + "wmo_66": "Lluvia engelante débil", + "wmo_67": "Lluvia engelante intensa", + "wmo_71": "Nevada débil", + "wmo_73": "Nevada moderada", + "wmo_75": "Nevada intensa", + "wmo_77": "Granos de nieve", + "wmo_80": "Chubascos débiles", + "wmo_81": "Chubascos moderados", + "wmo_82": "Chubascos violentos", + "wmo_85": "Chubascos de nieve débiles", + "wmo_86": "Chubascos de nieve intensos", + "wmo_95": "Tormenta", + "wmo_96": "Tormenta con granizo ligero", + "wmo_99": "Tormenta con granizo fuerte" } diff --git a/messages/fr.json b/messages/fr.json index 30c7e72..1b7a88f 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -335,5 +335,33 @@ "cadence_varies": "variable", "model_group_automatic": "Automatique", "model_group_reanalysis": "Réanalyse ECMWF", - "model_group_regional_reanalysis": "Réanalyse régionale" + "model_group_regional_reanalysis": "Réanalyse régionale", + "wmo_0": "Ciel dégagé", + "wmo_1": "Plutôt dégagé", + "wmo_2": "Partiellement nuageux", + "wmo_3": "Couvert", + "wmo_45": "Brouillard", + "wmo_48": "Brouillard givrant", + "wmo_51": "Bruine faible", + "wmo_53": "Bruine modérée", + "wmo_55": "Bruine dense", + "wmo_56": "Bruine verglaçante faible", + "wmo_57": "Bruine verglaçante dense", + "wmo_61": "Pluie faible", + "wmo_63": "Pluie modérée", + "wmo_65": "Pluie forte", + "wmo_66": "Pluie verglaçante faible", + "wmo_67": "Pluie verglaçante forte", + "wmo_71": "Neige faible", + "wmo_73": "Neige modérée", + "wmo_75": "Neige forte", + "wmo_77": "Grésil", + "wmo_80": "Averses faibles", + "wmo_81": "Averses modérées", + "wmo_82": "Averses violentes", + "wmo_85": "Averses de neige faibles", + "wmo_86": "Averses de neige fortes", + "wmo_95": "Orage", + "wmo_96": "Orage avec grêle légère", + "wmo_99": "Orage avec forte grêle" } diff --git a/messages/it.json b/messages/it.json index ad8ece2..c43c69c 100644 --- a/messages/it.json +++ b/messages/it.json @@ -335,5 +335,33 @@ "cadence_varies": "variabile", "model_group_automatic": "Automatico", "model_group_reanalysis": "Rianalisi ECMWF", - "model_group_regional_reanalysis": "Rianalisi regionale" + "model_group_regional_reanalysis": "Rianalisi regionale", + "wmo_0": "Cielo sereno", + "wmo_1": "Prevalentemente sereno", + "wmo_2": "Parzialmente nuvoloso", + "wmo_3": "Coperto", + "wmo_45": "Nebbia", + "wmo_48": "Nebbia gelata", + "wmo_51": "Pioviggine debole", + "wmo_53": "Pioviggine moderata", + "wmo_55": "Pioviggine intensa", + "wmo_56": "Pioviggine congelantesi debole", + "wmo_57": "Pioviggine congelantesi intensa", + "wmo_61": "Pioggia debole", + "wmo_63": "Pioggia moderata", + "wmo_65": "Pioggia forte", + "wmo_66": "Pioggia congelantesi debole", + "wmo_67": "Pioggia congelantesi forte", + "wmo_71": "Nevicata debole", + "wmo_73": "Nevicata moderata", + "wmo_75": "Nevicata forte", + "wmo_77": "Granuli di neve", + "wmo_80": "Rovesci deboli", + "wmo_81": "Rovesci moderati", + "wmo_82": "Rovesci violenti", + "wmo_85": "Rovesci di neve deboli", + "wmo_86": "Rovesci di neve forti", + "wmo_95": "Temporale", + "wmo_96": "Temporale con grandine leggera", + "wmo_99": "Temporale con grandine forte" } diff --git a/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte b/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte index ef64421..88aaa20 100644 --- a/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte +++ b/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte @@ -3,7 +3,7 @@ import * as m from '$lib/paraglide/messages'; - import { getWeatherIconName } from '../../utils/weather-codes'; + import { getWeatherDescription, getWeatherIconName } from '../../utils/weather-codes'; interface Props { timestamps: number[]; @@ -62,8 +62,9 @@ let displayModels = $derived(models.filter((m) => hourlyFlat[`weather_code_${m}`])); -{#snippet weatherIcon(name: string, size: number = 24)} +{#snippet weatherIcon(name: string, size: number = 24, description: string = '')} + {#if description}{description}{/if} {/snippet} @@ -135,7 +136,11 @@ ? 'border-l-2 border-l-primary/20' : ''} {!day ? 'bg-indigo-950/5 dark:bg-indigo-500/5' : ''}" > - {@render weatherIcon(getWeatherIconName(code, day))} + {@render weatherIcon( + getWeatherIconName(code, day), + 24, + getWeatherDescription(code) + )} {/each} diff --git a/src/routes/weather/historical/[location]/HistoricalDaily.svelte b/src/routes/weather/historical/[location]/HistoricalDaily.svelte index 1d5f989..3c7d27e 100644 --- a/src/routes/weather/historical/[location]/HistoricalDaily.svelte +++ b/src/routes/weather/historical/[location]/HistoricalDaily.svelte @@ -9,7 +9,7 @@ } from '$lib/services/weather'; import { getColor, getTempStyle } from '../../utils/colors'; - import { getWeatherIconName } from '../../utils/weather-codes'; + import { getWeatherDescription, getWeatherIconName } from '../../utils/weather-codes'; import { type WeatherUnits, getPrecipUnit, getTempUnit } from '../../week/[location]/types'; interface Props { @@ -202,7 +202,8 @@ >{formatZoned(date, timezone, 'd MMM')} - + + {getWeatherDescription(daily.weather_code[i])} string> = { + 0: m.wmo_0, + 1: m.wmo_1, + 2: m.wmo_2, + 3: m.wmo_3, + 45: m.wmo_45, + 48: m.wmo_48, + 51: m.wmo_51, + 53: m.wmo_53, + 55: m.wmo_55, + 56: m.wmo_56, + 57: m.wmo_57, + 61: m.wmo_61, + 63: m.wmo_63, + 65: m.wmo_65, + 66: m.wmo_66, + 67: m.wmo_67, + 71: m.wmo_71, + 73: m.wmo_73, + 75: m.wmo_75, + 77: m.wmo_77, + 80: m.wmo_80, + 81: m.wmo_81, + 82: m.wmo_82, + 85: m.wmo_85, + 86: m.wmo_86, + 95: m.wmo_95, + 96: m.wmo_96, + 99: m.wmo_99 +}; + +/** Localized condition text for a weather code; '' for codes we have no name for. */ +export function getWeatherDescription(code: number | null | undefined): string { + if (code == null || !Number.isFinite(code)) return ''; + return WMO_DESCRIPTIONS[code]?.() ?? ''; +} + // ─── Local day/night weather codes ────────────────────────────────────────── // Open-meteo's daily weather_code is a plain numeric max over all 24 hourly // codes (VariableDaily.swift: `.max(.weathercode)`) — there is no day/night @@ -113,8 +165,17 @@ const FOG = new Set([45, 48]); // counterparts when breaking frequency ties (fixes 80 "outranking" 65). const INTENSITY_EQUIV: Record = { 80: 61, 81: 63, 82: 65, 77: 71, 85: 71, 86: 75 }; -/** Most frequent code; ties go to the more intense (then higher) code. */ -function modeWithHighTiebreak(codes: number[]): number { +/** + * Most frequent code; ties go to the more intense (then higher) code, or with + * `preferLower` to the least intense one. + * + * Thunder is the one group that ties downwards. Within it the code only + * describes how much hail comes with the storm, and letting the worst of them + * win a coin-flip tie is how a single hour of 99 used to brand a whole day as + * the most extreme thing on the scale. Elsewhere the heavier code winning a tie + * is the point (heavy rain over slight showers). + */ +function modeWithHighTiebreak(codes: number[], preferLower = false): number { const counts = new Map(); for (const c of codes) counts.set(c, (counts.get(c) ?? 0) + 1); let best = codes[0]; @@ -122,11 +183,10 @@ function modeWithHighTiebreak(codes: number[]): number { for (const [code, count] of counts) { const intensity = INTENSITY_EQUIV[code] ?? code; const bestIntensity = INTENSITY_EQUIV[best] ?? best; - if ( - count > bestCount || - (count === bestCount && - (intensity > bestIntensity || (intensity === bestIntensity && code > best))) - ) { + const winsTie = preferLower + ? intensity < bestIntensity || (intensity === bestIntensity && code < best) + : intensity > bestIntensity || (intensity === bestIntensity && code > best); + if (count > bestCount || (count === bestCount && winsTie)) { best = code; bestCount = count; } @@ -144,7 +204,7 @@ function daypartCode(codes: number[]): number | null { // (open-meteo's "don't hide hazards" philosophy, kept per-group). for (const group of [THUNDER, FREEZING, SNOW, RAIN, DRIZZLE]) { const hits = hours.filter((c) => group.has(c)); - if (hits.length > 0) return modeWithHighTiebreak(hits); + if (hits.length > 0) return modeWithHighTiebreak(hits, group === THUNDER); } // Fog needs persistence (≥2h and ≥¼ of the daypart) so one misty hour at diff --git a/src/routes/weather/week/[location]/DailyCards.svelte b/src/routes/weather/week/[location]/DailyCards.svelte index debe2fd..5c3e440 100644 --- a/src/routes/weather/week/[location]/DailyCards.svelte +++ b/src/routes/weather/week/[location]/DailyCards.svelte @@ -6,7 +6,7 @@ import * as m from '$lib/paraglide/messages'; import { getTempStyle } from '../../utils/colors'; - import { getWeatherIconName } from '../../utils/weather-codes'; + import { getWeatherDescription, getWeatherIconName } from '../../utils/weather-codes'; import { precipIsSignificant, sunIsSignificant, windIsSignificant } from './significance'; import { type FetchedDaily, type WeatherUnits, getWindArrowRotation } from './types'; @@ -196,6 +196,7 @@ height="100px" style="filter: url(#thin-day-icon)" > + {getWeatherDescription(dayCode)} + {getWeatherDescription(nightCode)} + {getWeatherDescription(dayCode)} + {getWeatherDescription(nightCode)} + {#if description}{description}{/if} {/snippet} @@ -678,7 +679,11 @@ !cellData[i + 1].isDaytime} > {#if finite(wCode)} - {@render weatherIcon(getWeatherIconName(wCode, cell.isDaytime), iconPx)} + {@render weatherIcon( + getWeatherIconName(wCode, cell.isDaytime), + iconPx, + getWeatherDescription(wCode) + )} {/if} {/each} diff --git a/src/routes/weather/week/[location]/NearbyCities.svelte b/src/routes/weather/week/[location]/NearbyCities.svelte index 8c5bb53..442d87c 100644 --- a/src/routes/weather/week/[location]/NearbyCities.svelte +++ b/src/routes/weather/week/[location]/NearbyCities.svelte @@ -9,7 +9,7 @@ import { type NearbyCity, findNearbyCities } from '$lib/services/nearby-cities'; import { type NearbyDaily, fetchNearbyDaily } from '$lib/services/weather'; - import { getWeatherIconName } from '../../utils/weather-codes'; + import { getWeatherDescription, getWeatherIconName } from '../../utils/weather-codes'; import type { UnitPrefs } from '$lib/stores/settings'; @@ -113,6 +113,7 @@ > {#if day} + {getWeatherDescription(day.weatherCode)}