feat: add weather unit preferences
Some checks failed
CI / Lint, test, typecheck and build (push) Has been cancelled

This commit is contained in:
zv
2026-07-04 20:16:11 +02:00
parent ab6b7b414f
commit 91acdb39b8
27 changed files with 623 additions and 76 deletions

View File

@@ -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(

View File

@@ -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",

View File

@@ -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.");

View File

@@ -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,
});

View File

@@ -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: {

View File

@@ -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}:`,

View File

@@ -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") {