diff --git a/client/src/components/Map/LocationButton.test.tsx b/client/src/components/Map/LocationButton.test.tsx new file mode 100644 index 000000000..e62ee13af --- /dev/null +++ b/client/src/components/Map/LocationButton.test.tsx @@ -0,0 +1,69 @@ +// FE-MAP-LOCBTN-001 to FE-MAP-LOCBTN-004 +import { render } from '@testing-library/react'; +import LocationButton from './LocationButton'; +import type { GeoWatchErrorCode } from '../../hooks/useGeolocation'; + +// useToast talks to the mounted ToastContainer through window.__addToast, +// so a spy there observes exactly what the user would get shown. Outside a +// TranslationProvider t(key) echoes the key, so assertions target the keys. +const addToast = vi.fn(); + +beforeEach(() => { + addToast.mockClear(); + window.__addToast = addToast; +}); + +afterEach(() => { + delete window.__addToast; +}); + +const noop = () => {}; + +function button(errorCode: GeoWatchErrorCode | null, error: string | null = errorCode && 'raw webkit text') { + return ; +} + +describe('LocationButton', () => { + it('FE-MAP-LOCBTN-001: shows the mode title and stays quiet without an error', () => { + const { getByRole } = render(button(null)); + + expect(getByRole('button').getAttribute('title')).toBe('Show my location'); + expect(addToast).not.toHaveBeenCalled(); + }); + + it('FE-MAP-LOCBTN-002: titles with the localized error text, not the raw browser string', () => { + const { getByRole } = render(button('timeout')); + + expect(getByRole('button').getAttribute('title')).toBe('map.location.timeout'); + expect(getByRole('button').getAttribute('aria-label')).toBe('map.location.timeout'); + }); + + it('FE-MAP-LOCBTN-003: fires exactly one error toast when a code appears', () => { + const { rerender } = render(button(null)); + expect(addToast).not.toHaveBeenCalled(); + + rerender(button('permission-denied')); + expect(addToast).toHaveBeenCalledTimes(1); + expect(addToast).toHaveBeenCalledWith('map.location.denied', 'error', 6000); + + // The same error sticking around must not re-announce itself. + rerender(button('permission-denied')); + expect(addToast).toHaveBeenCalledTimes(1); + }); + + it('FE-MAP-LOCBTN-004: only the transition from no error to an error toasts', () => { + const { rerender } = render(button(null)); + rerender(button('permission-denied')); + expect(addToast).toHaveBeenCalledTimes(1); + + // Code to code without recovering in between stays silent. + rerender(button('timeout')); + expect(addToast).toHaveBeenCalledTimes(1); + + // After a recovery a fresh failure announces itself again. + rerender(button(null)); + rerender(button('timeout')); + expect(addToast).toHaveBeenCalledTimes(2); + expect(addToast).toHaveBeenLastCalledWith('map.location.timeout', 'error', 6000); + }); +}); diff --git a/client/src/components/Map/LocationButton.tsx b/client/src/components/Map/LocationButton.tsx index b2d446789..3e1abd83c 100644 --- a/client/src/components/Map/LocationButton.tsx +++ b/client/src/components/Map/LocationButton.tsx @@ -1,24 +1,55 @@ +import { useEffect, useRef } from 'react' import { Navigation, LocateFixed, Locate } from 'lucide-react' -import type { TrackingMode } from '../../hooks/useGeolocation' +import type { TrackingMode, GeoWatchErrorCode } from '../../hooks/useGeolocation' +import { useTranslation } from '../../i18n' +import { useToast } from '../shared/Toast' interface Props { mode: TrackingMode error: string | null + errorCode: GeoWatchErrorCode | null onClick: () => void // Offset from the bottom edge — callers push this up above the mobile // bottom nav. Defaults to 20px for desktop. bottomOffset?: number } +// The raw error string from the browser is unlocalized WebKit English, so the +// user-facing message is looked up from the typed code instead. 'unsupported' +// shares the generic text: a browser without geolocation cannot locate you. +const ERROR_KEYS: Record = { + 'permission-denied': 'map.location.denied', + 'unavailable': 'map.location.unavailable', + 'timeout': 'map.location.timeout', + 'unsupported': 'map.location.unavailable', +} + // Three-state FAB. Matches the Apple/Google Maps pattern: // off → outline locate icon // show → filled locate (blue dot is visible on the map) // follow → filled navigation arrow (map follows + rotates with heading) -export default function LocationButton({ mode, error, onClick, bottomOffset = 20 }: Props) { +export default function LocationButton({ mode, error, errorCode, onClick, bottomOffset = 20 }: Props) { + const { t } = useTranslation() + const toast = useToast() + // A title tooltip is invisible on touch, which is exactly where geolocation + // errors happen (iOS PWA, see discussion #2095), so surface new errors as a + // toast. The ref guards against re-fires: only the transition from no error + // to an error announces itself. + const prevCodeRef = useRef(null) + useEffect(() => { + const prev = prevCodeRef.current + prevCodeRef.current = errorCode + if (errorCode && prev === null) { + // The denied message is long, so give the toast extra time to be read. + toast.error(t(ERROR_KEYS[errorCode]), 6000) + } + }, [errorCode, t, toast]) + const Icon = mode === 'follow' ? Navigation : mode === 'show' ? LocateFixed : Locate const isActive = mode !== 'off' - const title = error - ? error + const errorText = errorCode ? t(ERROR_KEYS[errorCode]) : error + const title = errorText + ? errorText : mode === 'off' ? 'Show my location' : mode === 'show' diff --git a/client/src/components/Map/MapView.test.tsx b/client/src/components/Map/MapView.test.tsx index 00911a2f1..9c1757b12 100644 --- a/client/src/components/Map/MapView.test.tsx +++ b/client/src/components/Map/MapView.test.tsx @@ -36,6 +36,7 @@ const geoMock = vi.hoisted(() => ({ position: null as { lat: number; lng: number; accuracy: number; heading: number | null } | null, mode: 'off' as 'off' | 'show' | 'follow', error: null as string | null, + errorCode: null as string | null, cycleMode: vi.fn(), setMode: vi.fn(), })) diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx index c119af4a7..0635c50c1 100644 --- a/client/src/components/Map/MapView.tsx +++ b/client/src/components/Map/MapView.tsx @@ -802,7 +802,7 @@ export const MapView = memo(function MapView({ const TooltipOverlay = !hoverDisabled && hoveredPlace && tooltipPos && !isTouchDevice const CatIcon = TooltipOverlay ? getCategoryIcon(hoveredPlace.category_icon) : null - const { position: userPosition, mode: trackingMode, error: trackingError, cycleMode: cycleTrackingMode } = useGeolocation() + const { position: userPosition, mode: trackingMode, error: trackingError, errorCode: trackingErrorCode, cycleMode: cycleTrackingMode } = useGeolocation() // Desktop browsers only get IP-based geolocation (city-level accuracy), // so the button would be misleading. Mobile, where real GPS lives, keeps it. const isMobile = typeof window !== 'undefined' && window.innerWidth < 768 @@ -941,6 +941,7 @@ export const MapView = memo(function MapView({ {isMobile && } diff --git a/client/src/components/Map/MapViewGL.test.tsx b/client/src/components/Map/MapViewGL.test.tsx index d62a0e270..81bf73d64 100644 --- a/client/src/components/Map/MapViewGL.test.tsx +++ b/client/src/components/Map/MapViewGL.test.tsx @@ -195,6 +195,7 @@ const geoStub = vi.hoisted(() => ({ position: null as GeoPosition | null, mode: 'off' as TrackingMode, error: null as string | null, + errorCode: null as string | null, cycleMode: vi.fn(async () => {}), setMode: vi.fn((_m: TrackingMode | ((prev: TrackingMode) => TrackingMode)) => {}), })) diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index f82c03351..6134c63ae 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -509,7 +509,7 @@ export function MapViewGL({ onViewportChangeRef.current = onViewportChange const onMapReadyRef = useRef(onMapReady) onMapReadyRef.current = onMapReady - const { position: userPosition, mode: trackingMode, error: trackingError, cycleMode: cycleTrackingMode, setMode: setTrackingMode } = useGeolocation() + const { position: userPosition, mode: trackingMode, error: trackingError, errorCode: trackingErrorCode, cycleMode: cycleTrackingMode, setMode: setTrackingMode } = useGeolocation() const onClickRefs = useRef({ marker: onMarkerClick, map: onMapClick, context: onMapContextMenu }) onClickRefs.current.marker = onMarkerClick onClickRefs.current.map = onMapClick @@ -1548,6 +1548,7 @@ export function MapViewGL({ diff --git a/client/src/hooks/useGeolocation.test.ts b/client/src/hooks/useGeolocation.test.ts index ba536744c..7a241130f 100644 --- a/client/src/hooks/useGeolocation.test.ts +++ b/client/src/hooks/useGeolocation.test.ts @@ -1,4 +1,4 @@ -// FE-HOOK-GEO-001 to FE-HOOK-GEO-025 +// FE-HOOK-GEO-001 to FE-HOOK-GEO-029 import { StrictMode } from 'react'; import { act, renderHook } from '@testing-library/react'; import { GeoOnceError, getCurrentPositionOnce, useGeolocation } from './useGeolocation'; @@ -54,6 +54,7 @@ describe('useGeolocation', () => { expect(result.current.mode).toBe('off'); expect(result.current.position).toBeNull(); expect(result.current.error).toBeNull(); + expect(result.current.errorCode).toBeNull(); expect(watchPosition).not.toHaveBeenCalled(); }); @@ -305,6 +306,54 @@ describe('useGeolocation', () => { expect(result.current.mode).toBe('show'); }); + it.each([ + [1, 'permission-denied'], + [2, 'unavailable'], + [3, 'timeout'], + ])('FE-HOOK-GEO-026: maps watch error code %i to errorCode "%s"', async (code, expected) => { + const { result } = renderHook(() => useGeolocation()); + await act(() => result.current.cycleMode()); + + act(() => errorCb!(geoError(code))); + + expect(result.current.errorCode).toBe(expected); + expect(result.current.error).toBe('Location unavailable'); + }); + + it('FE-HOOK-GEO-027: a denial still stops the watch and keeps the code for the UI', async () => { + const { result } = renderHook(() => useGeolocation()); + await act(() => result.current.cycleMode()); + + act(() => errorCb!(geoError(1, 'User denied Geolocation'))); + + expect(result.current.mode).toBe('off'); + expect(clearWatch).toHaveBeenCalledWith(7); + expect(result.current.errorCode).toBe('permission-denied'); + }); + + it('FE-HOOK-GEO-028: a missing geolocation API reports the unsupported code', async () => { + delete (navigator as { geolocation?: Geolocation }).geolocation; + const { result } = renderHook(() => useGeolocation()); + + await act(() => result.current.cycleMode()); + + expect(result.current.errorCode).toBe('unsupported'); + }); + + it('FE-HOOK-GEO-029: a successful fix clears both error and errorCode', async () => { + const { result } = renderHook(() => useGeolocation()); + await act(() => result.current.cycleMode()); + + act(() => errorCb!(geoError(3, 'Timeout expired'))); + expect(result.current.error).toBe('Timeout expired'); + expect(result.current.errorCode).toBe('timeout'); + + act(() => successCb!(fix())); + expect(result.current.error).toBeNull(); + expect(result.current.errorCode).toBeNull(); + expect(result.current.position).not.toBeNull(); + }); + it('FE-HOOK-GEO-019: stops the watch when the component unmounts', async () => { const { result, unmount } = renderHook(() => useGeolocation()); await act(() => result.current.cycleMode()); diff --git a/client/src/hooks/useGeolocation.ts b/client/src/hooks/useGeolocation.ts index 54ee184ea..ee4bb06e7 100644 --- a/client/src/hooks/useGeolocation.ts +++ b/client/src/hooks/useGeolocation.ts @@ -17,10 +17,16 @@ export interface GeoPosition { export type TrackingMode = 'off' | 'show' | 'follow' +// Typed counterpart to the raw error string. The string is whatever the +// browser produced (raw WebKit English on iOS), so the UI keys off this +// code to show a localized, actionable message instead. +export type GeoWatchErrorCode = 'permission-denied' | 'unavailable' | 'timeout' | 'unsupported' + export interface UseGeolocationReturn { position: GeoPosition | null mode: TrackingMode error: string | null + errorCode: GeoWatchErrorCode | null /** Toggle through off → show → follow → off. Also triggers iOS orientation permission on first call. */ cycleMode: () => Promise /** Force-set mode. Accepts a function for derived updates like `prev => prev === 'follow' ? 'show' : prev`. */ @@ -101,6 +107,7 @@ export function useGeolocation(): UseGeolocationReturn { const modeRef = useRef(mode) modeRef.current = mode const [error, setError] = useState(null) + const [errorCode, setErrorCode] = useState(null) const watchIdRef = useRef(null) // True between the start of startWatch and the watchPosition call it awaits. const startingRef = useRef(false) @@ -129,6 +136,7 @@ export function useGeolocation(): UseGeolocationReturn { const startWatch = useCallback(async () => { if (!('geolocation' in navigator)) { setError('Geolocation is not supported in this browser') + setErrorCode('unsupported') return false } // Already watching, or still waiting on the iOS prompt below: a second @@ -138,6 +146,7 @@ export function useGeolocation(): UseGeolocationReturn { startingRef.current = true const run = ++startRunRef.current setError(null) + setErrorCode(null) // iOS: ask for orientation permission up front; on Android and desktop // no prompt is needed and the method is undefined. @@ -184,6 +193,10 @@ export function useGeolocation(): UseGeolocationReturn { startingRef.current = false watchIdRef.current = navigator.geolocation.watchPosition( (pos) => { + // A fix arriving means any earlier failure (timeout, no signal) + // has recovered, so clear the error state alongside it. + setError(null) + setErrorCode(null) setPosition({ lat: pos.coords.latitude, lng: pos.coords.longitude, @@ -197,6 +210,12 @@ export function useGeolocation(): UseGeolocationReturn { }, (err) => { setError(err.message || 'Location unavailable') + setErrorCode( + err.code === err.PERMISSION_DENIED ? 'permission-denied' + : err.code === err.POSITION_UNAVAILABLE ? 'unavailable' + : err.code === err.TIMEOUT ? 'timeout' + : null, + ) // Stay subscribed so a later fix can still recover (e.g. GPS // lock takes a while indoors). Only fully stop on permission denial. if (err.code === err.PERMISSION_DENIED) { @@ -243,5 +262,5 @@ export function useGeolocation(): UseGeolocationReturn { useEffect(() => stopWatch, [stopWatch]) - return { position, mode, error, cycleMode, setMode } + return { position, mode, error, errorCode, cycleMode, setMode } } diff --git a/shared/src/i18n/ar/map.ts b/shared/src/i18n/ar/map.ts index b5c555f9a..b286749a3 100644 --- a/shared/src/i18n/ar/map.ts +++ b/shared/src/i18n/ar/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'قمر صناعي', 'map.baseLayer.switchToSatellite': 'التبديل إلى عرض القمر الصناعي', 'map.baseLayer.switchToDefault': 'التبديل إلى عرض الخريطة', + 'map.location.denied': 'تم حظر الوصول إلى الموقع. تحقق من إعدادات الجهاز؛ فالتطبيق المثبت لديه إذن موقع خاص به منفصل عن المتصفح.', + 'map.location.unavailable': 'تعذر تحديد موقعك.', + 'map.location.timeout': 'استغرق تحديد موقعك وقتًا طويلًا. حاول مرة أخرى تحت سماء مكشوفة.', }; export default map; diff --git a/shared/src/i18n/br/map.ts b/shared/src/i18n/br/map.ts index 756ff4fee..167655546 100644 --- a/shared/src/i18n/br/map.ts +++ b/shared/src/i18n/br/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satélite', 'map.baseLayer.switchToSatellite': 'Mudar para vista de satélite', 'map.baseLayer.switchToDefault': 'Mudar para vista de mapa', + 'map.location.denied': 'O acesso à localização está bloqueado. Verifique as configurações do aparelho; um app instalado tem permissão de localização própria, separada do navegador.', + 'map.location.unavailable': 'Não foi possível determinar sua localização.', + 'map.location.timeout': 'A localização demorou demais. Tente de novo com uma visão mais aberta do céu.', }; export default map; diff --git a/shared/src/i18n/ca/map.ts b/shared/src/i18n/ca/map.ts index 35172d0da..abd643901 100644 --- a/shared/src/i18n/ca/map.ts +++ b/shared/src/i18n/ca/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satèl·lit', 'map.baseLayer.switchToSatellite': 'Canvia a vista de satèl·lit', 'map.baseLayer.switchToDefault': 'Canvia a vista de mapa', + 'map.location.denied': 'L’accés a la ubicació està bloquejat. Revisa la configuració del dispositiu; una app instal·lada té el seu propi permís d’ubicació, separat del navegador.', + 'map.location.unavailable': 'No s’ha pogut determinar la teva ubicació.', + 'map.location.timeout': 'La localització ha trigat massa. Torna-ho a provar amb una vista més clara del cel.', }; export default map; diff --git a/shared/src/i18n/cs/map.ts b/shared/src/i18n/cs/map.ts index c3ee0801e..c91d98cac 100644 --- a/shared/src/i18n/cs/map.ts +++ b/shared/src/i18n/cs/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satelit', 'map.baseLayer.switchToSatellite': 'Přepnout na satelitní zobrazení', 'map.baseLayer.switchToDefault': 'Přepnout na mapové zobrazení', + 'map.location.denied': 'Přístup k poloze je zablokován. Zkontrolujte nastavení zařízení; nainstalovaná aplikace má vlastní oprávnění k poloze, nezávislé na prohlížeči.', + 'map.location.unavailable': 'Vaši polohu se nepodařilo zjistit.', + 'map.location.timeout': 'Zjišťování polohy trvalo příliš dlouho. Zkuste to znovu s volným výhledem na oblohu.', }; export default map; diff --git a/shared/src/i18n/de/map.ts b/shared/src/i18n/de/map.ts index b7086627e..c02a651fd 100644 --- a/shared/src/i18n/de/map.ts +++ b/shared/src/i18n/de/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satellit', 'map.baseLayer.switchToSatellite': 'Zur Satellitenansicht wechseln', 'map.baseLayer.switchToDefault': 'Zur Kartenansicht wechseln', + 'map.location.denied': 'Der Standortzugriff ist blockiert. Prüfe die Geräteeinstellungen; eine installierte App hat eine eigene Standortberechtigung, unabhängig vom Browser.', + 'map.location.unavailable': 'Dein Standort konnte nicht ermittelt werden.', + 'map.location.timeout': 'Die Standortbestimmung hat zu lange gedauert. Versuche es mit freier Sicht zum Himmel erneut.', }; export default map; diff --git a/shared/src/i18n/en/map.ts b/shared/src/i18n/en/map.ts index 557f5ae74..700fba7e1 100644 --- a/shared/src/i18n/en/map.ts +++ b/shared/src/i18n/en/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satellite', 'map.baseLayer.switchToSatellite': 'Switch to satellite view', 'map.baseLayer.switchToDefault': 'Switch to map view', + 'map.location.denied': 'Location access is blocked. Check your device settings; an installed app has its own location permission, separate from the browser.', + 'map.location.unavailable': 'Your location could not be determined.', + 'map.location.timeout': 'Locating you took too long. Try again with a clearer view of the sky.', }; export default map; diff --git a/shared/src/i18n/es/map.ts b/shared/src/i18n/es/map.ts index 51999ac1c..5dff94102 100644 --- a/shared/src/i18n/es/map.ts +++ b/shared/src/i18n/es/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satélite', 'map.baseLayer.switchToSatellite': 'Cambiar a vista de satélite', 'map.baseLayer.switchToDefault': 'Cambiar a vista de mapa', + 'map.location.denied': 'El acceso a la ubicación está bloqueado. Revisa los ajustes del dispositivo; una app instalada tiene su propio permiso de ubicación, independiente del navegador.', + 'map.location.unavailable': 'No se pudo determinar tu ubicación.', + 'map.location.timeout': 'La localización tardó demasiado. Inténtalo de nuevo con una vista más despejada del cielo.', }; export default map; diff --git a/shared/src/i18n/fr/map.ts b/shared/src/i18n/fr/map.ts index 65d2e7783..a40e1f64c 100644 --- a/shared/src/i18n/fr/map.ts +++ b/shared/src/i18n/fr/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satellite', 'map.baseLayer.switchToSatellite': 'Passer à la vue satellite', 'map.baseLayer.switchToDefault': 'Passer à la vue carte', + 'map.location.denied': 'L’accès à la position est bloqué. Vérifiez les réglages de l’appareil. Une app installée possède sa propre autorisation de localisation, distincte du navigateur.', + 'map.location.unavailable': 'Votre position n’a pas pu être déterminée.', + 'map.location.timeout': 'La localisation a pris trop de temps. Réessayez avec une vue plus dégagée du ciel.', }; export default map; diff --git a/shared/src/i18n/gr/map.ts b/shared/src/i18n/gr/map.ts index c042a0cfd..63ec7ff83 100644 --- a/shared/src/i18n/gr/map.ts +++ b/shared/src/i18n/gr/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Δορυφόρος', 'map.baseLayer.switchToSatellite': 'Εναλλαγή σε δορυφορική προβολή', 'map.baseLayer.switchToDefault': 'Εναλλαγή σε προβολή χάρτη', + 'map.location.denied': 'Η πρόσβαση στην τοποθεσία είναι αποκλεισμένη. Ελέγξτε τις ρυθμίσεις της συσκευής. Μια εγκατεστημένη εφαρμογή έχει δική της άδεια τοποθεσίας, ξεχωριστή από το πρόγραμμα περιήγησης.', + 'map.location.unavailable': 'Δεν ήταν δυνατός ο προσδιορισμός της τοποθεσίας σας.', + 'map.location.timeout': 'Ο εντοπισμός της θέσης σας άργησε πολύ. Δοκιμάστε ξανά με καθαρή θέα στον ουρανό.', }; export default map; diff --git a/shared/src/i18n/hu/map.ts b/shared/src/i18n/hu/map.ts index af7bcf6e8..6b2a927d5 100644 --- a/shared/src/i18n/hu/map.ts +++ b/shared/src/i18n/hu/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Műhold', 'map.baseLayer.switchToSatellite': 'Váltás műholdas nézetre', 'map.baseLayer.switchToDefault': 'Váltás térkép nézetre', + 'map.location.denied': 'A helyhozzáférés le van tiltva. Ellenőrizd a készülék beállításait; a telepített alkalmazásnak saját helyengedélye van, a böngészőtől függetlenül.', + 'map.location.unavailable': 'A tartózkodási helyed nem határozható meg.', + 'map.location.timeout': 'A helymeghatározás túl sokáig tartott. Próbáld újra szabad ég alatt.', }; export default map; diff --git a/shared/src/i18n/id/map.ts b/shared/src/i18n/id/map.ts index 2a6ff0549..56454bdad 100644 --- a/shared/src/i18n/id/map.ts +++ b/shared/src/i18n/id/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satelit', 'map.baseLayer.switchToSatellite': 'Beralih ke tampilan satelit', 'map.baseLayer.switchToDefault': 'Beralih ke tampilan peta', + 'map.location.denied': 'Akses lokasi diblokir. Periksa pengaturan perangkat; aplikasi yang terpasang punya izin lokasi sendiri, terpisah dari browser.', + 'map.location.unavailable': 'Lokasi Anda tidak dapat ditentukan.', + 'map.location.timeout': 'Penentuan lokasi terlalu lama. Coba lagi di tempat dengan pandangan langit yang lebih terbuka.', }; export default map; diff --git a/shared/src/i18n/it/map.ts b/shared/src/i18n/it/map.ts index af78e55b6..c42884b44 100644 --- a/shared/src/i18n/it/map.ts +++ b/shared/src/i18n/it/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satellite', 'map.baseLayer.switchToSatellite': 'Passa alla vista satellitare', 'map.baseLayer.switchToDefault': 'Passa alla vista mappa', + 'map.location.denied': 'L’accesso alla posizione è bloccato. Controlla le impostazioni del dispositivo; un’app installata ha un proprio permesso di localizzazione, separato dal browser.', + 'map.location.unavailable': 'Impossibile determinare la tua posizione.', + 'map.location.timeout': 'La localizzazione ha richiesto troppo tempo. Riprova con una visuale più libera del cielo.', }; export default map; diff --git a/shared/src/i18n/ja/map.ts b/shared/src/i18n/ja/map.ts index 6e4e15c15..7e2aa1ae2 100644 --- a/shared/src/i18n/ja/map.ts +++ b/shared/src/i18n/ja/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': '衛星', 'map.baseLayer.switchToSatellite': '衛星表示に切り替える', 'map.baseLayer.switchToDefault': '地図表示に切り替える', + 'map.location.denied': '位置情報へのアクセスがブロックされています。端末の設定を確認してください。インストールしたアプリにはブラウザとは別の位置情報の許可があります。', + 'map.location.unavailable': '現在地を特定できませんでした。', + 'map.location.timeout': '位置情報の取得に時間がかかりすぎました。空がよく見える場所でもう一度お試しください。', }; export default map; diff --git a/shared/src/i18n/ko/map.ts b/shared/src/i18n/ko/map.ts index 8e143f3d7..d87093ad8 100644 --- a/shared/src/i18n/ko/map.ts +++ b/shared/src/i18n/ko/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': '위성', 'map.baseLayer.switchToSatellite': '위성 보기로 전환', 'map.baseLayer.switchToDefault': '지도 보기로 전환', + 'map.location.denied': '위치 접근이 차단되어 있습니다. 기기 설정을 확인하세요. 설치된 앱은 브라우저와 별도의 위치 권한을 사용합니다.', + 'map.location.unavailable': '현재 위치를 확인할 수 없습니다.', + 'map.location.timeout': '위치를 확인하는 데 시간이 너무 오래 걸렸습니다. 하늘이 잘 보이는 곳에서 다시 시도하세요.', }; export default map; diff --git a/shared/src/i18n/nl/map.ts b/shared/src/i18n/nl/map.ts index 291bc76eb..01cd0662e 100644 --- a/shared/src/i18n/nl/map.ts +++ b/shared/src/i18n/nl/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satelliet', 'map.baseLayer.switchToSatellite': 'Overschakelen naar satellietweergave', 'map.baseLayer.switchToDefault': 'Overschakelen naar kaartweergave', + 'map.location.denied': 'Locatietoegang is geblokkeerd. Controleer de apparaatinstellingen; een geïnstalleerde app heeft een eigen locatiemachtiging, los van de browser.', + 'map.location.unavailable': 'Je locatie kon niet worden bepaald.', + 'map.location.timeout': 'Het bepalen van je locatie duurde te lang. Probeer het opnieuw met vrij zicht op de lucht.', }; export default map; diff --git a/shared/src/i18n/pl/map.ts b/shared/src/i18n/pl/map.ts index 4e2d11315..8462c41c9 100644 --- a/shared/src/i18n/pl/map.ts +++ b/shared/src/i18n/pl/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satelita', 'map.baseLayer.switchToSatellite': 'Przełącz na widok satelitarny', 'map.baseLayer.switchToDefault': 'Przełącz na widok mapy', + 'map.location.denied': 'Dostęp do lokalizacji jest zablokowany. Sprawdź ustawienia urządzenia; zainstalowana aplikacja ma własne uprawnienie do lokalizacji, niezależne od przeglądarki.', + 'map.location.unavailable': 'Nie udało się ustalić Twojej lokalizacji.', + 'map.location.timeout': 'Ustalanie lokalizacji trwało zbyt długo. Spróbuj ponownie przy lepszej widoczności nieba.', }; export default map; diff --git a/shared/src/i18n/ru/map.ts b/shared/src/i18n/ru/map.ts index c613124c2..fe33312f9 100644 --- a/shared/src/i18n/ru/map.ts +++ b/shared/src/i18n/ru/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Спутник', 'map.baseLayer.switchToSatellite': 'Переключить на спутниковый вид', 'map.baseLayer.switchToDefault': 'Переключить на вид карты', + 'map.location.denied': 'Доступ к геопозиции заблокирован. Проверьте настройки устройства; у установленного приложения есть собственное разрешение на геопозицию, отдельное от браузера.', + 'map.location.unavailable': 'Не удалось определить ваше местоположение.', + 'map.location.timeout': 'Определение местоположения заняло слишком много времени. Попробуйте ещё раз под открытым небом.', }; export default map; diff --git a/shared/src/i18n/sv/map.ts b/shared/src/i18n/sv/map.ts index f08130660..f851d4016 100644 --- a/shared/src/i18n/sv/map.ts +++ b/shared/src/i18n/sv/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Satellit', 'map.baseLayer.switchToSatellite': 'Byt till satellitvy', 'map.baseLayer.switchToDefault': 'Byt till kartvy', + 'map.location.denied': 'Platsåtkomst är blockerad. Kontrollera enhetens inställningar; en installerad app har ett eget platstillstånd, skilt från webbläsaren.', + 'map.location.unavailable': 'Din plats kunde inte fastställas.', + 'map.location.timeout': 'Platsbestämningen tog för lång tid. Försök igen med friare sikt mot himlen.', }; export default map; diff --git a/shared/src/i18n/tr/map.ts b/shared/src/i18n/tr/map.ts index 1f05041ef..f0440ce01 100644 --- a/shared/src/i18n/tr/map.ts +++ b/shared/src/i18n/tr/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Uydu', 'map.baseLayer.switchToSatellite': 'Uydu görünümüne geç', 'map.baseLayer.switchToDefault': 'Harita görünümüne geç', + 'map.location.denied': 'Konum erişimi engellendi. Cihaz ayarlarını kontrol edin; yüklü bir uygulamanın tarayıcıdan ayrı kendi konum izni vardır.', + 'map.location.unavailable': 'Konumunuz belirlenemedi.', + 'map.location.timeout': 'Konum belirleme çok uzun sürdü. Gökyüzünü daha iyi gören bir yerde tekrar deneyin.', }; export default map; diff --git a/shared/src/i18n/uk/map.ts b/shared/src/i18n/uk/map.ts index c1a67f383..7ad961420 100644 --- a/shared/src/i18n/uk/map.ts +++ b/shared/src/i18n/uk/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Супутник', 'map.baseLayer.switchToSatellite': 'Перемкнути на супутниковий вигляд', 'map.baseLayer.switchToDefault': 'Перемкнути на вигляд карти', + 'map.location.denied': 'Доступ до геолокації заблоковано. Перевірте налаштування пристрою; встановлений застосунок має власний дозвіл на геолокацію, окремий від браузера.', + 'map.location.unavailable': 'Не вдалося визначити ваше місцезнаходження.', + 'map.location.timeout': 'Визначення місцезнаходження тривало надто довго. Спробуйте ще раз просто неба.', }; export default map; diff --git a/shared/src/i18n/vi/map.ts b/shared/src/i18n/vi/map.ts index ee735f448..98592065c 100644 --- a/shared/src/i18n/vi/map.ts +++ b/shared/src/i18n/vi/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': 'Vệ tinh', 'map.baseLayer.switchToSatellite': 'Chuyển sang chế độ xem vệ tinh', 'map.baseLayer.switchToDefault': 'Chuyển sang chế độ xem bản đồ', + 'map.location.denied': 'Quyền truy cập vị trí đang bị chặn. Hãy kiểm tra cài đặt thiết bị; ứng dụng đã cài đặt có quyền vị trí riêng, tách biệt với trình duyệt.', + 'map.location.unavailable': 'Không thể xác định vị trí của bạn.', + 'map.location.timeout': 'Việc xác định vị trí mất quá nhiều thời gian. Hãy thử lại ở nơi nhìn thấy bầu trời thoáng hơn.', }; export default map; diff --git a/shared/src/i18n/zh-TW/map.ts b/shared/src/i18n/zh-TW/map.ts index 9765f2f55..d5d2c5ea3 100644 --- a/shared/src/i18n/zh-TW/map.ts +++ b/shared/src/i18n/zh-TW/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': '衛星', 'map.baseLayer.switchToSatellite': '切換到衛星檢視', 'map.baseLayer.switchToDefault': '切換到地圖檢視', + 'map.location.denied': '定位權限已被封鎖。請檢查裝置設定,已安裝的應用程式擁有與瀏覽器分開的定位權限。', + 'map.location.unavailable': '無法判斷你的位置。', + 'map.location.timeout': '定位花費的時間過長。請在能看到天空的開闊位置重試。', }; export default map; diff --git a/shared/src/i18n/zh/map.ts b/shared/src/i18n/zh/map.ts index ed57dfdaf..ddc257a2e 100644 --- a/shared/src/i18n/zh/map.ts +++ b/shared/src/i18n/zh/map.ts @@ -19,5 +19,8 @@ const map: TranslationStrings = { 'map.baseLayer.satellite': '卫星', 'map.baseLayer.switchToSatellite': '切换到卫星视图', 'map.baseLayer.switchToDefault': '切换到地图视图', + 'map.location.denied': '定位权限已被阻止。请检查设备设置,已安装的应用拥有与浏览器分开的定位权限。', + 'map.location.unavailable': '无法确定你的位置。', + 'map.location.timeout': '定位耗时过长。请在能看到天空的开阔位置重试。', }; export default map;