diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx index 86c19ce13..accb1d687 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx @@ -1,10 +1,11 @@ import { useEffect, useState, useRef } from 'react' -import { X, Plus, Image, Minus, Check, MapPin } from 'lucide-react' +import { X, Plus, Image, Minus, Check, MapPin, Locate } from 'lucide-react' import { normalizeImageFiles } from '../../utils/convertHeic' import { type ResilientResult, type UploadProgress } from '../../utils/uploadQueue' import { useTranslation } from '../../i18n' import { journeyApi, mapsApi, addonsApi } from '../../api/client' import { useToast } from '../shared/Toast' +import { getCurrentPositionOnce, GeoOnceError } from '../../hooks/useGeolocation' import { getApiErrorMessage } from '../../types' import type { JourneyEntry, JourneyPhoto, GalleryPhoto, JourneyTrip } from '../../store/journeyStore' import { MOOD_CONFIG, WEATHER_CONFIG } from '../../pages/journeyDetail/JourneyDetailPage.constants' @@ -28,7 +29,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, onAddProviderPhotos?: (entryId: number, group: PendingProviderGroup) => Promise onDone: () => void }) { - const { t } = useTranslation() + const { t, language } = useTranslation() const toast = useToast() const [title, setTitle] = useState(entry.title || '') const [story, setStory] = useState(entry.story || '') @@ -41,6 +42,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, const [locationResults, setLocationResults] = useState<{ name: string; address?: string; lat: number; lng: number }[]>([]) const [locationSearching, setLocationSearching] = useState(false) const [showLocationResults, setShowLocationResults] = useState(false) + const [locating, setLocating] = useState(false) const locationTimerRef = useRef | null>(null) const [mood, setMood] = useState(entry.mood || '') const [weather, setWeather] = useState(entry.weather || '') @@ -196,6 +198,41 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, ? { lat: locationLat!, lng: locationLng!, name: locationName || undefined } : null + const handleUseCurrentLocation = async () => { + if (locating) return + setLocating(true) + try { + const pos = await getCurrentPositionOnce() + // Fill coordinates right away; the name is refined below once the + // reverse geocode comes back. + const fallbackName = `${pos.lat.toFixed(5)}, ${pos.lng.toFixed(5)}` + if (locationTimerRef.current) clearTimeout(locationTimerRef.current) + setLocationSearching(false) + setLocationLat(pos.lat) + setLocationLng(pos.lng) + setLocationName(fallbackName) + setLocationQuery('') + setLocationResults([]) + setShowLocationResults(false) + try { + const data = await mapsApi.reverse(pos.lat, pos.lng, language) + const name = data.name || data.address + // Only replace the coordinate fallback — don't clobber a search + // result the user may have picked while the reverse call was in flight. + if (name) setLocationName(prev => (prev === fallbackName ? name : prev)) + } catch { /* best effort — keep the coordinate fallback */ } + } catch (err) { + const code = err instanceof GeoOnceError ? err.code : 'unavailable' + toast.error( + code === 'permission-denied' ? t('journey.editor.locationPermissionDenied') + : code === 'timeout' ? t('journey.editor.locationTimeout') + : code === 'insecure-context' ? t('journey.editor.locationInsecureContext') + : t('journey.editor.locationUnavailable')) + } finally { + setLocating(false) + } + } + return (
{/* The modal itself is constrained to the feed column on desktop so it @@ -558,8 +595,20 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, }} onFocus={() => { if (locationResults.length > 0) setShowLocationResults(true) }} placeholder={t('journey.editor.searchLocation')} - className="w-full px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500" + className="w-full pl-3 pr-9 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500" /> +
{showLocationResults && locationResults.length > 0 && ( <> diff --git a/client/src/hooks/useGeolocation.test.ts b/client/src/hooks/useGeolocation.test.ts index fb9ce889c..0f3bba137 100644 --- a/client/src/hooks/useGeolocation.test.ts +++ b/client/src/hooks/useGeolocation.test.ts @@ -1,6 +1,6 @@ -// FE-HOOK-GEO-001 to FE-HOOK-GEO-014 +// FE-HOOK-GEO-001 to FE-HOOK-GEO-023 import { act, renderHook } from '@testing-library/react'; -import { useGeolocation } from './useGeolocation'; +import { GeoOnceError, getCurrentPositionOnce, useGeolocation } from './useGeolocation'; type WatchSuccess = (pos: GeolocationPosition) => void; type WatchFailure = (err: GeolocationPositionError) => void; @@ -259,3 +259,52 @@ describe('useGeolocation', () => { expect(clearWatch).toHaveBeenCalledWith(7); }); }); + +describe('getCurrentPositionOnce', () => { + /** Swaps the watch-based stub from beforeEach for a one-shot one. */ + function stubOnce(getCurrentPosition: (ok: PositionCallback, fail?: PositionErrorCallback) => void): void { + Object.defineProperty(navigator, 'geolocation', { + value: { getCurrentPosition: vi.fn(getCurrentPosition) }, + configurable: true, + }); + } + + it('FE-HOOK-GEO-020: resolves a mapped position from a single fix', async () => { + stubOnce(ok => ok(fix({ latitude: 41.9, longitude: 12.5, accuracy: 10 }))); + + await expect(getCurrentPositionOnce()).resolves.toEqual({ + lat: 41.9, lng: 12.5, accuracy: 10, heading: null, speed: null, timestamp: 1_700_000_000_000, + }); + }); + + it('FE-HOOK-GEO-021: rejects as unsupported when the browser has no geolocation', async () => { + // Same guard as FE-HOOK-GEO-008: the key has to go away entirely. + delete (navigator as { geolocation?: Geolocation }).geolocation; + + await expect(getCurrentPositionOnce()).rejects.toMatchObject({ + name: 'GeoOnceError', + code: 'unsupported', + }); + }); + + it('FE-HOOK-GEO-022: rejects outside a secure context without asking the browser', async () => { + const getCurrentPosition = vi.fn(); + stubOnce(getCurrentPosition); + vi.stubGlobal('isSecureContext', false); + + await expect(getCurrentPositionOnce()).rejects.toMatchObject({ code: 'insecure-context' }); + expect(getCurrentPosition).not.toHaveBeenCalled(); + }); + + it.each([ + [1, 'permission-denied'], + [2, 'unavailable'], + [3, 'timeout'], + ])('FE-HOOK-GEO-023: maps error code %i to "%s"', async (code, expected) => { + stubOnce((_ok, fail) => fail?.(geoError(code))); + + const rejection = await getCurrentPositionOnce().catch((e: unknown) => e); + expect(rejection).toBeInstanceOf(GeoOnceError); + expect((rejection as GeoOnceError).code).toBe(expected); + }); +}); diff --git a/client/src/hooks/useGeolocation.ts b/client/src/hooks/useGeolocation.ts index 70557d9ee..e6f991082 100644 --- a/client/src/hooks/useGeolocation.ts +++ b/client/src/hooks/useGeolocation.ts @@ -40,6 +40,56 @@ function smoothAngle(prev: number | null, next: number, alpha = 0.25): number { return (prev + delta * alpha + 360) % 360 } +export type GeoOnceErrorCode = 'unsupported' | 'insecure-context' | 'permission-denied' | 'unavailable' | 'timeout' + +export class GeoOnceError extends Error { + code: GeoOnceErrorCode + + constructor(code: GeoOnceErrorCode, message?: string) { + super(message || code) + this.name = 'GeoOnceError' + this.code = code + } +} + +// One-shot position fix for form fields (e.g. journal entry location). +// Unlike the watch below, a cached fix up to a minute old is fine here — +// the user is standing where they're journaling — so maximumAge is +// generous to make the button feel instant when the OS already has a fix. +export function getCurrentPositionOnce( + options: PositionOptions = { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 }, +): Promise { + return new Promise((resolve, reject) => { + if (!('geolocation' in navigator)) { + reject(new GeoOnceError('unsupported')) + return + } + // Browsers hard-block geolocation outside secure contexts; surface that + // explicitly since self-hosted instances served over plain HTTP hit it. + if (typeof window !== 'undefined' && window.isSecureContext === false) { + reject(new GeoOnceError('insecure-context')) + return + } + navigator.geolocation.getCurrentPosition( + (pos) => resolve({ + lat: pos.coords.latitude, + lng: pos.coords.longitude, + accuracy: pos.coords.accuracy, + heading: pos.coords.heading ?? null, + speed: pos.coords.speed ?? null, + timestamp: pos.timestamp, + }), + (err) => reject(new GeoOnceError( + err.code === err.PERMISSION_DENIED ? 'permission-denied' + : err.code === err.TIMEOUT ? 'timeout' + : 'unavailable', + err.message, + )), + options, + ) + }) +} + export function useGeolocation(): UseGeolocationReturn { const [position, setPosition] = useState(null) const [mode, setModeState] = useState('off') diff --git a/client/src/pages/JourneyDetailPage.test.tsx b/client/src/pages/JourneyDetailPage.test.tsx index 352cbd50d..58a828b98 100644 --- a/client/src/pages/JourneyDetailPage.test.tsx +++ b/client/src/pages/JourneyDetailPage.test.tsx @@ -3850,4 +3850,95 @@ describe('JourneyDetailPage', () => { expect(updateCalls).toBe(1); }); }); + + // ── FE-PAGE-JOURNEYDETAIL-154 ────────────────────────────────────────── + describe('FE-PAGE-JOURNEYDETAIL-154: EntryEditor use-current-location button', () => { + const originalGeolocation = Object.getOwnPropertyDescriptor(navigator, 'geolocation'); + + function stubGeolocation(getCurrentPosition: (success: PositionCallback, error?: PositionErrorCallback) => void) { + Object.defineProperty(navigator, 'geolocation', { + configurable: true, + value: { getCurrentPosition: vi.fn(getCurrentPosition) }, + }); + } + + const stubGeoSuccess = () => + stubGeolocation(success => success({ + coords: { + latitude: 41.9, longitude: 12.5, accuracy: 10, + heading: null, speed: null, altitude: null, altitudeAccuracy: null, + }, + timestamp: now, + } as unknown as GeolocationPosition)); + + afterEach(() => { + if (originalGeolocation) { + Object.defineProperty(navigator, 'geolocation', originalGeolocation); + } else { + delete (navigator as { geolocation?: unknown }).geolocation; + } + delete (window as { __addToast?: unknown }).__addToast; + }); + + it('fills the location field from geolocation and reverse geocoding', async () => { + stubGeoSuccess(); + server.use( + http.get('/api/maps/reverse', () => { + return HttpResponse.json({ name: 'Colosseum', address: 'Rome, Italy' }); + }), + ); + + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + await renderAndWait(); + await openEntryEditor(user); + + await user.click(screen.getByRole('button', { name: 'Use my current location' })); + + await waitFor(() => { + expect(screen.getByPlaceholderText('Search location...')).toHaveValue('Colosseum'); + }); + }); + + it('falls back to coordinates when reverse geocoding returns nothing', async () => { + stubGeoSuccess(); + server.use( + http.get('/api/maps/reverse', () => { + return HttpResponse.json({ name: null, address: null }); + }), + ); + + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + await renderAndWait(); + await openEntryEditor(user); + + await user.click(screen.getByRole('button', { name: 'Use my current location' })); + + await waitFor(() => { + expect(screen.getByPlaceholderText('Search location...')).toHaveValue('41.90000, 12.50000'); + }); + }); + + it('shows an error toast when location permission is denied', async () => { + const addToast = vi.fn(); + (window as { __addToast?: unknown }).__addToast = addToast; + stubGeolocation((_success, error) => error?.({ + code: 1, message: 'denied', + PERMISSION_DENIED: 1, POSITION_UNAVAILABLE: 2, TIMEOUT: 3, + } as GeolocationPositionError)); + + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + await renderAndWait(); + await openEntryEditor(user); + + await user.click(screen.getByRole('button', { name: 'Use my current location' })); + + await waitFor(() => { + expect(addToast).toHaveBeenCalledWith( + 'Location access was denied. Allow it in your browser settings and try again.', + 'error', + undefined, + ); + }); + }); + }); }); diff --git a/shared/src/i18n/ar/journey.ts b/shared/src/i18n/ar/journey.ts index 32fb80068..f37cc168b 100644 --- a/shared/src/i18n/ar/journey.ts +++ b/shared/src/i18n/ar/journey.ts @@ -20,6 +20,11 @@ const journey: TranslationStrings = { 'journey.editor.addAnother': 'إضافة آخر', 'journey.editor.makeFirst': 'جعله الأول', 'journey.editor.searching': 'جارٍ البحث...', + 'journey.editor.useCurrentLocation': 'استخدام موقعي الحالي', + 'journey.editor.locationPermissionDenied': 'تم رفض الوصول إلى الموقع. اسمح به في إعدادات المتصفح وحاول مرة أخرى.', + 'journey.editor.locationTimeout': 'انتهت مهلة تحديد موقعك. حاول مرة أخرى.', + 'journey.editor.locationUnavailable': 'تعذر تحديد موقعك.', + 'journey.editor.locationInsecureContext': 'يتطلب تحديد الموقع اتصالاً آمناً (HTTPS).', 'journey.share.copy': 'نسخ', 'journey.share.copied': 'تم النسخ!', 'journey.invite.role': 'الدور', diff --git a/shared/src/i18n/br/journey.ts b/shared/src/i18n/br/journey.ts index 5121213e8..cc1c1dead 100644 --- a/shared/src/i18n/br/journey.ts +++ b/shared/src/i18n/br/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1º', 'journey.editor.makeFirst': 'Tornar 1º', 'journey.editor.searching': 'Pesquisando...', + 'journey.editor.useCurrentLocation': 'Usar minha localização atual', + 'journey.editor.locationPermissionDenied': + 'O acesso à localização foi negado. Permita nas configurações do navegador e tente novamente.', + 'journey.editor.locationTimeout': 'Tempo esgotado ao obter sua localização. Tente novamente.', + 'journey.editor.locationUnavailable': 'Não foi possível determinar sua localização.', + 'journey.editor.locationInsecureContext': 'A localização requer uma conexão segura (HTTPS).', 'journey.mood.amazing': 'Incrível', 'journey.mood.good': 'Bom', 'journey.mood.neutral': 'Neutro', diff --git a/shared/src/i18n/ca/journey.ts b/shared/src/i18n/ca/journey.ts index 6b4ac085b..a3008611c 100644 --- a/shared/src/i18n/ca/journey.ts +++ b/shared/src/i18n/ca/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1r', 'journey.editor.makeFirst': 'Fes 1r', 'journey.editor.searching': 'Cercant...', + 'journey.editor.useCurrentLocation': 'Utilitza la meva ubicació actual', + 'journey.editor.locationPermissionDenied': + "S'ha denegat l'accés a la ubicació. Permet-lo a la configuració del navegador i torna-ho a provar.", + 'journey.editor.locationTimeout': "S'ha esgotat el temps per obtenir la ubicació. Torna-ho a provar.", + 'journey.editor.locationUnavailable': "No s'ha pogut determinar la teva ubicació.", + 'journey.editor.locationInsecureContext': 'La ubicació requereix una connexió segura (HTTPS).', 'journey.mood.amazing': 'Increïble', 'journey.mood.good': 'Bé', 'journey.mood.neutral': 'Neutral', diff --git a/shared/src/i18n/cs/journey.ts b/shared/src/i18n/cs/journey.ts index 5ab03c0e2..d76f84097 100644 --- a/shared/src/i18n/cs/journey.ts +++ b/shared/src/i18n/cs/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1.', 'journey.editor.makeFirst': 'Nastavit jako 1.', 'journey.editor.searching': 'Hledání...', + 'journey.editor.useCurrentLocation': 'Použít moji aktuální polohu', + 'journey.editor.locationPermissionDenied': + 'Přístup k poloze byl odepřen. Povolte ho v nastavení prohlížeče a zkuste to znovu.', + 'journey.editor.locationTimeout': 'Vypršel čas při zjišťování polohy. Zkuste to znovu.', + 'journey.editor.locationUnavailable': 'Nepodařilo se zjistit vaši polohu.', + 'journey.editor.locationInsecureContext': 'Poloha vyžaduje zabezpečené připojení (HTTPS).', 'journey.mood.amazing': 'Úžasný', 'journey.mood.good': 'Dobrý', 'journey.mood.neutral': 'Neutrální', diff --git a/shared/src/i18n/de/journey.ts b/shared/src/i18n/de/journey.ts index d838d12c7..6b4cad6a2 100644 --- a/shared/src/i18n/de/journey.ts +++ b/shared/src/i18n/de/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1.', 'journey.editor.makeFirst': 'Als 1. setzen', 'journey.editor.searching': 'Suche...', + 'journey.editor.useCurrentLocation': 'Meinen aktuellen Standort verwenden', + 'journey.editor.locationPermissionDenied': + 'Standortzugriff wurde verweigert. Erlaube ihn in den Browser-Einstellungen und versuche es erneut.', + 'journey.editor.locationTimeout': 'Zeitüberschreitung beim Ermitteln deines Standorts. Versuche es erneut.', + 'journey.editor.locationUnavailable': 'Dein Standort konnte nicht ermittelt werden.', + 'journey.editor.locationInsecureContext': 'Der Standortzugriff erfordert eine sichere Verbindung (HTTPS).', 'journey.mood.amazing': 'Großartig', 'journey.mood.good': 'Gut', 'journey.mood.neutral': 'Neutral', diff --git a/shared/src/i18n/en/journey.ts b/shared/src/i18n/en/journey.ts index a9a153ff1..64cc10d7e 100644 --- a/shared/src/i18n/en/journey.ts +++ b/shared/src/i18n/en/journey.ts @@ -133,6 +133,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1st', 'journey.editor.makeFirst': 'Make 1st', 'journey.editor.searching': 'Searching...', + 'journey.editor.useCurrentLocation': 'Use my current location', + 'journey.editor.locationPermissionDenied': + 'Location access was denied. Allow it in your browser settings and try again.', + 'journey.editor.locationTimeout': 'Timed out getting your location. Try again.', + 'journey.editor.locationUnavailable': 'Could not determine your location.', + 'journey.editor.locationInsecureContext': 'Location requires a secure (HTTPS) connection.', 'journey.mood.amazing': 'Amazing', 'journey.mood.good': 'Good', 'journey.mood.neutral': 'Neutral', diff --git a/shared/src/i18n/es/journey.ts b/shared/src/i18n/es/journey.ts index 7f329a547..9f032a828 100644 --- a/shared/src/i18n/es/journey.ts +++ b/shared/src/i18n/es/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1º', 'journey.editor.makeFirst': 'Hacer 1º', 'journey.editor.searching': 'Buscando...', + 'journey.editor.useCurrentLocation': 'Usar mi ubicación actual', + 'journey.editor.locationPermissionDenied': + 'Se denegó el acceso a la ubicación. Permítelo en la configuración de tu navegador e inténtalo de nuevo.', + 'journey.editor.locationTimeout': 'Se agotó el tiempo para obtener tu ubicación. Inténtalo de nuevo.', + 'journey.editor.locationUnavailable': 'No se pudo determinar tu ubicación.', + 'journey.editor.locationInsecureContext': 'La ubicación requiere una conexión segura (HTTPS).', 'journey.mood.amazing': 'Increíble', 'journey.mood.good': 'Bien', 'journey.mood.neutral': 'Neutral', diff --git a/shared/src/i18n/fr/journey.ts b/shared/src/i18n/fr/journey.ts index 4112756aa..30131c3ce 100644 --- a/shared/src/i18n/fr/journey.ts +++ b/shared/src/i18n/fr/journey.ts @@ -126,6 +126,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1er', 'journey.editor.makeFirst': 'Mettre en 1er', 'journey.editor.searching': 'Recherche...', + 'journey.editor.useCurrentLocation': 'Utiliser ma position actuelle', + 'journey.editor.locationPermissionDenied': + "L'accès à la position a été refusé. Autorisez-le dans les paramètres de votre navigateur et réessayez.", + 'journey.editor.locationTimeout': 'Délai dépassé pour obtenir votre position. Réessayez.', + 'journey.editor.locationUnavailable': 'Impossible de déterminer votre position.', + 'journey.editor.locationInsecureContext': 'La géolocalisation nécessite une connexion sécurisée (HTTPS).', 'journey.mood.amazing': 'Incroyable', 'journey.mood.good': 'Bien', 'journey.mood.neutral': 'Neutre', diff --git a/shared/src/i18n/gr/journey.ts b/shared/src/i18n/gr/journey.ts index 66a78114b..dcf197e1f 100644 --- a/shared/src/i18n/gr/journey.ts +++ b/shared/src/i18n/gr/journey.ts @@ -128,6 +128,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1η', 'journey.editor.makeFirst': 'Ορισμός ως 1η', 'journey.editor.searching': 'Αναζήτηση...', + 'journey.editor.useCurrentLocation': 'Χρήση της τρέχουσας τοποθεσίας μου', + 'journey.editor.locationPermissionDenied': + 'Η πρόσβαση στην τοποθεσία απορρίφθηκε. Επιτρέψτε την στις ρυθμίσεις του προγράμματος περιήγησης και δοκιμάστε ξανά.', + 'journey.editor.locationTimeout': 'Λήξη χρονικού ορίου κατά τον εντοπισμό της τοποθεσίας σας. Δοκιμάστε ξανά.', + 'journey.editor.locationUnavailable': 'Δεν ήταν δυνατός ο εντοπισμός της τοποθεσίας σας.', + 'journey.editor.locationInsecureContext': 'Η τοποθεσία απαιτεί ασφαλή σύνδεση (HTTPS).', 'journey.mood.amazing': 'Καταπληκτική', 'journey.mood.good': 'Καλή', 'journey.mood.neutral': 'Ουδέτερη', diff --git a/shared/src/i18n/hu/journey.ts b/shared/src/i18n/hu/journey.ts index b7c5405a3..73e14620f 100644 --- a/shared/src/i18n/hu/journey.ts +++ b/shared/src/i18n/hu/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1.', 'journey.editor.makeFirst': 'Legyen az 1.', 'journey.editor.searching': 'Keresés...', + 'journey.editor.useCurrentLocation': 'Jelenlegi helyzetem használata', + 'journey.editor.locationPermissionDenied': + 'A helymeghatározás engedélye megtagadva. Engedélyezd a böngésző beállításaiban, majd próbáld újra.', + 'journey.editor.locationTimeout': 'Időtúllépés a helyzeted lekérése közben. Próbáld újra.', + 'journey.editor.locationUnavailable': 'Nem sikerült meghatározni a helyzeted.', + 'journey.editor.locationInsecureContext': 'A helymeghatározáshoz biztonságos (HTTPS) kapcsolat szükséges.', 'journey.mood.amazing': 'Fantasztikus', 'journey.mood.good': 'Jó', 'journey.mood.neutral': 'Semleges', diff --git a/shared/src/i18n/id/journey.ts b/shared/src/i18n/id/journey.ts index adfc52a55..83d17d99e 100644 --- a/shared/src/i18n/id/journey.ts +++ b/shared/src/i18n/id/journey.ts @@ -125,6 +125,11 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1.', 'journey.editor.makeFirst': 'Jadikan ke-1', 'journey.editor.searching': 'Mencari...', + 'journey.editor.useCurrentLocation': 'Gunakan lokasi saya saat ini', + 'journey.editor.locationPermissionDenied': 'Akses lokasi ditolak. Izinkan di pengaturan browser lalu coba lagi.', + 'journey.editor.locationTimeout': 'Waktu habis saat mengambil lokasi. Coba lagi.', + 'journey.editor.locationUnavailable': 'Tidak dapat menentukan lokasi Anda.', + 'journey.editor.locationInsecureContext': 'Lokasi memerlukan koneksi aman (HTTPS).', 'journey.mood.amazing': 'Luar biasa', 'journey.mood.good': 'Baik', 'journey.mood.neutral': 'Biasa', diff --git a/shared/src/i18n/it/journey.ts b/shared/src/i18n/it/journey.ts index 67e9c4a74..0a8ddc337 100644 --- a/shared/src/i18n/it/journey.ts +++ b/shared/src/i18n/it/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1°', 'journey.editor.makeFirst': 'Metti 1°', 'journey.editor.searching': 'Ricerca...', + 'journey.editor.useCurrentLocation': 'Usa la mia posizione attuale', + 'journey.editor.locationPermissionDenied': + "L'accesso alla posizione è stato negato. Consentilo nelle impostazioni del browser e riprova.", + 'journey.editor.locationTimeout': 'Tempo scaduto durante il rilevamento della posizione. Riprova.', + 'journey.editor.locationUnavailable': 'Impossibile determinare la tua posizione.', + 'journey.editor.locationInsecureContext': 'La posizione richiede una connessione sicura (HTTPS).', 'journey.mood.amazing': 'Fantastico', 'journey.mood.good': 'Buono', 'journey.mood.neutral': 'Neutro', diff --git a/shared/src/i18n/ja/journey.ts b/shared/src/i18n/ja/journey.ts index 6bf5e5d97..0b4f25b81 100644 --- a/shared/src/i18n/ja/journey.ts +++ b/shared/src/i18n/ja/journey.ts @@ -123,6 +123,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1番目', 'journey.editor.makeFirst': '1番目にする', 'journey.editor.searching': '検索中…', + 'journey.editor.useCurrentLocation': '現在地を使用', + 'journey.editor.locationPermissionDenied': + '位置情報へのアクセスが拒否されました。ブラウザの設定で許可して再試行してください。', + 'journey.editor.locationTimeout': '位置情報の取得がタイムアウトしました。もう一度お試しください。', + 'journey.editor.locationUnavailable': '現在地を特定できませんでした。', + 'journey.editor.locationInsecureContext': '位置情報には安全な接続(HTTPS)が必要です。', 'journey.mood.amazing': '最高', 'journey.mood.good': '良い', 'journey.mood.neutral': '普通', diff --git a/shared/src/i18n/ko/journey.ts b/shared/src/i18n/ko/journey.ts index f7abe9850..4344b9483 100644 --- a/shared/src/i18n/ko/journey.ts +++ b/shared/src/i18n/ko/journey.ts @@ -123,6 +123,11 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1번째', 'journey.editor.makeFirst': '1번째로 설정', 'journey.editor.searching': '검색 중...', + 'journey.editor.useCurrentLocation': '현재 위치 사용', + 'journey.editor.locationPermissionDenied': '위치 접근이 거부되었습니다. 브라우저 설정에서 허용한 후 다시 시도하세요.', + 'journey.editor.locationTimeout': '위치를 가져오는 데 시간이 초과되었습니다. 다시 시도하세요.', + 'journey.editor.locationUnavailable': '위치를 확인할 수 없습니다.', + 'journey.editor.locationInsecureContext': '위치 기능에는 보안 연결(HTTPS)이 필요합니다.', 'journey.mood.amazing': '최고!', 'journey.mood.good': '좋음', 'journey.mood.neutral': '보통', diff --git a/shared/src/i18n/nl/journey.ts b/shared/src/i18n/nl/journey.ts index e038106b2..e0f20736b 100644 --- a/shared/src/i18n/nl/journey.ts +++ b/shared/src/i18n/nl/journey.ts @@ -126,6 +126,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1e', 'journey.editor.makeFirst': 'Maak 1e', 'journey.editor.searching': 'Zoeken...', + 'journey.editor.useCurrentLocation': 'Mijn huidige locatie gebruiken', + 'journey.editor.locationPermissionDenied': + 'Locatietoegang is geweigerd. Sta het toe in je browserinstellingen en probeer het opnieuw.', + 'journey.editor.locationTimeout': 'Time-out bij het ophalen van je locatie. Probeer het opnieuw.', + 'journey.editor.locationUnavailable': 'Je locatie kon niet worden bepaald.', + 'journey.editor.locationInsecureContext': 'Locatie vereist een beveiligde (HTTPS-)verbinding.', 'journey.mood.amazing': 'Fantastisch', 'journey.mood.good': 'Goed', 'journey.mood.neutral': 'Neutraal', diff --git a/shared/src/i18n/pl/journey.ts b/shared/src/i18n/pl/journey.ts index 2d65c50eb..66310554d 100644 --- a/shared/src/i18n/pl/journey.ts +++ b/shared/src/i18n/pl/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1.', 'journey.editor.makeFirst': 'Ustaw jako 1.', 'journey.editor.searching': 'Szukanie...', + 'journey.editor.useCurrentLocation': 'Użyj mojej bieżącej lokalizacji', + 'journey.editor.locationPermissionDenied': + 'Dostęp do lokalizacji został odrzucony. Zezwól na niego w ustawieniach przeglądarki i spróbuj ponownie.', + 'journey.editor.locationTimeout': 'Przekroczono limit czasu pobierania lokalizacji. Spróbuj ponownie.', + 'journey.editor.locationUnavailable': 'Nie udało się ustalić Twojej lokalizacji.', + 'journey.editor.locationInsecureContext': 'Lokalizacja wymaga bezpiecznego połączenia (HTTPS).', 'journey.mood.amazing': 'Niesamowity', 'journey.mood.good': 'Dobry', 'journey.mood.neutral': 'Neutralny', diff --git a/shared/src/i18n/ru/journey.ts b/shared/src/i18n/ru/journey.ts index 40fdcefec..3b616b486 100644 --- a/shared/src/i18n/ru/journey.ts +++ b/shared/src/i18n/ru/journey.ts @@ -125,6 +125,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1-е', 'journey.editor.makeFirst': 'Сделать 1-м', 'journey.editor.searching': 'Поиск...', + 'journey.editor.useCurrentLocation': 'Использовать моё текущее местоположение', + 'journey.editor.locationPermissionDenied': + 'Доступ к геолокации запрещён. Разрешите его в настройках браузера и попробуйте снова.', + 'journey.editor.locationTimeout': 'Время ожидания определения местоположения истекло. Попробуйте снова.', + 'journey.editor.locationUnavailable': 'Не удалось определить ваше местоположение.', + 'journey.editor.locationInsecureContext': 'Для геолокации требуется защищённое соединение (HTTPS).', 'journey.mood.amazing': 'Потрясающе', 'journey.mood.good': 'Хорошо', 'journey.mood.neutral': 'Нейтрально', diff --git a/shared/src/i18n/sv/journey.ts b/shared/src/i18n/sv/journey.ts index d625a0b7c..60876cccb 100644 --- a/shared/src/i18n/sv/journey.ts +++ b/shared/src/i18n/sv/journey.ts @@ -126,6 +126,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1:a', 'journey.editor.makeFirst': 'Gör 1:a', 'journey.editor.searching': 'Söker...', + 'journey.editor.useCurrentLocation': 'Använd min nuvarande plats', + 'journey.editor.locationPermissionDenied': + 'Platsåtkomst nekades. Tillåt den i webbläsarens inställningar och försök igen.', + 'journey.editor.locationTimeout': 'Tidsgränsen för att hämta din plats överskreds. Försök igen.', + 'journey.editor.locationUnavailable': 'Din plats kunde inte fastställas.', + 'journey.editor.locationInsecureContext': 'Plats kräver en säker anslutning (HTTPS).', 'journey.mood.amazing': 'Fantastiskt', 'journey.mood.good': 'Bra', 'journey.mood.neutral': 'Neutral', diff --git a/shared/src/i18n/tr/journey.ts b/shared/src/i18n/tr/journey.ts index 751239440..666ececfa 100644 --- a/shared/src/i18n/tr/journey.ts +++ b/shared/src/i18n/tr/journey.ts @@ -123,6 +123,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1.', 'journey.editor.makeFirst': '1. yap', 'journey.editor.searching': 'Aranıyor...', + 'journey.editor.useCurrentLocation': 'Mevcut konumumu kullan', + 'journey.editor.locationPermissionDenied': + 'Konum erişimi reddedildi. Tarayıcı ayarlarından izin verip tekrar deneyin.', + 'journey.editor.locationTimeout': 'Konum alınırken zaman aşımı oluştu. Tekrar deneyin.', + 'journey.editor.locationUnavailable': 'Konumunuz belirlenemedi.', + 'journey.editor.locationInsecureContext': 'Konum için güvenli (HTTPS) bir bağlantı gerekir.', 'journey.mood.amazing': 'Muhteşem', 'journey.mood.good': 'İyi', 'journey.mood.neutral': 'Nötr', diff --git a/shared/src/i18n/uk/journey.ts b/shared/src/i18n/uk/journey.ts index 274b34288..9e181584d 100644 --- a/shared/src/i18n/uk/journey.ts +++ b/shared/src/i18n/uk/journey.ts @@ -123,6 +123,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '1-е', 'journey.editor.makeFirst': 'Зробити першим', 'journey.editor.searching': 'Пошук...', + 'journey.editor.useCurrentLocation': 'Використати моє поточне місцезнаходження', + 'journey.editor.locationPermissionDenied': + 'Доступ до геолокації заборонено. Дозвольте його в налаштуваннях браузера та спробуйте ще раз.', + 'journey.editor.locationTimeout': 'Час очікування визначення місцезнаходження вичерпано. Спробуйте ще раз.', + 'journey.editor.locationUnavailable': 'Не вдалося визначити ваше місцезнаходження.', + 'journey.editor.locationInsecureContext': "Для геолокації потрібне захищене з'єднання (HTTPS).", 'journey.mood.amazing': 'Неймовірно', 'journey.mood.good': 'Добре', 'journey.mood.neutral': 'Нейтрально', diff --git a/shared/src/i18n/vi/journey.ts b/shared/src/i18n/vi/journey.ts index fbafd0a74..20eaaf909 100644 --- a/shared/src/i18n/vi/journey.ts +++ b/shared/src/i18n/vi/journey.ts @@ -126,6 +126,12 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': 'thứ nhất', 'journey.editor.makeFirst': 'làm số 1', 'journey.editor.searching': 'Đang tìm kiếm...', + 'journey.editor.useCurrentLocation': 'Dùng vị trí hiện tại của tôi', + 'journey.editor.locationPermissionDenied': + 'Quyền truy cập vị trí bị từ chối. Hãy cho phép trong cài đặt trình duyệt rồi thử lại.', + 'journey.editor.locationTimeout': 'Hết thời gian lấy vị trí. Hãy thử lại.', + 'journey.editor.locationUnavailable': 'Không thể xác định vị trí của bạn.', + 'journey.editor.locationInsecureContext': 'Vị trí yêu cầu kết nối an toàn (HTTPS).', 'journey.mood.amazing': 'Tuyệt vời', 'journey.mood.good': 'Tốt', 'journey.mood.neutral': 'Trung lập', diff --git a/shared/src/i18n/zh-TW/journey.ts b/shared/src/i18n/zh-TW/journey.ts index e1adffeb7..5622a99a5 100644 --- a/shared/src/i18n/zh-TW/journey.ts +++ b/shared/src/i18n/zh-TW/journey.ts @@ -125,6 +125,11 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '第1張', 'journey.editor.makeFirst': '設為第1張', 'journey.editor.searching': '搜尋中...', + 'journey.editor.useCurrentLocation': '使用我的目前位置', + 'journey.editor.locationPermissionDenied': '位置存取被拒絕。請在瀏覽器設定中允許後重試。', + 'journey.editor.locationTimeout': '取得位置逾時,請重試。', + 'journey.editor.locationUnavailable': '無法判斷您的位置。', + 'journey.editor.locationInsecureContext': '定位需要安全連線(HTTPS)。', 'journey.mood.amazing': '太棒了', 'journey.mood.good': '不錯', 'journey.mood.neutral': '一般', diff --git a/shared/src/i18n/zh/journey.ts b/shared/src/i18n/zh/journey.ts index ab1b12cfe..14b5bef8f 100644 --- a/shared/src/i18n/zh/journey.ts +++ b/shared/src/i18n/zh/journey.ts @@ -125,6 +125,11 @@ const journey: TranslationStrings = { 'journey.editor.photoFirst': '第1张', 'journey.editor.makeFirst': '设为第1张', 'journey.editor.searching': '搜索中...', + 'journey.editor.useCurrentLocation': '使用我的当前位置', + 'journey.editor.locationPermissionDenied': '位置访问被拒绝。请在浏览器设置中允许后重试。', + 'journey.editor.locationTimeout': '获取位置超时,请重试。', + 'journey.editor.locationUnavailable': '无法确定您的位置。', + 'journey.editor.locationInsecureContext': '定位需要安全连接(HTTPS)。', 'journey.mood.amazing': '太棒了', 'journey.mood.good': '不错', 'journey.mood.neutral': '一般',