feat: add temperature unit preference
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import type { Language, TranslationKey } from "@/lib/i18n";
|
||||
import { translate } from "@/lib/i18n";
|
||||
import { DEFAULT_WIND_SPEED_UNIT, formatWindSpeed } from "@/lib/weather-utils";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT, formatTemperature, formatWindSpeed } from "@/lib/weather-utils";
|
||||
import type { HourlyForecast } from "@/types/forecast";
|
||||
import type { WindSpeedUnit } from "@/types/units";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
export function getForecastConditionKey(code: number | null): TranslationKey {
|
||||
if (code === 0) return "forecast.condition.clear";
|
||||
@@ -20,9 +20,9 @@ export function getForecastCondition(code: number | null, language: Language) {
|
||||
return translate(language, getForecastConditionKey(code));
|
||||
}
|
||||
|
||||
export function formatForecastTemperature(value: number | null, language: Language) {
|
||||
export function formatForecastTemperature(value: number | null, language: Language, unit: TemperatureUnit = DEFAULT_TEMPERATURE_UNIT) {
|
||||
if (value === null) return "—";
|
||||
return `${new Intl.NumberFormat(language === "pl" ? "pl-PL" : "en-GB", { maximumFractionDigits: 0 }).format(value)}°`;
|
||||
return formatTemperature(value, language, unit);
|
||||
}
|
||||
|
||||
export function formatForecastRainfall(value: number | null, language: Language) {
|
||||
|
||||
10
lib/i18n.tsx
10
lib/i18n.tsx
@@ -26,6 +26,11 @@ const translations = {
|
||||
"settings.language.description": "Zmieniaj język interfejsu. Nazwy stacji i treści IMGW pozostają bez automatycznego tłumaczenia.",
|
||||
"settings.theme.title": "Motyw",
|
||||
"settings.theme.description": "Przełącz jasny lub ciemny wygląd aplikacji na tym urządzeniu.",
|
||||
"settings.units.temperature.title": "Jednostka temperatury",
|
||||
"settings.units.temperature.description": "Stosowana w prognozie, briefach, powiadomieniach i bieżących pomiarach na tym urządzeniu.",
|
||||
"settings.units.temperature.label": "Wybierz jednostkę temperatury",
|
||||
"settings.units.temperature.c": "°C",
|
||||
"settings.units.temperature.f": "°F",
|
||||
"settings.units.wind.title": "Jednostka wiatru",
|
||||
"settings.units.wind.description": "Stosowana w prognozie, briefach, powiadomieniach i bieżących pomiarach na tym urządzeniu.",
|
||||
"settings.units.wind.label": "Wybierz jednostkę wiatru",
|
||||
@@ -268,6 +273,11 @@ const translations = {
|
||||
"settings.language.description": "Change the interface language. Station names and IMGW content are not translated automatically.",
|
||||
"settings.theme.title": "Theme",
|
||||
"settings.theme.description": "Switch the light or dark appearance on this device.",
|
||||
"settings.units.temperature.title": "Temperature unit",
|
||||
"settings.units.temperature.description": "Used in forecasts, briefs, notifications and current readings on this device.",
|
||||
"settings.units.temperature.label": "Choose temperature unit",
|
||||
"settings.units.temperature.c": "°C",
|
||||
"settings.units.temperature.f": "°F",
|
||||
"settings.units.wind.title": "Wind unit",
|
||||
"settings.units.wind.description": "Used in forecasts, briefs, notifications and current readings on this device.",
|
||||
"settings.units.wind.label": "Choose wind unit",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Language } from "@/lib/i18n";
|
||||
import type { Province } from "@/types/province";
|
||||
import type { WindSpeedUnit } from "@/types/units";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
interface VapidKeyResponse {
|
||||
configured: boolean;
|
||||
@@ -20,6 +20,7 @@ interface SavePushSubscriptionOptions {
|
||||
longitude?: number | null;
|
||||
locationName?: string | null;
|
||||
countyTeryt?: string | null;
|
||||
temperatureUnit?: TemperatureUnit;
|
||||
windSpeedUnit?: WindSpeedUnit;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export async function savePushSubscription(subscription: PushSubscription, provi
|
||||
longitude: options.longitude ?? null,
|
||||
locationName: options.locationName ?? null,
|
||||
countyTeryt: options.countyTeryt ?? null,
|
||||
temperatureUnit: options.temperatureUnit,
|
||||
windSpeedUnit: options.windSpeedUnit,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { SelectedLocation } from "@/types/location";
|
||||
import type { Province } from "@/types/province";
|
||||
import type { WindSpeedUnit } from "@/types/units";
|
||||
import { DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils";
|
||||
|
||||
type NotificationProvinceMode = "selected" | "manual";
|
||||
|
||||
@@ -18,6 +18,7 @@ interface WeatherStore {
|
||||
tomorrowBriefNotificationsEnabled: boolean;
|
||||
warningNotificationProvinceMode: NotificationProvinceMode;
|
||||
warningNotificationProvince: Province | null;
|
||||
temperatureUnit: TemperatureUnit;
|
||||
windSpeedUnit: WindSpeedUnit;
|
||||
toggleFavorite: (id: string) => void;
|
||||
selectStation: (id: string) => void;
|
||||
@@ -27,6 +28,7 @@ interface WeatherStore {
|
||||
setTomorrowBriefNotificationsEnabled: (enabled: boolean) => void;
|
||||
setWarningNotificationProvinceMode: (mode: NotificationProvinceMode) => void;
|
||||
setWarningNotificationProvince: (province: Province | null) => void;
|
||||
setTemperatureUnit: (unit: TemperatureUnit) => void;
|
||||
setWindSpeedUnit: (unit: WindSpeedUnit) => void;
|
||||
}
|
||||
|
||||
@@ -41,6 +43,7 @@ export const useWeatherStore = create<WeatherStore>()(
|
||||
tomorrowBriefNotificationsEnabled: false,
|
||||
warningNotificationProvinceMode: "selected",
|
||||
warningNotificationProvince: null,
|
||||
temperatureUnit: DEFAULT_TEMPERATURE_UNIT,
|
||||
windSpeedUnit: DEFAULT_WIND_SPEED_UNIT,
|
||||
toggleFavorite: (id) =>
|
||||
set((state) => ({
|
||||
@@ -55,6 +58,7 @@ export const useWeatherStore = create<WeatherStore>()(
|
||||
setTomorrowBriefNotificationsEnabled: (enabled) => set({ tomorrowBriefNotificationsEnabled: enabled }),
|
||||
setWarningNotificationProvinceMode: (mode) => set({ warningNotificationProvinceMode: mode }),
|
||||
setWarningNotificationProvince: (province) => set({ warningNotificationProvince: province }),
|
||||
setTemperatureUnit: (unit) => set({ temperatureUnit: unit }),
|
||||
setWindSpeedUnit: (unit) => set({ windSpeedUnit: unit }),
|
||||
}),
|
||||
{ name: "wtr:preferences" },
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { HourlyForecast, WeatherForecast } from "@/types/forecast";
|
||||
import type { WeatherWarning } from "@/types/imgw";
|
||||
import type { Province } from "@/types/province";
|
||||
import { warningMatchesCounty } from "@/lib/warning-regions";
|
||||
import { DEFAULT_WIND_SPEED_UNIT, formatWindSpeed } from "@/lib/weather-utils";
|
||||
import type { WindSpeedUnit } from "@/types/units";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT, formatTemperature as formatDisplayTemperature, formatWindSpeed } from "@/lib/weather-utils";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
export type WeatherBriefSeverity = "normal" | "attention" | "warning";
|
||||
|
||||
@@ -26,6 +26,7 @@ interface BuildWeatherBriefInput {
|
||||
countyTeryt?: string | null;
|
||||
locationName: string;
|
||||
language: Language;
|
||||
temperatureUnit?: TemperatureUnit;
|
||||
windSpeedUnit?: WindSpeedUnit;
|
||||
now?: Date;
|
||||
}
|
||||
@@ -36,10 +37,6 @@ function formatNumber(value: number, language: Language, digits = 0) {
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function formatTemperature(value: number, language: Language) {
|
||||
return `${formatNumber(value, language)}°C`;
|
||||
}
|
||||
|
||||
function formatRainfall(value: number, language: Language) {
|
||||
return `${formatNumber(value, language, 1)} mm`;
|
||||
}
|
||||
@@ -202,7 +199,7 @@ function getTomorrowCondition(hours: HourlyForecast[], rainfallTotal: number | n
|
||||
return language === "pl" ? "bez istotnych opadów" : "no significant precipitation";
|
||||
}
|
||||
|
||||
export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, locationName, language, windSpeedUnit = DEFAULT_WIND_SPEED_UNIT, now = new Date() }: BuildWeatherBriefInput): WeatherBrief | null {
|
||||
export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, locationName, language, temperatureUnit = DEFAULT_TEMPERATURE_UNIT, windSpeedUnit = DEFAULT_WIND_SPEED_UNIT, now = new Date() }: BuildWeatherBriefInput): WeatherBrief | null {
|
||||
const hours = getUpcomingHours(forecast.hourly, now, 24);
|
||||
if (!hours.length) return null;
|
||||
|
||||
@@ -218,7 +215,7 @@ export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, l
|
||||
const rainPeakHour = getPeakHour(hours, (hour) => hour.precipitationProbability);
|
||||
const conditionPeakHour = hours.find((hour) => hour.weatherCode !== null) ?? null;
|
||||
const temperatureRange = minimumTemperature !== null && maximumTemperature !== null
|
||||
? `${formatTemperature(minimumTemperature, language)}-${formatTemperature(maximumTemperature, language)}`
|
||||
? `${formatDisplayTemperature(minimumTemperature, language, temperatureUnit)}-${formatDisplayTemperature(maximumTemperature, language, temperatureUnit)}`
|
||||
: null;
|
||||
const hasRainSignal = (maximumProbability ?? 0) >= 35 || (rainfallTotal ?? 0) >= 0.5;
|
||||
const hasWindSignal = (maximumWind ?? 0) >= 8;
|
||||
@@ -295,7 +292,7 @@ export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, l
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTomorrowWeatherBrief({ forecast, warnings, province, countyTeryt, locationName, language, windSpeedUnit = DEFAULT_WIND_SPEED_UNIT, now = new Date() }: BuildWeatherBriefInput): WeatherBrief | null {
|
||||
export function buildTomorrowWeatherBrief({ forecast, warnings, province, countyTeryt, locationName, language, temperatureUnit = DEFAULT_TEMPERATURE_UNIT, windSpeedUnit = DEFAULT_WIND_SPEED_UNIT, now = new Date() }: BuildWeatherBriefInput): WeatherBrief | null {
|
||||
const targetDateKey = getWarsawDateKey(now, 1);
|
||||
const hours = getForecastHoursForDate(forecast.hourly, targetDateKey);
|
||||
if (!hours.length) return null;
|
||||
@@ -318,7 +315,7 @@ export function buildTomorrowWeatherBrief({ forecast, warnings, province, county
|
||||
const hasWindSignal = (maximumWind ?? 0) >= 8;
|
||||
const condition = getTomorrowCondition(hours, rainfallTotal, maximumProbability, language);
|
||||
const temperatureRange = minimumTemperature !== null && maximumTemperature !== null
|
||||
? `${formatTemperature(minimumTemperature, language)} / ${formatTemperature(maximumTemperature, language)}`
|
||||
? `${formatDisplayTemperature(minimumTemperature, language, temperatureUnit)} / ${formatDisplayTemperature(maximumTemperature, language, temperatureUnit)}`
|
||||
: null;
|
||||
const severity: WeatherBriefSeverity = topWarning || hasThunderstorm ? "warning" : hasRainSignal || hasWindSignal ? "attention" : "normal";
|
||||
const provinceLabel = province ? formatProvinceName(province, language) : null;
|
||||
|
||||
@@ -11,12 +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 { WindSpeedUnit } from "@/types/units";
|
||||
import type { 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 = "ms";
|
||||
export const WIND_SPEED_UNITS: WindSpeedUnit[] = ["ms", "kmh", "mph"];
|
||||
|
||||
const temperatureUnitLabels: Record<TemperatureUnit, string> = {
|
||||
c: "°C",
|
||||
f: "°F",
|
||||
};
|
||||
|
||||
const windSpeedUnitLabels: Record<WindSpeedUnit, string> = {
|
||||
ms: "m/s",
|
||||
kmh: "km/h",
|
||||
@@ -105,8 +112,28 @@ export function normalizeWarning(raw: RawWarning, kind: WarningKind, index: numb
|
||||
};
|
||||
}
|
||||
|
||||
export function formatTemperature(value: number | null, language: Language = "pl") {
|
||||
return value === null ? translate(language, "common.noData") : `${Math.round(value)}°`;
|
||||
export function isTemperatureUnit(value: unknown): value is TemperatureUnit {
|
||||
return typeof value === "string" && TEMPERATURE_UNITS.includes(value as TemperatureUnit);
|
||||
}
|
||||
|
||||
export function getTemperatureUnitLabel(unit: TemperatureUnit) {
|
||||
return temperatureUnitLabels[unit];
|
||||
}
|
||||
|
||||
export function convertTemperature(value: number, unit: TemperatureUnit) {
|
||||
if (unit === "f") return (value * 9) / 5 + 32;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatTemperatureValue(value: number, language: Language = "pl", unit: TemperatureUnit = DEFAULT_TEMPERATURE_UNIT) {
|
||||
const formattedValue = new Intl.NumberFormat(locales[language], {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
return `${formattedValue}${getTemperatureUnitLabel(unit)}`;
|
||||
}
|
||||
|
||||
export function formatTemperature(value: number | null, language: Language = "pl", unit: TemperatureUnit = DEFAULT_TEMPERATURE_UNIT) {
|
||||
return value === null ? translate(language, "common.noData") : formatTemperatureValue(convertTemperature(value, unit), language, unit);
|
||||
}
|
||||
|
||||
export function formatPressure(value: number | null, language: Language = "pl") {
|
||||
|
||||
Reference in New Issue
Block a user