mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Add "use my current location" button to Journey entry editor
- One-shot getCurrentPositionOnce() helper in useGeolocation with typed error codes (unsupported, insecure context, permission denied, timeout) - Button inside the entry editor Location field fills coordinates from the device and reverse-geocodes the place name via /api/maps/reverse, falling back to formatted coordinates when no name is available - New journey.editor.* i18n keys in all 23 locales - Unit tests for the helper and component tests for the button flow
This commit is contained in:
@@ -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<void>
|
||||
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<ReturnType<typeof setTimeout> | 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 (
|
||||
<div className="fixed inset-0 z-[9999]" style={{ background: 'rgba(9,9,11,0.6)', backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)' }}>
|
||||
{/* 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUseCurrentLocation}
|
||||
disabled={locating}
|
||||
title={t('journey.editor.useCurrentLocation')}
|
||||
aria-label={t('journey.editor.useCurrentLocation')}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 p-1.5 rounded-md text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 disabled:opacity-50"
|
||||
>
|
||||
{locating
|
||||
? <div className="w-3.5 h-3.5 border-2 border-zinc-300 border-t-zinc-600 rounded-full animate-spin" />
|
||||
: <Locate size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
{showLocationResults && locationResults.length > 0 && (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<GeoPosition> {
|
||||
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<GeoPosition | null>(null)
|
||||
const [mode, setModeState] = useState<TrackingMode>('off')
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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': 'الدور',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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í',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Ουδέτερη',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '普通',
|
||||
|
||||
@@ -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': '보통',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Нейтрально',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Нейтрально',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '一般',
|
||||
|
||||
@@ -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': '一般',
|
||||
|
||||
Reference in New Issue
Block a user