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;