feat: add weather unit preferences
Some checks failed
CI / Lint, test, typecheck and build (push) Has been cancelled
Some checks failed
CI / Lint, test, typecheck and build (push) Has been cancelled
This commit is contained in:
@@ -3,11 +3,13 @@ import { translate } from "@/lib/i18n";
|
||||
import {
|
||||
DEFAULT_TEMPERATURE_UNIT,
|
||||
DEFAULT_WIND_SPEED_UNIT,
|
||||
DEFAULT_PRECIPITATION_UNIT,
|
||||
formatPrecipitation,
|
||||
formatTemperature,
|
||||
formatWindSpeed,
|
||||
} from "@/lib/weather-utils";
|
||||
import type { DailyForecast, HourlyForecast } from "@/types/forecast";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import type { PrecipitationUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
export function getForecastConditionKey(code: number | null): TranslationKey {
|
||||
if (code === 0) return "forecast.condition.clear";
|
||||
@@ -47,9 +49,13 @@ export function formatForecastTemperature(
|
||||
return formatTemperature(value, language, unit);
|
||||
}
|
||||
|
||||
export function formatForecastRainfall(value: number | null, language: Language) {
|
||||
export function formatForecastRainfall(
|
||||
value: number | null,
|
||||
language: Language,
|
||||
unit: PrecipitationUnit = DEFAULT_PRECIPITATION_UNIT,
|
||||
) {
|
||||
if (value === null) return "—";
|
||||
return `${new Intl.NumberFormat(language === "pl" ? "pl-PL" : "en-GB", { maximumFractionDigits: 1 }).format(value)} mm`;
|
||||
return formatPrecipitation(value, language, unit);
|
||||
}
|
||||
|
||||
export function formatForecastWind(
|
||||
|
||||
58
lib/i18n.tsx
58
lib/i18n.tsx
@@ -54,6 +54,27 @@ const translations = {
|
||||
"settings.units.wind.ms": "m/s",
|
||||
"settings.units.wind.kmh": "km/h",
|
||||
"settings.units.wind.mph": "mph",
|
||||
"settings.units.wind.kt": "kt",
|
||||
"settings.units.wind.bft": "Bft",
|
||||
"settings.units.precipitation.title": "Jednostka opadu",
|
||||
"settings.units.precipitation.description":
|
||||
"Stosowana w prognozie, briefach, powiadomieniach i bieżących pomiarach na tym urządzeniu.",
|
||||
"settings.units.precipitation.label": "Wybierz jednostkę opadu",
|
||||
"settings.units.precipitation.mm": "mm",
|
||||
"settings.units.precipitation.cm": "cm",
|
||||
"settings.units.precipitation.in": "in",
|
||||
"settings.units.pressure.title": "Jednostka ciśnienia",
|
||||
"settings.units.pressure.description": "Stosowana w bieżących pomiarach na tym urządzeniu.",
|
||||
"settings.units.pressure.label": "Wybierz jednostkę ciśnienia",
|
||||
"settings.units.pressure.hpa": "hPa",
|
||||
"settings.units.pressure.kpa": "kPa",
|
||||
"settings.units.pressure.inhg": "inHg",
|
||||
"settings.units.pressure.mmhg": "mm Hg",
|
||||
"settings.units.distance.title": "Jednostka dystansu",
|
||||
"settings.units.distance.description": "Stosowana przy odległości do najbliższej stacji.",
|
||||
"settings.units.distance.label": "Wybierz jednostkę dystansu",
|
||||
"settings.units.distance.km": "km",
|
||||
"settings.units.distance.mi": "mi",
|
||||
"settings.notifications.title": "Powiadomienia o ostrzeżeniach meteo",
|
||||
"settings.notifications.description":
|
||||
"Przygotuj alerty dla nowych ostrzeżeń meteorologicznych IMGW, takich jak burze, upał, silny wiatr lub intensywny deszcz.",
|
||||
@@ -121,17 +142,17 @@ const translations = {
|
||||
"location.nearest": "Najbliższa stacja IMGW",
|
||||
"location.modelSource": "Źródło modelowe",
|
||||
"location.currentSource":
|
||||
"{location}: współrzędne miejscowości są używane dla lokalnej analizy IMGW Hybrid. Najbliższa stacja pomiarowa IMGW: {station} · około {distance} km.",
|
||||
"{location}: współrzędne miejscowości są używane dla lokalnej analizy IMGW Hybrid. Najbliższa stacja pomiarowa IMGW: {station} · około {distance}.",
|
||||
"location.currentSourceGlobal":
|
||||
"{location}: współrzędne są używane dla modelowych warunków bieżących i prognozy Open-Meteo.",
|
||||
"location.heroHybridSource": "Analiza IMGW Hybrid dla lokalizacji: {location}",
|
||||
"location.heroGlobalModelSource": "Modelowe warunki bieżące Open-Meteo dla lokalizacji: {location}",
|
||||
"location.heroHybridLoading": "Pobieram lokalną analizę IMGW Hybrid. Tymczasowo pokazuję odczyt stacji: {station}.",
|
||||
"location.heroHybridPartial":
|
||||
"Lokalna analiza opadu IMGW Hybrid. Pozostałe parametry zastępczo ze stacji IMGW: {station} · około {distance} km",
|
||||
"location.heroNearestStation": "Najbliższa stacja pomiarowa IMGW: {station} · około {distance} km",
|
||||
"Lokalna analiza opadu IMGW Hybrid. Pozostałe parametry zastępczo ze stacji IMGW: {station} · około {distance}",
|
||||
"location.heroNearestStation": "Najbliższa stacja pomiarowa IMGW: {station} · około {distance}",
|
||||
"location.heroStationFallback": "Dane zastępcze ze stacji IMGW: {station}",
|
||||
"location.heroStationFallbackWithDistance": "Dane zastępcze ze stacji IMGW: {station} · około {distance} km",
|
||||
"location.heroStationFallbackWithDistance": "Dane zastępcze ze stacji IMGW: {station} · około {distance}",
|
||||
"location.heroDistantFallback": "Stacja jest oddalona od lokalizacji. Lokalne warunki mogą się różnić.",
|
||||
"location.attribution": "Wyszukiwanie miejscowości:",
|
||||
"location.gpsUse": "Użyj mojej lokalizacji",
|
||||
@@ -344,6 +365,27 @@ const translations = {
|
||||
"settings.units.wind.ms": "m/s",
|
||||
"settings.units.wind.kmh": "km/h",
|
||||
"settings.units.wind.mph": "mph",
|
||||
"settings.units.wind.kt": "kt",
|
||||
"settings.units.wind.bft": "Bft",
|
||||
"settings.units.precipitation.title": "Precipitation unit",
|
||||
"settings.units.precipitation.description":
|
||||
"Used in forecasts, briefs, notifications and current readings on this device.",
|
||||
"settings.units.precipitation.label": "Choose precipitation unit",
|
||||
"settings.units.precipitation.mm": "mm",
|
||||
"settings.units.precipitation.cm": "cm",
|
||||
"settings.units.precipitation.in": "in",
|
||||
"settings.units.pressure.title": "Pressure unit",
|
||||
"settings.units.pressure.description": "Used in current readings on this device.",
|
||||
"settings.units.pressure.label": "Choose pressure unit",
|
||||
"settings.units.pressure.hpa": "hPa",
|
||||
"settings.units.pressure.kpa": "kPa",
|
||||
"settings.units.pressure.inhg": "inHg",
|
||||
"settings.units.pressure.mmhg": "mm Hg",
|
||||
"settings.units.distance.title": "Distance unit",
|
||||
"settings.units.distance.description": "Used for the distance to the nearest station.",
|
||||
"settings.units.distance.label": "Choose distance unit",
|
||||
"settings.units.distance.km": "km",
|
||||
"settings.units.distance.mi": "mi",
|
||||
"settings.notifications.title": "Weather warning notifications",
|
||||
"settings.notifications.description":
|
||||
"Prepare alerts for new IMGW meteorological warnings, such as thunderstorms, heat, strong wind or heavy rain.",
|
||||
@@ -410,7 +452,7 @@ const translations = {
|
||||
"location.nearest": "Nearest IMGW station",
|
||||
"location.modelSource": "Model source",
|
||||
"location.currentSource":
|
||||
"{location}: the place coordinates are used for local IMGW Hybrid analysis. Nearest IMGW measurement station: {station} · approximately {distance} km away.",
|
||||
"{location}: the place coordinates are used for local IMGW Hybrid analysis. Nearest IMGW measurement station: {station} · approximately {distance} away.",
|
||||
"location.heroHybridSource": "IMGW Hybrid analysis for: {location}",
|
||||
"location.currentSourceGlobal":
|
||||
"{location}: coordinates are used for Open-Meteo model current conditions and forecast.",
|
||||
@@ -418,11 +460,11 @@ const translations = {
|
||||
"location.heroHybridLoading":
|
||||
"Loading local IMGW Hybrid analysis. Temporarily showing the station reading: {station}.",
|
||||
"location.heroHybridPartial":
|
||||
"Local IMGW Hybrid rainfall analysis. Other parameters use fallback data from IMGW station: {station} · approximately {distance} km away",
|
||||
"location.heroNearestStation": "Nearest IMGW measurement station: {station} · approximately {distance} km away",
|
||||
"Local IMGW Hybrid rainfall analysis. Other parameters use fallback data from IMGW station: {station} · approximately {distance} away",
|
||||
"location.heroNearestStation": "Nearest IMGW measurement station: {station} · approximately {distance} away",
|
||||
"location.heroStationFallback": "Fallback data from IMGW station: {station}",
|
||||
"location.heroStationFallbackWithDistance":
|
||||
"Fallback data from IMGW station: {station} · approximately {distance} km away",
|
||||
"Fallback data from IMGW station: {station} · approximately {distance} away",
|
||||
"location.heroDistantFallback": "The station is far from this place. Local conditions may differ.",
|
||||
"location.attribution": "Place search:",
|
||||
"location.gpsUse": "Use my location",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Language } from "@/lib/i18n";
|
||||
import type { Province } from "@/types/province";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import type { DistanceUnit, PrecipitationUnit, PressureUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import type { WeatherRegion } from "@/types/weather-region";
|
||||
|
||||
interface VapidKeyResponse {
|
||||
@@ -26,6 +26,9 @@ interface SavePushSubscriptionOptions {
|
||||
countyTeryt?: string | null;
|
||||
temperatureUnit?: TemperatureUnit;
|
||||
windSpeedUnit?: WindSpeedUnit;
|
||||
precipitationUnit?: PrecipitationUnit;
|
||||
pressureUnit?: PressureUnit;
|
||||
distanceUnit?: DistanceUnit;
|
||||
}
|
||||
|
||||
export async function savePushSubscription(
|
||||
@@ -52,6 +55,9 @@ export async function savePushSubscription(
|
||||
countyTeryt: options.countyTeryt ?? null,
|
||||
temperatureUnit: options.temperatureUnit,
|
||||
windSpeedUnit: options.windSpeedUnit,
|
||||
precipitationUnit: options.precipitationUnit,
|
||||
pressureUnit: options.pressureUnit,
|
||||
distanceUnit: options.distanceUnit,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Unable to save push subscription.");
|
||||
|
||||
@@ -2,8 +2,14 @@ import Database from "better-sqlite3";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
DEFAULT_DISTANCE_UNIT,
|
||||
DEFAULT_PRECIPITATION_UNIT,
|
||||
DEFAULT_PRESSURE_UNIT,
|
||||
DEFAULT_TEMPERATURE_UNIT,
|
||||
DEFAULT_WIND_SPEED_UNIT,
|
||||
isDistanceUnit,
|
||||
isPrecipitationUnit,
|
||||
isPressureUnit,
|
||||
isTemperatureUnit,
|
||||
isWindSpeedUnit,
|
||||
} from "@/lib/weather-utils";
|
||||
@@ -31,6 +37,9 @@ interface PushSubscriptionRow {
|
||||
county_teryt: string | null;
|
||||
temperature_unit: string | null;
|
||||
wind_speed_unit: string | null;
|
||||
precipitation_unit: string | null;
|
||||
pressure_unit: string | null;
|
||||
distance_unit: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -71,6 +80,9 @@ function initializeDatabase(database: Database.Database) {
|
||||
county_teryt TEXT,
|
||||
temperature_unit TEXT NOT NULL DEFAULT '${DEFAULT_TEMPERATURE_UNIT}',
|
||||
wind_speed_unit TEXT NOT NULL DEFAULT '${DEFAULT_WIND_SPEED_UNIT}',
|
||||
precipitation_unit TEXT NOT NULL DEFAULT '${DEFAULT_PRECIPITATION_UNIT}',
|
||||
pressure_unit TEXT NOT NULL DEFAULT '${DEFAULT_PRESSURE_UNIT}',
|
||||
distance_unit TEXT NOT NULL DEFAULT '${DEFAULT_DISTANCE_UNIT}',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
@@ -106,6 +118,27 @@ function initializeDatabase(database: Database.Database) {
|
||||
if (!columns.some((column) => column.name === "timezone")) {
|
||||
database.prepare("ALTER TABLE push_subscriptions ADD COLUMN timezone TEXT").run();
|
||||
}
|
||||
if (!columns.some((column) => column.name === "precipitation_unit")) {
|
||||
database
|
||||
.prepare(
|
||||
`ALTER TABLE push_subscriptions ADD COLUMN precipitation_unit TEXT NOT NULL DEFAULT '${DEFAULT_PRECIPITATION_UNIT}'`,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
if (!columns.some((column) => column.name === "pressure_unit")) {
|
||||
database
|
||||
.prepare(
|
||||
`ALTER TABLE push_subscriptions ADD COLUMN pressure_unit TEXT NOT NULL DEFAULT '${DEFAULT_PRESSURE_UNIT}'`,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
if (!columns.some((column) => column.name === "distance_unit")) {
|
||||
database
|
||||
.prepare(
|
||||
`ALTER TABLE push_subscriptions ADD COLUMN distance_unit TEXT NOT NULL DEFAULT '${DEFAULT_DISTANCE_UNIT}'`,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
function getDatabase() {
|
||||
@@ -139,6 +172,11 @@ function parseSubscription(row: PushSubscriptionRow): WarningPushSubscription |
|
||||
countyTeryt: row.county_teryt,
|
||||
temperatureUnit: isTemperatureUnit(row.temperature_unit) ? row.temperature_unit : DEFAULT_TEMPERATURE_UNIT,
|
||||
windSpeedUnit: isWindSpeedUnit(row.wind_speed_unit) ? row.wind_speed_unit : DEFAULT_WIND_SPEED_UNIT,
|
||||
precipitationUnit: isPrecipitationUnit(row.precipitation_unit)
|
||||
? row.precipitation_unit
|
||||
: DEFAULT_PRECIPITATION_UNIT,
|
||||
pressureUnit: isPressureUnit(row.pressure_unit) ? row.pressure_unit : DEFAULT_PRESSURE_UNIT,
|
||||
distanceUnit: isDistanceUnit(row.distance_unit) ? row.distance_unit : DEFAULT_DISTANCE_UNIT,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
@@ -167,6 +205,9 @@ export function upsertPushSubscription(subscription: WarningPushSubscription) {
|
||||
county_teryt,
|
||||
temperature_unit,
|
||||
wind_speed_unit,
|
||||
precipitation_unit,
|
||||
pressure_unit,
|
||||
distance_unit,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@@ -185,6 +226,9 @@ export function upsertPushSubscription(subscription: WarningPushSubscription) {
|
||||
@countyTeryt,
|
||||
@temperatureUnit,
|
||||
@windSpeedUnit,
|
||||
@precipitationUnit,
|
||||
@pressureUnit,
|
||||
@distanceUnit,
|
||||
@createdAt,
|
||||
@updatedAt
|
||||
)
|
||||
@@ -203,6 +247,9 @@ export function upsertPushSubscription(subscription: WarningPushSubscription) {
|
||||
county_teryt = excluded.county_teryt,
|
||||
temperature_unit = excluded.temperature_unit,
|
||||
wind_speed_unit = excluded.wind_speed_unit,
|
||||
precipitation_unit = excluded.precipitation_unit,
|
||||
pressure_unit = excluded.pressure_unit,
|
||||
distance_unit = excluded.distance_unit,
|
||||
updated_at = excluded.updated_at
|
||||
`,
|
||||
)
|
||||
@@ -222,6 +269,9 @@ export function upsertPushSubscription(subscription: WarningPushSubscription) {
|
||||
countyTeryt: subscription.countyTeryt,
|
||||
temperatureUnit: subscription.temperatureUnit,
|
||||
windSpeedUnit: subscription.windSpeedUnit,
|
||||
precipitationUnit: subscription.precipitationUnit,
|
||||
pressureUnit: subscription.pressureUnit,
|
||||
distanceUnit: subscription.distanceUnit,
|
||||
createdAt: subscription.createdAt,
|
||||
updatedAt: subscription.updatedAt,
|
||||
});
|
||||
|
||||
39
lib/store.ts
39
lib/store.ts
@@ -4,7 +4,7 @@ import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { SelectedLocation } from "@/types/location";
|
||||
import type { Province } from "@/types/province";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import type { DistanceUnit, PrecipitationUnit, PressureUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import {
|
||||
DEFAULT_APP_SECTION_VISIBILITY,
|
||||
normalizeAppSectionVisibility,
|
||||
@@ -17,7 +17,18 @@ import {
|
||||
type DashboardSectionId,
|
||||
type DashboardSectionVisibility,
|
||||
} from "@/lib/dashboard-sections";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils";
|
||||
import {
|
||||
DEFAULT_DISTANCE_UNIT,
|
||||
DEFAULT_PRECIPITATION_UNIT,
|
||||
DEFAULT_PRESSURE_UNIT,
|
||||
DEFAULT_TEMPERATURE_UNIT,
|
||||
DEFAULT_WIND_SPEED_UNIT,
|
||||
isDistanceUnit,
|
||||
isPrecipitationUnit,
|
||||
isPressureUnit,
|
||||
isTemperatureUnit,
|
||||
isWindSpeedUnit,
|
||||
} from "@/lib/weather-utils";
|
||||
import type { WeatherRegion } from "@/types/weather-region";
|
||||
import { getWeatherRegionForCountryCode } from "@/types/weather-region";
|
||||
|
||||
@@ -34,6 +45,9 @@ interface WeatherStore {
|
||||
warningNotificationProvince: Province | null;
|
||||
temperatureUnit: TemperatureUnit;
|
||||
windSpeedUnit: WindSpeedUnit;
|
||||
precipitationUnit: PrecipitationUnit;
|
||||
pressureUnit: PressureUnit;
|
||||
distanceUnit: DistanceUnit;
|
||||
dashboardSections: DashboardSectionVisibility;
|
||||
appSections: AppSectionVisibility;
|
||||
toggleFavorite: (id: string) => void;
|
||||
@@ -46,6 +60,9 @@ interface WeatherStore {
|
||||
setWarningNotificationProvince: (province: Province | null) => void;
|
||||
setTemperatureUnit: (unit: TemperatureUnit) => void;
|
||||
setWindSpeedUnit: (unit: WindSpeedUnit) => void;
|
||||
setPrecipitationUnit: (unit: PrecipitationUnit) => void;
|
||||
setPressureUnit: (unit: PressureUnit) => void;
|
||||
setDistanceUnit: (unit: DistanceUnit) => void;
|
||||
setDashboardSectionVisible: (section: DashboardSectionId, visible: boolean) => void;
|
||||
setAppSectionVisible: (section: AppSectionId, visible: boolean) => void;
|
||||
}
|
||||
@@ -63,6 +80,9 @@ export const useWeatherStore = create<WeatherStore>()(
|
||||
warningNotificationProvince: null,
|
||||
temperatureUnit: DEFAULT_TEMPERATURE_UNIT,
|
||||
windSpeedUnit: DEFAULT_WIND_SPEED_UNIT,
|
||||
precipitationUnit: DEFAULT_PRECIPITATION_UNIT,
|
||||
pressureUnit: DEFAULT_PRESSURE_UNIT,
|
||||
distanceUnit: DEFAULT_DISTANCE_UNIT,
|
||||
dashboardSections: DEFAULT_DASHBOARD_SECTION_VISIBILITY,
|
||||
appSections: DEFAULT_APP_SECTION_VISIBILITY,
|
||||
toggleFavorite: (id) =>
|
||||
@@ -80,6 +100,9 @@ export const useWeatherStore = create<WeatherStore>()(
|
||||
setWarningNotificationProvince: (province) => set({ warningNotificationProvince: province }),
|
||||
setTemperatureUnit: (unit) => set({ temperatureUnit: unit }),
|
||||
setWindSpeedUnit: (unit) => set({ windSpeedUnit: unit }),
|
||||
setPrecipitationUnit: (unit) => set({ precipitationUnit: unit }),
|
||||
setPressureUnit: (unit) => set({ pressureUnit: unit }),
|
||||
setDistanceUnit: (unit) => set({ distanceUnit: unit }),
|
||||
setDashboardSectionVisible: (section, visible) =>
|
||||
set((state) => ({
|
||||
dashboardSections: { ...state.dashboardSections, [section]: visible },
|
||||
@@ -100,11 +123,21 @@ export const useWeatherStore = create<WeatherStore>()(
|
||||
if (!state) return persisted;
|
||||
const dashboardSections = normalizeDashboardSectionVisibility(state.dashboardSections);
|
||||
const appSections = normalizeAppSectionVisibility(state.appSections);
|
||||
if (!location) return { ...state, dashboardSections, appSections };
|
||||
const unitPreferences = {
|
||||
temperatureUnit: isTemperatureUnit(state.temperatureUnit) ? state.temperatureUnit : DEFAULT_TEMPERATURE_UNIT,
|
||||
windSpeedUnit: isWindSpeedUnit(state.windSpeedUnit) ? state.windSpeedUnit : DEFAULT_WIND_SPEED_UNIT,
|
||||
precipitationUnit: isPrecipitationUnit(state.precipitationUnit)
|
||||
? state.precipitationUnit
|
||||
: DEFAULT_PRECIPITATION_UNIT,
|
||||
pressureUnit: isPressureUnit(state.pressureUnit) ? state.pressureUnit : DEFAULT_PRESSURE_UNIT,
|
||||
distanceUnit: isDistanceUnit(state.distanceUnit) ? state.distanceUnit : DEFAULT_DISTANCE_UNIT,
|
||||
};
|
||||
if (!location) return { ...state, ...unitPreferences, dashboardSections, appSections };
|
||||
const countryCode = location.countryCode ?? (location.province ? "PL" : null);
|
||||
const region = location.region ?? getWeatherRegionForCountryCode(countryCode);
|
||||
return {
|
||||
...state,
|
||||
...unitPreferences,
|
||||
dashboardSections,
|
||||
appSections,
|
||||
selectedLocation: {
|
||||
|
||||
@@ -8,10 +8,12 @@ import { warningMatchesCounty } from "@/lib/warning-regions";
|
||||
import {
|
||||
DEFAULT_TEMPERATURE_UNIT,
|
||||
DEFAULT_WIND_SPEED_UNIT,
|
||||
DEFAULT_PRECIPITATION_UNIT,
|
||||
formatPrecipitation as formatDisplayPrecipitation,
|
||||
formatTemperature as formatDisplayTemperature,
|
||||
formatWindSpeed,
|
||||
} from "@/lib/weather-utils";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import type { PrecipitationUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
export type WeatherBriefSeverity = "normal" | "attention" | "warning";
|
||||
|
||||
@@ -33,19 +35,10 @@ interface BuildWeatherBriefInput {
|
||||
language: Language;
|
||||
temperatureUnit?: TemperatureUnit;
|
||||
windSpeedUnit?: WindSpeedUnit;
|
||||
precipitationUnit?: PrecipitationUnit;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
function formatNumber(value: number, language: Language, digits = 0) {
|
||||
return new Intl.NumberFormat(language === "pl" ? "pl-PL" : "en-GB", {
|
||||
maximumFractionDigits: digits,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function formatRainfall(value: number, language: Language) {
|
||||
return `${formatNumber(value, language, 1)} mm`;
|
||||
}
|
||||
|
||||
function formatHour(value: string) {
|
||||
return value.slice(11, 16);
|
||||
}
|
||||
@@ -237,6 +230,7 @@ export function buildWeatherBrief({
|
||||
language,
|
||||
temperatureUnit = DEFAULT_TEMPERATURE_UNIT,
|
||||
windSpeedUnit = DEFAULT_WIND_SPEED_UNIT,
|
||||
precipitationUnit = DEFAULT_PRECIPITATION_UNIT,
|
||||
now = new Date(),
|
||||
}: BuildWeatherBriefInput): WeatherBrief | null {
|
||||
const hours = getUpcomingHours(forecast.hourly, now, 24);
|
||||
@@ -313,8 +307,8 @@ export function buildWeatherBrief({
|
||||
: "";
|
||||
body.push(
|
||||
language === "pl"
|
||||
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatRainfall(rainfallTotal, language)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
|
||||
: `Rainfall: ${rainfallTotal === null ? "not fully available" : formatRainfall(rainfallTotal, language)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`,
|
||||
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatDisplayPrecipitation(rainfallTotal, language, precipitationUnit)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
|
||||
: `Rainfall: ${rainfallTotal === null ? "not fully available" : formatDisplayPrecipitation(rainfallTotal, language, precipitationUnit)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`,
|
||||
);
|
||||
}
|
||||
if (maximumWind !== null) {
|
||||
@@ -371,6 +365,7 @@ export function buildTomorrowWeatherBrief({
|
||||
language,
|
||||
temperatureUnit = DEFAULT_TEMPERATURE_UNIT,
|
||||
windSpeedUnit = DEFAULT_WIND_SPEED_UNIT,
|
||||
precipitationUnit = DEFAULT_PRECIPITATION_UNIT,
|
||||
now = new Date(),
|
||||
}: BuildWeatherBriefInput): WeatherBrief | null {
|
||||
const targetDateKey = getWarsawDateKey(now, 1);
|
||||
@@ -453,8 +448,8 @@ export function buildTomorrowWeatherBrief({
|
||||
: "";
|
||||
body.push(
|
||||
language === "pl"
|
||||
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatRainfall(rainfallTotal, language)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
|
||||
: `Precipitation: ${rainfallTotal === null ? "not fully available" : formatRainfall(rainfallTotal, language)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`,
|
||||
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatDisplayPrecipitation(rainfallTotal, language, precipitationUnit)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
|
||||
: `Precipitation: ${rainfallTotal === null ? "not fully available" : formatDisplayPrecipitation(rainfallTotal, language, precipitationUnit)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`,
|
||||
);
|
||||
} else {
|
||||
body.push(language === "pl" ? "Bez istotnego opadu w prognozie." : "No significant precipitation in the forecast.");
|
||||
@@ -469,7 +464,7 @@ export function buildTomorrowWeatherBrief({
|
||||
|
||||
const rainPushPart =
|
||||
(hasRainSignal || (rainfallTotal ?? 0) > 0.1) && rainfallTotal !== null
|
||||
? formatRainfall(rainfallTotal, language)
|
||||
? formatDisplayPrecipitation(rainfallTotal, language, precipitationUnit)
|
||||
: null;
|
||||
const pushParts = [
|
||||
`${locationName}:`,
|
||||
|
||||
@@ -11,13 +11,19 @@ import type {
|
||||
import { translate, type Language } from "@/lib/i18n";
|
||||
import { getProvinceFromTeryt, normalizeProvinceName } from "@/lib/provinces";
|
||||
import type { CurrentWeatherCondition } from "@/types/imgw-current";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import type { DistanceUnit, PrecipitationUnit, PressureUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
const locales: Record<Language, string> = { pl: "pl-PL", en: "en-GB" };
|
||||
export const DEFAULT_TEMPERATURE_UNIT: TemperatureUnit = "c";
|
||||
export const TEMPERATURE_UNITS: TemperatureUnit[] = ["c", "f"];
|
||||
export const DEFAULT_WIND_SPEED_UNIT: WindSpeedUnit = "kmh";
|
||||
export const WIND_SPEED_UNITS: WindSpeedUnit[] = ["ms", "kmh", "mph"];
|
||||
export const WIND_SPEED_UNITS: WindSpeedUnit[] = ["ms", "kmh", "mph", "kt", "bft"];
|
||||
export const DEFAULT_PRECIPITATION_UNIT: PrecipitationUnit = "mm";
|
||||
export const PRECIPITATION_UNITS: PrecipitationUnit[] = ["mm", "cm", "in"];
|
||||
export const DEFAULT_PRESSURE_UNIT: PressureUnit = "hpa";
|
||||
export const PRESSURE_UNITS: PressureUnit[] = ["hpa", "kpa", "inhg", "mmhg"];
|
||||
export const DEFAULT_DISTANCE_UNIT: DistanceUnit = "km";
|
||||
export const DISTANCE_UNITS: DistanceUnit[] = ["km", "mi"];
|
||||
|
||||
const temperatureUnitLabels: Record<TemperatureUnit, string> = {
|
||||
c: "°C",
|
||||
@@ -28,6 +34,26 @@ const windSpeedUnitLabels: Record<WindSpeedUnit, string> = {
|
||||
ms: "m/s",
|
||||
kmh: "km/h",
|
||||
mph: "mph",
|
||||
kt: "kt",
|
||||
bft: "Bft",
|
||||
};
|
||||
|
||||
const precipitationUnitLabels: Record<PrecipitationUnit, string> = {
|
||||
mm: "mm",
|
||||
cm: "cm",
|
||||
in: "in",
|
||||
};
|
||||
|
||||
const pressureUnitLabels: Record<PressureUnit, string> = {
|
||||
hpa: "hPa",
|
||||
kpa: "kPa",
|
||||
inhg: "inHg",
|
||||
mmhg: "mm Hg",
|
||||
};
|
||||
|
||||
const distanceUnitLabels: Record<DistanceUnit, string> = {
|
||||
km: "km",
|
||||
mi: "mi",
|
||||
};
|
||||
|
||||
export function toNumber(value: unknown): number | null {
|
||||
@@ -152,10 +178,6 @@ export function formatTemperature(
|
||||
: formatTemperatureValue(convertTemperature(value, unit), language, unit);
|
||||
}
|
||||
|
||||
export function formatPressure(value: number | null, language: Language = "pl") {
|
||||
return value === null ? translate(language, "common.noData") : `${value.toFixed(1)} hPa`;
|
||||
}
|
||||
|
||||
export function formatHumidity(value: number | null, language: Language = "pl") {
|
||||
return value === null ? translate(language, "common.noData") : `${Math.round(value)}%`;
|
||||
}
|
||||
@@ -168,18 +190,60 @@ export function getWindSpeedUnitLabel(unit: WindSpeedUnit) {
|
||||
return windSpeedUnitLabels[unit];
|
||||
}
|
||||
|
||||
export function isPrecipitationUnit(value: unknown): value is PrecipitationUnit {
|
||||
return typeof value === "string" && PRECIPITATION_UNITS.includes(value as PrecipitationUnit);
|
||||
}
|
||||
|
||||
export function getPrecipitationUnitLabel(unit: PrecipitationUnit) {
|
||||
return precipitationUnitLabels[unit];
|
||||
}
|
||||
|
||||
export function isPressureUnit(value: unknown): value is PressureUnit {
|
||||
return typeof value === "string" && PRESSURE_UNITS.includes(value as PressureUnit);
|
||||
}
|
||||
|
||||
export function getPressureUnitLabel(unit: PressureUnit) {
|
||||
return pressureUnitLabels[unit];
|
||||
}
|
||||
|
||||
export function isDistanceUnit(value: unknown): value is DistanceUnit {
|
||||
return typeof value === "string" && DISTANCE_UNITS.includes(value as DistanceUnit);
|
||||
}
|
||||
|
||||
export function getDistanceUnitLabel(unit: DistanceUnit) {
|
||||
return distanceUnitLabels[unit];
|
||||
}
|
||||
|
||||
export function convertWindSpeed(speed: number, unit: WindSpeedUnit) {
|
||||
if (unit === "kmh") return speed * 3.6;
|
||||
if (unit === "mph") return speed * 2.2369362921;
|
||||
if (unit === "kt") return speed * 1.9438444924;
|
||||
return speed;
|
||||
}
|
||||
|
||||
export function convertWindSpeedToBeaufort(speed: number) {
|
||||
if (speed < 0.3) return 0;
|
||||
if (speed < 1.6) return 1;
|
||||
if (speed < 3.4) return 2;
|
||||
if (speed < 5.5) return 3;
|
||||
if (speed < 8) return 4;
|
||||
if (speed < 10.8) return 5;
|
||||
if (speed < 13.9) return 6;
|
||||
if (speed < 17.2) return 7;
|
||||
if (speed < 20.8) return 8;
|
||||
if (speed < 24.5) return 9;
|
||||
if (speed < 28.5) return 10;
|
||||
if (speed < 32.7) return 11;
|
||||
return 12;
|
||||
}
|
||||
|
||||
export function formatWindSpeed(
|
||||
speed: number | null,
|
||||
language: Language = "pl",
|
||||
unit: WindSpeedUnit = DEFAULT_WIND_SPEED_UNIT,
|
||||
) {
|
||||
if (speed === null) return translate(language, "common.noData");
|
||||
if (unit === "bft") return `${convertWindSpeedToBeaufort(speed)} ${getWindSpeedUnitLabel(unit)}`;
|
||||
const convertedSpeed = convertWindSpeed(speed, unit);
|
||||
const digits = unit === "ms" ? 1 : 0;
|
||||
const formattedSpeed = new Intl.NumberFormat(locales[language], {
|
||||
@@ -200,8 +264,73 @@ export function formatWind(
|
||||
return `${formatWindSpeed(speed, language, unit)}${directionLabel}`;
|
||||
}
|
||||
|
||||
export function formatRainfall(value: number | null, language: Language = "pl") {
|
||||
return value === null ? translate(language, "common.noData") : `${value.toFixed(value < 1 ? 2 : 1)} mm`;
|
||||
export function formatRainfall(
|
||||
value: number | null,
|
||||
language: Language = "pl",
|
||||
unit: PrecipitationUnit = DEFAULT_PRECIPITATION_UNIT,
|
||||
) {
|
||||
return formatPrecipitation(value, language, unit);
|
||||
}
|
||||
|
||||
export function convertPrecipitation(value: number, unit: PrecipitationUnit) {
|
||||
if (unit === "cm") return value / 10;
|
||||
if (unit === "in") return value / 25.4;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatPrecipitation(
|
||||
value: number | null,
|
||||
language: Language = "pl",
|
||||
unit: PrecipitationUnit = DEFAULT_PRECIPITATION_UNIT,
|
||||
) {
|
||||
if (value === null) return translate(language, "common.noData");
|
||||
const convertedValue = convertPrecipitation(value, unit);
|
||||
const digits = unit === "mm" ? (convertedValue < 1 ? 2 : 1) : unit === "cm" ? 2 : 2;
|
||||
const formattedValue = new Intl.NumberFormat(locales[language], {
|
||||
minimumFractionDigits: unit === "in" ? 2 : 0,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(convertedValue);
|
||||
return `${formattedValue} ${getPrecipitationUnitLabel(unit)}`;
|
||||
}
|
||||
|
||||
export function convertPressure(value: number, unit: PressureUnit) {
|
||||
if (unit === "kpa") return value / 10;
|
||||
if (unit === "inhg") return value * 0.0295299830714;
|
||||
if (unit === "mmhg") return value * 0.750061683;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatPressure(
|
||||
value: number | null,
|
||||
language: Language = "pl",
|
||||
unit: PressureUnit = DEFAULT_PRESSURE_UNIT,
|
||||
) {
|
||||
if (value === null) return translate(language, "common.noData");
|
||||
const convertedValue = convertPressure(value, unit);
|
||||
const digits = unit === "inhg" ? 2 : unit === "kpa" ? 1 : unit === "mmhg" ? 0 : 1;
|
||||
const formattedValue = new Intl.NumberFormat(locales[language], {
|
||||
minimumFractionDigits: unit === "inhg" ? 2 : 0,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(convertedValue);
|
||||
return `${formattedValue} ${getPressureUnitLabel(unit)}`;
|
||||
}
|
||||
|
||||
export function convertDistance(value: number, unit: DistanceUnit) {
|
||||
if (unit === "mi") return value * 0.621371;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatDistance(
|
||||
value: number | null,
|
||||
language: Language = "pl",
|
||||
unit: DistanceUnit = DEFAULT_DISTANCE_UNIT,
|
||||
) {
|
||||
if (value === null) return translate(language, "common.noData");
|
||||
const convertedValue = convertDistance(value, unit);
|
||||
const formattedValue = new Intl.NumberFormat(locales[language], {
|
||||
maximumFractionDigits: convertedValue < 10 ? 1 : 0,
|
||||
}).format(convertedValue);
|
||||
return `${formattedValue} ${getDistanceUnitLabel(unit)}`;
|
||||
}
|
||||
|
||||
export function formatWaterLevel(value: number | null, language: Language = "pl") {
|
||||
|
||||
Reference in New Issue
Block a user