From 6c2e731c60fd866dee9f1a5b316afc6c05bebce6 Mon Sep 17 00:00:00 2001 From: zv Date: Mon, 1 Jun 2026 18:54:08 +0200 Subject: [PATCH] feat: add Polish and English language switcher --- README.md | 2 + app/layout.tsx | 6 + app/offline/page.tsx | 10 +- app/warnings/page.tsx | 15 +- components/charts/snapshot-chart.tsx | 14 +- components/dashboard/dashboard-page.tsx | 4 +- components/hydro/hydro-page.tsx | 26 +- components/hydro/hydro-station-card.tsx | 12 +- components/layout/app-shell.tsx | 12 +- components/layout/providers.tsx | 11 +- components/states/error-state.tsx | 12 +- components/states/loading-skeleton.tsx | 6 +- components/ui/install-pwa-button.tsx | 4 +- components/ui/language-toggle.tsx | 23 ++ components/ui/theme-toggle.tsx | 4 +- components/warnings/warning-card.tsx | 16 +- components/warnings/warnings-page-content.tsx | 18 ++ components/warnings/warnings-panel.tsx | 6 +- .../weather/current-conditions-card.tsx | 18 +- components/weather/favorites-section.tsx | 8 +- components/weather/station-card.tsx | 10 +- components/weather/station-detail-page.tsx | 24 +- components/weather/station-grid.tsx | 6 +- components/weather/station-search.tsx | 32 +- components/weather/stations-explorer.tsx | 12 +- components/weather/weather-hero.tsx | 18 +- lib/constants.ts | 6 +- lib/i18n.tsx | 290 ++++++++++++++++++ lib/weather-utils.ts | 49 +-- 29 files changed, 531 insertions(+), 143 deletions(-) create mode 100644 components/ui/language-toggle.tsx create mode 100644 components/warnings/warnings-page-content.tsx create mode 100644 lib/i18n.tsx diff --git a/README.md b/README.md index c6ccc83..250b999 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ `wtr.` to nowoczesna pogodowa PWA dla Polski oparta o publiczne dane IMGW. Aplikacja prezentuje bieżące odczyty synoptyczne, stacje hydrologiczne oraz ostrzeżenia w spokojnym, mobilnym interfejsie z gradientami, kartami glassmorphism i subtelnymi animacjami. +Interfejs jest dostępny po polsku i angielsku. Wybrany język jest zapisywany lokalnie w przeglądarce. Oryginalne treści ostrzeżeń oraz nazwy stacji pochodzą bezpośrednio z API IMGW i nie są automatycznie tłumaczone. + ## Stack - Next.js z App Router i TypeScript diff --git a/app/layout.tsx b/app/layout.tsx index ccce5ee..fea1e93 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,6 +13,11 @@ const themeScript = ` document.documentElement.classList.toggle("dark", storedTheme ? storedTheme === "dark" : prefersDark); } catch {} `; +const languageScript = ` + try { + document.documentElement.lang = localStorage.getItem("wtr:language") === "en" ? "en" : "pl"; + } catch {} +`; export const metadata: Metadata = { title: { default: "wtr. | Pogoda z danych IMGW", template: "%s | wtr." }, @@ -41,6 +46,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac + {children} diff --git a/app/offline/page.tsx b/app/offline/page.tsx index b477092..04729e2 100644 --- a/app/offline/page.tsx +++ b/app/offline/page.tsx @@ -1,13 +1,17 @@ +"use client"; + import Link from "next/link"; import { WifiOff } from "lucide-react"; +import { useI18n } from "@/lib/i18n"; export default function OfflinePage() { + const { t } = useI18n(); return (
-

Brak połączenia

-

wtr. nie może teraz pobrać aktualnych danych IMGW. Ostatnio odwiedzone widoki mogą być dostępne z pamięci urządzenia.

- Wróć do aplikacji +

{t("offline.title")}

+

{t("offline.description")}

+ {t("offline.back")}
); } diff --git a/app/warnings/page.tsx b/app/warnings/page.tsx index 542c563..07c74e4 100644 --- a/app/warnings/page.tsx +++ b/app/warnings/page.tsx @@ -1,17 +1,8 @@ import type { Metadata } from "next"; -import { WarningsPanel } from "@/components/warnings/warnings-panel"; +import { WarningsPageContent } from "@/components/warnings/warnings-page-content"; -export const metadata: Metadata = { title: "Ostrzeżenia" }; +export const metadata: Metadata = { title: "Ostrzeżenia / Warnings" }; export default function WarningsPage() { - return ( -
-
-

Komunikaty IMGW

-

Ostrzeżenia

-

Aktualne ostrzeżenia meteorologiczne i hydrologiczne publikowane przez IMGW. Szczegóły obszaru i czasu obowiązywania pochodzą bezpośrednio z API.

-
- -
- ); + return ; } diff --git a/components/charts/snapshot-chart.tsx b/components/charts/snapshot-chart.tsx index 04a3037..3190782 100644 --- a/components/charts/snapshot-chart.tsx +++ b/components/charts/snapshot-chart.tsx @@ -3,19 +3,21 @@ import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import type { SynopStation } from "@/types/imgw"; import { Card } from "@/components/ui/card"; +import { useI18n } from "@/lib/i18n"; export function SnapshotChart({ station }: { station: SynopStation }) { + const { t } = useI18n(); const rows = [ - { name: "Wilgotność", value: station.humidity, unit: "%", max: 100, color: "#38bdf8" }, - { name: "Wiatr", value: station.windSpeed, unit: "m/s", max: 20, color: "#818cf8" }, - { name: "Opad", value: station.rainfall, unit: "mm", max: 30, color: "#22d3ee" }, + { name: t("weather.humidity"), value: station.humidity, unit: "%", max: 100, color: "#38bdf8" }, + { name: t("weather.wind"), value: station.windSpeed, unit: "m/s", max: 20, color: "#818cf8" }, + { name: t("weather.rainfall"), value: station.rainfall, unit: "mm", max: 30, color: "#22d3ee" }, ].filter((row) => row.value !== null).map((row) => ({ ...row, normalized: Math.min(100, ((row.value ?? 0) / row.max) * 100) })); return ( -

Snapshot pomiarowy

-

Aktualne proporcje parametrów

-

Wizualizacja bieżącego odczytu. API synoptyczne IMGW nie udostępnia historii ani prognozy godzinowej.

+

{t("snapshot.label")}

+

{t("snapshot.title")}

+

{t("snapshot.description")}

diff --git a/components/dashboard/dashboard-page.tsx b/components/dashboard/dashboard-page.tsx index 4764403..98a446f 100644 --- a/components/dashboard/dashboard-page.tsx +++ b/components/dashboard/dashboard-page.tsx @@ -8,12 +8,14 @@ import { FavoritesSection } from "@/components/weather/favorites-section"; import { StationsExplorer } from "@/components/weather/stations-explorer"; import { PageLoadingSkeleton } from "@/components/states/loading-skeleton"; import { ErrorState } from "@/components/states/error-state"; +import { useI18n } from "@/lib/i18n"; export function DashboardPage() { + const { t } = useI18n(); const { data: stations, isPending, isError, refetch } = useWeatherStations(); const selectedStationId = useWeatherStore((state) => state.selectedStationId); if (isPending) return ; - if (isError || !stations?.length) return refetch()} description="Nie udało się pobrać listy stacji synoptycznych IMGW." />; + if (isError || !stations?.length) return refetch()} description={t("dashboard.error")} />; const selectedStation = stations.find((station) => station.id === selectedStationId) ?? stations.find((station) => station.name === DEFAULT_STATION_NAME) ?? stations[0]; diff --git a/components/hydro/hydro-page.tsx b/components/hydro/hydro-page.tsx index 8b305c4..9b2afc6 100644 --- a/components/hydro/hydro-page.tsx +++ b/components/hydro/hydro-page.tsx @@ -8,37 +8,39 @@ import { Button } from "@/components/ui/button"; import { PageLoadingSkeleton } from "@/components/states/loading-skeleton"; import { ErrorState } from "@/components/states/error-state"; import { EmptyState } from "@/components/states/empty-state"; +import { useI18n } from "@/lib/i18n"; const PAGE_SIZE = 48; export function HydroPage() { + const { locale, t } = useI18n(); const { data: stations, isPending, isError, refetch } = useHydroStations(); const [query, setQuery] = useState(""); const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const filteredStations = useMemo(() => (stations ?? []).filter((station) => { - const haystack = `${station.name} ${station.river ?? ""} ${station.province ?? ""}`.toLocaleLowerCase("pl"); - return haystack.includes(query.trim().toLocaleLowerCase("pl")); - }), [query, stations]); + const haystack = `${station.name} ${station.river ?? ""} ${station.province ?? ""}`.toLocaleLowerCase(locale); + return haystack.includes(query.trim().toLocaleLowerCase(locale)); + }), [locale, query, stations]); if (isPending) return ; - if (isError) return refetch()} description="Nie udało się pobrać stacji hydrologicznych IMGW." />; + if (isError) return refetch()} description={t("hydro.error")} />; return (
-

Monitoring wód IMGW

-

Hydro

-

Najnowsze dostępne pomiary poziomu wody, temperatury i przepływu. Każdy parametr może mieć własny czas aktualizacji.

+

{t("hydro.section")}

+

{t("hydro.title")}

+

{t("hydro.description")}

-

Znaleziono {filteredStations.length} stacji. Wyświetlono {Math.min(visibleCount, filteredStations.length)}.

- {!filteredStations.length ? : ( +

{t("hydro.results", { total: filteredStations.length, visible: Math.min(visibleCount, filteredStations.length) })}

+ {!filteredStations.length ? : ( <>
{filteredStations.slice(0, visibleCount).map((station, index) => )}
- {visibleCount < filteredStations.length &&
} + {visibleCount < filteredStations.length &&
} )}
diff --git a/components/hydro/hydro-station-card.tsx b/components/hydro/hydro-station-card.tsx index ae6dc59..79ccf46 100644 --- a/components/hydro/hydro-station-card.tsx +++ b/components/hydro/hydro-station-card.tsx @@ -5,23 +5,25 @@ import { Activity, Droplets, MapPin, Thermometer } from "lucide-react"; import type { HydroStation } from "@/types/imgw"; import { formatDateTime, formatFlow, formatTemperature, formatWaterLevel } from "@/lib/weather-utils"; import { Card } from "@/components/ui/card"; +import { useI18n } from "@/lib/i18n"; export function HydroStationCard({ station, index = 0 }: { station: HydroStation; index?: number }) { + const { language, t } = useI18n(); return (

{station.name}

-

{station.river ?? "Rzeka: brak danych"}{station.province ? ` · ${station.province}` : ""}

+

{station.river ?? t("hydro.riverUnavailable")}{station.province ? ` · ${station.province}` : ""}

- - - + + +
-

Pomiar poziomu: {formatDateTime(station.waterLevelMeasuredAt)}

+

{t("hydro.levelMeasurement", { date: formatDateTime(station.waterLevelMeasuredAt, language) })}

); diff --git a/components/layout/app-shell.tsx b/components/layout/app-shell.tsx index b2a3c3c..68e7378 100644 --- a/components/layout/app-shell.tsx +++ b/components/layout/app-shell.tsx @@ -7,11 +7,14 @@ import { NAV_ITEMS } from "@/lib/constants"; import { cn } from "@/lib/utils"; import { InstallPWAButton } from "@/components/ui/install-pwa-button"; import { ThemeToggle } from "@/components/ui/theme-toggle"; +import { LanguageToggle } from "@/components/ui/language-toggle"; +import { useI18n } from "@/lib/i18n"; const icons = [CloudSun, TriangleAlert, Droplets]; export function AppShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); + const { t } = useI18n(); return (
@@ -19,31 +22,32 @@ export function AppShell({ children }: { children: React.ReactNode }) { wtr. -
{children}
-