-
Wszystkie stacje
+
{t("station.all")}
toggleFavorite(station.id)}>
- {favorite ? "Usuń z ulubionych" : "Dodaj do ulubionych"}
+ {favorite ? t("favorites.remove") : t("favorites.add")}
- Stacja {station.name}
- Aktualne parametry
- Najnowszy pomiar udostępniony przez IMGW. Brakujące wartości są oznaczone bez uzupełniania ich danymi szacunkowymi.
+ {t("station.label", { name: station.name })}
+ {t("station.parameters")}
+ {t("station.parametersDescription")}
-
- Ostatni pomiar IMGW
- Czas poniżej pochodzi bezpośrednio z najnowszego odczytu udostępnionego przez IMGW.
+
+ {t("station.lastMeasurementImgw")}
+ {t("station.qualityDescription")}
-
Ostatni pomiar {formatDateTime(station.measuredAt)}
-
Źródło Publiczne API IMGW
+
{t("station.lastMeasurement")} {formatDateTime(station.measuredAt, language)}
+
{t("station.source")} {t("station.publicApi")}
diff --git a/components/weather/station-grid.tsx b/components/weather/station-grid.tsx
index 0f24a62..352eacb 100644
--- a/components/weather/station-grid.tsx
+++ b/components/weather/station-grid.tsx
@@ -1,9 +1,13 @@
+"use client";
+
import type { SynopStation } from "@/types/imgw";
import { StationCard } from "@/components/weather/station-card";
import { EmptyState } from "@/components/states/empty-state";
import { SearchX } from "lucide-react";
+import { useI18n } from "@/lib/i18n";
export function StationGrid({ stations }: { stations: SynopStation[] }) {
- if (!stations.length) return
;
+ const { t } = useI18n();
+ if (!stations.length) return
;
return
{stations.map((station, index) => )}
;
}
diff --git a/components/weather/station-search.tsx b/components/weather/station-search.tsx
index 0fc5507..2f139e3 100644
--- a/components/weather/station-search.tsx
+++ b/components/weather/station-search.tsx
@@ -1,33 +1,37 @@
+"use client";
+
import { Search, SlidersHorizontal } from "lucide-react";
import type { StationFilter, StationSort } from "@/components/weather/stations-explorer";
+import { useI18n } from "@/lib/i18n";
export function StationSearch({ query, onQueryChange, sort, onSortChange, filter, onFilterChange }: { query: string; onQueryChange: (value: string) => void; sort: StationSort; onSortChange: (value: StationSort) => void; filter: StationFilter; onFilterChange: (value: StationFilter) => void }) {
+ const { t } = useI18n();
return (
- Szukaj stacji synoptycznej
+ {t("stations.searchLabel")}
- onQueryChange(event.target.value)} placeholder="Szukaj stacji IMGW…" className="w-full rounded-2xl border border-white/40 bg-white/45 py-3 pl-10 pr-4 text-sm placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 dark:border-white/10 dark:bg-white/5" />
+ onQueryChange(event.target.value)} placeholder={t("stations.searchPlaceholder")} className="w-full rounded-2xl border border-white/40 bg-white/45 py-3 pl-10 pr-4 text-sm placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 dark:border-white/10 dark:bg-white/5" />
- Sortowanie stacji
+ {t("stations.sortLabel")}
onSortChange(event.target.value as StationSort)} className="w-full appearance-none rounded-2xl border border-white/40 bg-white/45 py-3 pl-4 pr-9 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 dark:border-white/10 dark:bg-slate-900/60">
- Alfabetycznie
- Temperatura: najwyższa
- Temperatura: najniższa
- Wilgotność: najwyższa
- Ciśnienie: najwyższe
+ {t("stations.sortAlphabetical")}
+ {t("stations.sortTemperatureDesc")}
+ {t("stations.sortTemperatureAsc")}
+ {t("stations.sortHumidityDesc")}
+ {t("stations.sortPressureDesc")}
- Filtr stacji
+ {t("stations.filterLabel")}
onFilterChange(event.target.value as StationFilter)} className="w-full rounded-2xl border border-white/40 bg-white/45 px-4 py-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 dark:border-white/10 dark:bg-slate-900/60">
- Wszystkie stacje
- Najcieplejsze
- Najzimniejsze
- Największy wiatr
- Największy opad
+ {t("stations.filterAll")}
+ {t("stations.filterWarmest")}
+ {t("stations.filterColdest")}
+ {t("stations.filterWindy")}
+ {t("stations.filterRainy")}
diff --git a/components/weather/stations-explorer.tsx b/components/weather/stations-explorer.tsx
index 473d4c9..3bf82b0 100644
--- a/components/weather/stations-explorer.tsx
+++ b/components/weather/stations-explorer.tsx
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
import type { SynopStation } from "@/types/imgw";
import { StationGrid } from "@/components/weather/station-grid";
import { StationSearch } from "@/components/weather/station-search";
+import { useI18n } from "@/lib/i18n";
export type StationSort = "alphabetical" | "temperature-desc" | "temperature-asc" | "humidity-desc" | "pressure-desc";
export type StationFilter = "all" | "warmest" | "coldest" | "windy" | "rainy";
@@ -15,28 +16,29 @@ function compareNumbers(a: number | null, b: number | null, direction: "asc" | "
}
export function StationsExplorer({ stations }: { stations: SynopStation[] }) {
+ const { locale, t } = useI18n();
const [query, setQuery] = useState("");
const [sort, setSort] = useState
("alphabetical");
const [filter, setFilter] = useState("all");
const visibleStations = useMemo(() => {
- const searched = stations.filter((station) => station.name.toLocaleLowerCase("pl").includes(query.trim().toLocaleLowerCase("pl")));
+ const searched = stations.filter((station) => station.name.toLocaleLowerCase(locale).includes(query.trim().toLocaleLowerCase(locale)));
const sorted = [...searched].sort((a, b) => {
if (sort === "temperature-desc") return compareNumbers(a.temperature, b.temperature, "desc");
if (sort === "temperature-asc") return compareNumbers(a.temperature, b.temperature, "asc");
if (sort === "humidity-desc") return compareNumbers(a.humidity, b.humidity, "desc");
if (sort === "pressure-desc") return compareNumbers(a.pressure, b.pressure, "desc");
- return a.name.localeCompare(b.name, "pl");
+ return a.name.localeCompare(b.name, locale);
});
if (filter === "all") return sorted;
const key = { warmest: "temperature", coldest: "temperature", windy: "windSpeed", rainy: "rainfall" }[filter] as keyof SynopStation;
return [...sorted].sort((a, b) => compareNumbers(a[key] as number | null, b[key] as number | null, filter === "coldest" ? "asc" : "desc")).slice(0, 12);
- }, [filter, query, sort, stations]);
+ }, [filter, locale, query, sort, stations]);
return (
-
Stacje synoptyczne
-
Pogoda w Polsce
+
{t("stations.section")}
+
{t("stations.title")}
diff --git a/components/weather/weather-hero.tsx b/components/weather/weather-hero.tsx
index cf44add..6be395b 100644
--- a/components/weather/weather-hero.tsx
+++ b/components/weather/weather-hero.tsx
@@ -16,15 +16,17 @@ import {
} from "@/lib/weather-utils";
import type { SynopStation } from "@/types/imgw";
import { WeatherIcon } from "@/components/weather/weather-icon";
+import { useI18n } from "@/lib/i18n";
export function WeatherHero({ station }: { station: SynopStation }) {
+ const { language, t } = useI18n();
const mood = getWeatherMoodFromData(station);
const feelsLike = calculateFeelsLike(station.temperature, station.humidity, station.windSpeed);
const metrics = [
- { icon: Droplets, label: "Wilgotność", value: formatHumidity(station.humidity) },
- { icon: Wind, label: "Wiatr", value: formatWind(station.windSpeed) },
- { icon: Umbrella, label: "Opad", value: formatRainfall(station.rainfall) },
- { icon: Gauge, label: "Ciśnienie", value: formatPressure(station.pressure) },
+ { icon: Droplets, label: t("weather.humidity"), value: formatHumidity(station.humidity, language) },
+ { icon: Wind, label: t("weather.wind"), value: formatWind(station.windSpeed, null, language) },
+ { icon: Umbrella, label: t("weather.rainfall"), value: formatRainfall(station.rainfall, language) },
+ { icon: Gauge, label: t("weather.pressure"), value: formatPressure(station.pressure, language) },
];
return (
@@ -43,10 +45,10 @@ export function WeatherHero({ station }: { station: SynopStation }) {
- {formatTemperature(station.temperature)}
+ {formatTemperature(station.temperature, language)}
-
{getWeatherDescription(station)}
-
Odczuwalna {formatTemperature(feelsLike)} · pomiar {formatDateTime(station.measuredAt)}
+
{getWeatherDescription(station, language)}
+
{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(station.measuredAt, language)}
@@ -61,7 +63,7 @@ export function WeatherHero({ station }: { station: SynopStation }) {
{station.windDirection !== null && (
- Kierunek wiatru: {station.windDirection}°
+ {t("weather.windDirection")}: {station.windDirection}°
)}
diff --git a/lib/constants.ts b/lib/constants.ts
index 75f0d5f..75e7c94 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -6,7 +6,7 @@ export const QUERY_STALE_TIME = 5 * 60 * 1000;
export const QUERY_GC_TIME = 60 * 60 * 1000;
export const NAV_ITEMS = [
- { href: "/", label: "Pogoda" },
- { href: "/warnings", label: "Ostrzeżenia" },
- { href: "/hydro", label: "Hydro" },
+ { href: "/", labelKey: "nav.weather" },
+ { href: "/warnings", labelKey: "nav.warnings" },
+ { href: "/hydro", labelKey: "nav.hydro" },
] as const;
diff --git a/lib/i18n.tsx b/lib/i18n.tsx
new file mode 100644
index 0000000..bdb627c
--- /dev/null
+++ b/lib/i18n.tsx
@@ -0,0 +1,290 @@
+"use client";
+
+import { createContext, useCallback, useContext, useEffect, useMemo, useState, type PropsWithChildren } from "react";
+
+export type Language = "pl" | "en";
+type TranslationParams = Record