"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type PropsWithChildren } from "react"; export type Language = "pl" | "en"; type TranslationParams = Record; const translations = { pl: { "nav.weather": "Pogoda", "nav.warnings": "Ostrzeżenia", "nav.hydro": "Hydro", "nav.main": "Główna nawigacja", "nav.mobile": "Mobilna nawigacja", "language.label": "Wybierz język", "language.polish": "Polski", "language.english": "English", "theme.change": "Zmień motyw", "theme.light": "Włącz jasny motyw", "theme.dark": "Włącz ciemny motyw", "pwa.install": "Zainstaluj", "common.noData": "Brak danych", "common.loading": "Ładowanie danych", "common.retry": "Spróbuj ponownie", "error.title": "Nie udało się pobrać danych", "error.description": "Sprawdź połączenie i spróbuj ponownie.", "dashboard.error": "Nie udało się pobrać listy stacji synoptycznych IMGW.", "location.label": "Twoja lokalizacja", "location.searchLabel": "Szukaj miejscowości w Polsce", "location.placeholder": "Wpisz miejscowość, np. Piaseczno…", "location.clear": "Wyczyść wyszukiwanie", "location.error": "Nie udało się wyszukać miejscowości. Spróbuj ponownie.", "location.preparing": "Przygotowuję listę najbliższych stacji IMGW…", "location.empty": "Nie znaleziono pasującej miejscowości w Polsce.", "location.nearest": "Najbliższa stacja IMGW", "location.currentSource": "{location}: odczyt ze stacji IMGW {station}, około {distance} km.", "location.heroSource": "stacja IMGW: {station} · około {distance} km", "location.attribution": "Wyszukiwanie miejscowości:", "location.gpsUse": "Użyj mojej lokalizacji", "location.gpsLocating": "Ustalam lokalizację…", "location.gpsPromptTitle": "Pokazać pogodę dla Twojej lokalizacji?", "location.gpsPromptDescription": "Po Twojej zgodzie wtr. użyje GPS, aby wybrać miejscowość, najbliższą stację IMGW i prognozę. Pozycja zostanie zaokrąglona do około 100 metrów.", "location.gpsAllow": "Użyj GPS", "location.gpsNotNow": "Nie teraz", "location.gpsSecureContext": "GPS wymaga HTTPS. Na iPhonie lokalny adres HTTP z adresem IP nie może wyświetlić systemowego pytania o położenie. Użyj wdrożonej wersji HTTPS.", "location.gpsUnavailable": "Ta przeglądarka nie udostępnia lokalizacji GPS.", "location.gpsDenied": "Dostęp do lokalizacji został odrzucony. Możesz zmienić uprawnienia witryny w ustawieniach przeglądarki.", "location.gpsTimeout": "Nie udało się ustalić lokalizacji w wymaganym czasie. Spróbuj ponownie.", "location.gpsPositionUnavailable": "Nie udało się ustalić lokalizacji GPS. Sprawdź ustawienia urządzenia i spróbuj ponownie.", "location.gpsStationsPending": "Lista stacji IMGW nie jest jeszcze gotowa. Spróbuj ponownie za chwilę.", "location.gpsFallbackName": "Bieżąca lokalizacja", "location.gpsSelected": "Wybrano lokalizację: {location}.", "location.gpsAttribution": "Nazwy miejsc dla GPS:", "featured.label": "Szybki wybór", "featured.title": "Wybrane stacje IMGW", "favorites.title": "Ulubione lokalizacje", "favorites.empty": "Dodaj stacje do ulubionych, aby mieć ich odczyty pod ręką.", "favorites.addStation": "Dodaj {name} do ulubionych", "favorites.removeStation": "Usuń {name} z ulubionych", "favorites.add": "Dodaj do ulubionych", "favorites.remove": "Usuń z ulubionych", "weather.humidity": "Wilgotność", "weather.wind": "Wiatr", "weather.rainfall": "Suma opadu", "weather.pressure": "Ciśnienie", "weather.feelsLike": "Odczuwalna", "weather.measurement": "pomiar", "weather.windDirection": "Kierunek wiatru", "weather.calm": "Spokojne warunki", "weather.humid": "Wilgotno", "weather.strongWind": "Silny wiatr", "weather.airTemperature": "Temperatura", "weather.windSpeed": "Prędkość wiatru", "weather.rainfallTotal": "Suma opadu", "weather.feelsLikeDetail": "Wartość obliczana, gdy warunki na to pozwalają", "weather.humidityDetail": "Wilgotność względna powietrza", "weather.pressureDetail": "Ciśnienie atmosferyczne", "weather.windSpeedDetail": "Bieżący odczyt IMGW", "weather.windDirectionDetail": "Kierunek napływu wiatru", "weather.rainfallDetail": "Akumulowana suma opadu z pomiaru IMGW. Nie oznacza, że pada w tej chwili.", "weather.temperatureDetail": "Temperatura powietrza", "forecast.label": "Prognoza modelowa", "forecast.title": "Najbliższe godziny i dni", "forecast.description": "Prognoza dla {location}. Bieżący pomiar powyżej pochodzi ze stacji IMGW, a poniższe wartości są prognozą modelową.", "forecast.hourly": "Najbliższe 24 godziny", "forecast.daily": "Prognoza 7-dniowa", "forecast.today": "Dzisiaj", "forecast.openDayDetails": "Otwórz szczegółową prognozę dla: {day}", "forecast.dayDetails": "Prognoza szczegółowa", "forecast.closeDetails": "Zamknij prognozę szczegółową", "forecast.hourlyForDay": "Przebieg godzinowy", "forecast.temperatureChart": "Temperatura w ciągu dnia", "forecast.temperatureChartDescription": "Temperatura powietrza i temperatura odczuwalna według modelu.", "forecast.rainfallChart": "Opad w ciągu dnia", "forecast.rainfallChartDescription": "Przewidywana suma opadu oraz prawdopodobieństwo opadu.", "forecast.temperature": "Temperatura", "forecast.apparentTemperature": "Odczuwalna", "forecast.precipitation": "Opad", "forecast.precipitationProbability": "Prawdopodobieństwo", "forecast.sunrise": "Wschód słońca", "forecast.sunset": "Zachód słońca", "forecast.maxWind": "Maks. wiatr", "forecast.nextHoursOverview": "Najbliższe 24 godziny w skrócie", "forecast.temperatureRange": "Zakres temperatur", "forecast.rainfallTotal": "Suma opadu", "forecast.maxProbability": "Maks. szansa opadu", "forecast.pastHour": "Miniona godzina", "forecast.source": "Źródło prognozy:", "forecast.error": "Nie udało się pobrać prognozy Open-Meteo.", "forecast.emptyTitle": "Brak prognozy", "forecast.emptyDescription": "Open-Meteo nie zwróciło teraz kompletnej prognozy dla tej lokalizacji.", "forecast.condition.clear": "Bezchmurnie", "forecast.condition.partlyCloudy": "Częściowe zachmurzenie", "forecast.condition.cloudy": "Pochmurno", "forecast.condition.fog": "Mgła", "forecast.condition.drizzle": "Mżawka", "forecast.condition.rain": "Opady deszczu", "forecast.condition.snow": "Opady śniegu", "forecast.condition.thunderstorm": "Burza", "forecast.condition.unknown": "Brak opisu", "stations.emptyTitle": "Brak pasujących stacji", "station.all": "Wszystkie stacje", "station.label": "Stacja {name}", "station.parameters": "Aktualne parametry", "station.parametersDescription": "Najnowszy pomiar udostępniony przez IMGW. Brakujące wartości są oznaczone bez uzupełniania ich danymi szacunkowymi.", "station.error": "Nie udało się pobrać danych wybranej stacji IMGW.", "station.quality": "Jakość danych", "station.lastMeasurementImgw": "Ostatni pomiar IMGW", "station.qualityDescription": "Czas poniżej pochodzi bezpośrednio z najnowszego odczytu udostępnionego przez IMGW.", "station.lastMeasurement": "Ostatni pomiar", "station.source": "Źródło", "station.publicApi": "Publiczne API IMGW", "snapshot.label": "Snapshot pomiarowy", "snapshot.title": "Aktualne proporcje parametrów", "snapshot.description": "Wizualizacja bieżącego odczytu. API synoptyczne IMGW nie udostępnia historii ani prognozy godzinowej.", "warnings.section": "Komunikaty IMGW", "warnings.title": "Ostrzeżenia", "warnings.description": "Aktualne ostrzeżenia meteorologiczne i hydrologiczne publikowane przez IMGW. Szczegóły obszaru i czasu obowiązywania pochodzą bezpośrednio z API.", "warnings.error": "Nie udało się pobrać ostrzeżeń meteorologicznych ani hydrologicznych.", "warnings.emptyTitle": "Brak aktywnych ostrzeżeń", "warnings.emptyDescription": "IMGW nie publikuje obecnie ostrzeżeń meteorologicznych ani hydrologicznych.", "warnings.drought": "Susza hydrologiczna", "warnings.levelUnknown": "Poziom nieokreślony", "warnings.level": "Stopień {level}", "warnings.hydro": "Hydrologiczne", "warnings.meteo": "Meteorologiczne", "warnings.untilCancelled": "do odwołania", "warnings.areaUnknown": "Obszar nieokreślony", "warnings.moreAreas": "i {count} więcej", "warnings.probability": "Prawdopodobieństwo: {value}%", "warnings.genericHydro": "Ostrzeżenie hydrologiczne", "warnings.genericMeteo": "Ostrzeżenie meteorologiczne", "hydro.section": "Monitoring wód IMGW", "hydro.title": "Hydro", "hydro.description": "Najnowsze dostępne pomiary poziomu wody, temperatury i przepływu. Każdy parametr może mieć własny czas aktualizacji.", "hydro.error": "Nie udało się pobrać stacji hydrologicznych IMGW.", "hydro.searchLabel": "Szukaj stacji hydrologicznej", "hydro.searchPlaceholder": "Szukaj stacji, rzeki lub województwa…", "hydro.results": "Znaleziono {total} stacji. Wyświetlono {visible}.", "hydro.emptyDescription": "Zmień wyszukiwaną nazwę stacji, rzeki lub województwa.", "hydro.more": "Pokaż więcej stacji", "hydro.riverUnavailable": "Rzeka: brak danych", "hydro.level": "Poziom", "hydro.water": "Woda", "hydro.flow": "Przepływ", "hydro.levelMeasurement": "Pomiar poziomu: {date}", "offline.title": "Brak połączenia", "offline.description": "wtr. nie może teraz pobrać aktualnych danych IMGW. Ostatnio odwiedzone widoki mogą być dostępne z pamięci urządzenia.", "offline.back": "Wróć do aplikacji", }, en: { "nav.weather": "Weather", "nav.warnings": "Warnings", "nav.hydro": "Hydro", "nav.main": "Main navigation", "nav.mobile": "Mobile navigation", "language.label": "Select language", "language.polish": "Polski", "language.english": "English", "theme.change": "Change theme", "theme.light": "Enable light theme", "theme.dark": "Enable dark theme", "pwa.install": "Install", "common.noData": "No data", "common.loading": "Loading data", "common.retry": "Try again", "error.title": "Unable to load data", "error.description": "Check your connection and try again.", "dashboard.error": "Unable to load the IMGW synoptic station list.", "location.label": "Your location", "location.searchLabel": "Search places in Poland", "location.placeholder": "Enter a place, e.g. Piaseczno…", "location.clear": "Clear search", "location.error": "Unable to search for places. Try again.", "location.preparing": "Preparing the nearest IMGW stations…", "location.empty": "No matching place was found in Poland.", "location.nearest": "Nearest IMGW station", "location.currentSource": "{location}: reading from IMGW station {station}, approximately {distance} km away.", "location.heroSource": "IMGW station: {station} · approximately {distance} km", "location.attribution": "Place search:", "location.gpsUse": "Use my location", "location.gpsLocating": "Finding your location…", "location.gpsPromptTitle": "Show weather for your location?", "location.gpsPromptDescription": "With your permission, wtr. will use GPS to select your place, the nearest IMGW station and the forecast. Your position will be rounded to approximately 100 metres.", "location.gpsAllow": "Use GPS", "location.gpsNotNow": "Not now", "location.gpsSecureContext": "GPS requires HTTPS. On iPhone, a local HTTP address using an IP cannot display the system location prompt. Use the deployed HTTPS version.", "location.gpsUnavailable": "This browser does not provide GPS location access.", "location.gpsDenied": "Location access was denied. You can change the website permission in your browser settings.", "location.gpsTimeout": "Your location could not be determined in time. Try again.", "location.gpsPositionUnavailable": "Your GPS location could not be determined. Check your device settings and try again.", "location.gpsStationsPending": "The IMGW station list is not ready yet. Try again in a moment.", "location.gpsFallbackName": "Current location", "location.gpsSelected": "Selected location: {location}.", "location.gpsAttribution": "GPS place names:", "featured.label": "Quick select", "featured.title": "Selected IMGW stations", "favorites.title": "Favourite locations", "favorites.empty": "Add stations to favourites to keep their readings close at hand.", "favorites.addStation": "Add {name} to favourites", "favorites.removeStation": "Remove {name} from favourites", "favorites.add": "Add to favourites", "favorites.remove": "Remove from favourites", "weather.humidity": "Humidity", "weather.wind": "Wind", "weather.rainfall": "Rainfall total", "weather.pressure": "Pressure", "weather.feelsLike": "Feels like", "weather.measurement": "measurement", "weather.windDirection": "Wind direction", "weather.calm": "Calm conditions", "weather.humid": "Humid", "weather.strongWind": "Strong wind", "weather.airTemperature": "Temperature", "weather.windSpeed": "Wind speed", "weather.rainfallTotal": "Rainfall total", "weather.feelsLikeDetail": "Calculated when the available conditions allow it", "weather.humidityDetail": "Relative air humidity", "weather.pressureDetail": "Atmospheric pressure", "weather.windSpeedDetail": "Current IMGW reading", "weather.windDirectionDetail": "Direction the wind is coming from", "weather.rainfallDetail": "Accumulated rainfall total from the IMGW reading. It does not mean that it is raining right now.", "weather.temperatureDetail": "Air temperature", "forecast.label": "Model forecast", "forecast.title": "Upcoming hours and days", "forecast.description": "Forecast for {location}. The current reading above comes from an IMGW station. The values below are a model forecast.", "forecast.hourly": "Next 24 hours", "forecast.daily": "7-day forecast", "forecast.today": "Today", "forecast.openDayDetails": "Open detailed forecast for: {day}", "forecast.dayDetails": "Detailed forecast", "forecast.closeDetails": "Close detailed forecast", "forecast.hourlyForDay": "Hourly conditions", "forecast.temperatureChart": "Temperature throughout the day", "forecast.temperatureChartDescription": "Air temperature and apparent temperature according to the model.", "forecast.rainfallChart": "Rainfall throughout the day", "forecast.rainfallChartDescription": "Forecast rainfall total and precipitation probability.", "forecast.temperature": "Temperature", "forecast.apparentTemperature": "Feels like", "forecast.precipitation": "Rainfall", "forecast.precipitationProbability": "Probability", "forecast.sunrise": "Sunrise", "forecast.sunset": "Sunset", "forecast.maxWind": "Max. wind", "forecast.nextHoursOverview": "Next 24 hours at a glance", "forecast.temperatureRange": "Temperature range", "forecast.rainfallTotal": "Rainfall total", "forecast.maxProbability": "Max. rain chance", "forecast.pastHour": "Past hour", "forecast.source": "Forecast source:", "forecast.error": "Unable to load the Open-Meteo forecast.", "forecast.emptyTitle": "Forecast unavailable", "forecast.emptyDescription": "Open-Meteo did not return a complete forecast for this location.", "forecast.condition.clear": "Clear sky", "forecast.condition.partlyCloudy": "Partly cloudy", "forecast.condition.cloudy": "Cloudy", "forecast.condition.fog": "Fog", "forecast.condition.drizzle": "Drizzle", "forecast.condition.rain": "Rain", "forecast.condition.snow": "Snow", "forecast.condition.thunderstorm": "Thunderstorm", "forecast.condition.unknown": "Description unavailable", "stations.emptyTitle": "No matching stations", "station.all": "All stations", "station.label": "Station {name}", "station.parameters": "Current parameters", "station.parametersDescription": "The latest measurement published by IMGW. Missing values are clearly marked and never replaced with estimates.", "station.error": "Unable to load data for the selected IMGW station.", "station.quality": "Data details", "station.lastMeasurementImgw": "Latest IMGW measurement", "station.qualityDescription": "The time below comes directly from the latest reading published by IMGW.", "station.lastMeasurement": "Latest measurement", "station.source": "Source", "station.publicApi": "Public IMGW API", "snapshot.label": "Measurement snapshot", "snapshot.title": "Current parameter proportions", "snapshot.description": "Visualisation of the current reading. The IMGW synoptic API does not provide historical data or an hourly forecast.", "warnings.section": "IMGW notices", "warnings.title": "Warnings", "warnings.description": "Current meteorological and hydrological warnings published by IMGW. Area and validity details come directly from the API.", "warnings.error": "Unable to load meteorological or hydrological warnings.", "warnings.emptyTitle": "No active warnings", "warnings.emptyDescription": "IMGW is not currently publishing any meteorological or hydrological warnings.", "warnings.drought": "Hydrological drought", "warnings.levelUnknown": "Level not specified", "warnings.level": "Level {level}", "warnings.hydro": "Hydrological", "warnings.meteo": "Meteorological", "warnings.untilCancelled": "until cancelled", "warnings.areaUnknown": "Area not specified", "warnings.moreAreas": "and {count} more", "warnings.probability": "Probability: {value}%", "warnings.genericHydro": "Hydrological warning", "warnings.genericMeteo": "Meteorological warning", "hydro.section": "IMGW water monitoring", "hydro.title": "Hydro", "hydro.description": "Latest available water level, temperature and flow readings. Each parameter may have a different update time.", "hydro.error": "Unable to load IMGW hydrological stations.", "hydro.searchLabel": "Search hydrological stations", "hydro.searchPlaceholder": "Search by station, river or province…", "hydro.results": "Found {total} stations. Showing {visible}.", "hydro.emptyDescription": "Adjust the station, river or province name in your search.", "hydro.more": "Show more stations", "hydro.riverUnavailable": "River: no data", "hydro.level": "Level", "hydro.water": "Water", "hydro.flow": "Flow", "hydro.levelMeasurement": "Level measurement: {date}", "offline.title": "No connection", "offline.description": "wtr. cannot fetch current IMGW data right now. Recently visited views may still be available from your device cache.", "offline.back": "Back to the app", }, } as const; export type TranslationKey = keyof typeof translations.pl; function interpolate(value: string, params?: TranslationParams) { if (!params) return value; return value.replace(/\{(\w+)\}/g, (_, key: string) => String(params[key] ?? `{${key}}`)); } export function translate(language: Language, key: TranslationKey, params?: TranslationParams) { return interpolate(translations[language][key] ?? translations.en[key], params); } interface I18nContextValue { language: Language; locale: string; setLanguage: (language: Language) => void; t: (key: TranslationKey, params?: TranslationParams) => string; } const I18nContext = createContext(null); export function I18nProvider({ children }: PropsWithChildren) { const [language, setLanguageState] = useState("pl"); useEffect(() => { const stored = window.localStorage.getItem("wtr:language"); const nextLanguage: Language = stored === "en" ? "en" : "pl"; const animationFrame = window.requestAnimationFrame(() => setLanguageState(nextLanguage)); document.documentElement.lang = nextLanguage; return () => window.cancelAnimationFrame(animationFrame); }, []); const setLanguage = useCallback((nextLanguage: Language) => { window.localStorage.setItem("wtr:language", nextLanguage); document.documentElement.lang = nextLanguage; setLanguageState(nextLanguage); }, []); const value = useMemo(() => ({ language, locale: language === "pl" ? "pl-PL" : "en-GB", setLanguage, t: (key, params) => translate(language, key, params), }), [language, setLanguage]); return {children}; } export function useI18n() { const context = useContext(I18nContext); if (!context) throw new Error("useI18n must be used within I18nProvider."); return context; }