diff --git a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx index ea7266aeb..3e8ed056c 100644 --- a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx +++ b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx @@ -1,14 +1,19 @@ -import React, { useMemo, useState } from 'react' -import { Fuel, Zap, ParkingSquare, Tent, Utensils, Camera, Search, Plus, RotateCw, AlertTriangle, type LucideIcon } from 'lucide-react' +import React from 'react' +import { + Fuel, Zap, ParkingSquare, Tent, Utensils, Camera, Search, Plus, RotateCw, + AlertTriangle, MapPin, type LucideIcon, +} from 'lucide-react' import { useTranslation } from '../../i18n/TranslationContext' import { useSettingsStore } from '../../store/settingsStore' import { formatDistance } from '../../utils/units' import CustomSelect from '../shared/CustomSelect' -import { useCorridorPois, type CorridorPoi } from './useCorridorPois' -import type { LatLng } from './corridor' +import { formatDurationShort } from './roadtripModel' +import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor' +import type { CorridorPoi } from './useCorridorPois' import type { RoadtripRoutes } from './useRoadtripRoutes' interface RoadtripCorridorPanelProps { + corridor: RoadtripCorridor routes: RoadtripRoutes /** Opens the place form prefilled from a POI, optionally on a day. */ onAddPoi?: ( @@ -17,39 +22,27 @@ interface RoadtripCorridorPanelProps { ) => void } -/** - * What a drive needs, rather than what a city visit does. Deliberately a short list: - * every extra category is another round of requests against a shared Overpass mirror. - */ -const CORRIDOR_CATEGORIES: { key: string; labelKey: string; Icon: LucideIcon }[] = [ - { key: 'fuel', labelKey: 'roadtrip.poi.fuel', Icon: Fuel }, - { key: 'charging', labelKey: 'roadtrip.poi.charging', Icon: Zap }, - { key: 'rest_area', labelKey: 'roadtrip.poi.rest', Icon: ParkingSquare }, - { key: 'campsite', labelKey: 'roadtrip.poi.campsite', Icon: Tent }, - { key: 'restaurant', labelKey: 'roadtrip.poi.food', Icon: Utensils }, - { key: 'sights', labelKey: 'roadtrip.poi.sights', Icon: Camera }, -] +const CATEGORY_META: Record = { + fuel: { labelKey: 'roadtrip.poi.fuel', Icon: Fuel }, + charging: { labelKey: 'roadtrip.poi.charging', Icon: Zap }, + rest_area: { labelKey: 'roadtrip.poi.rest', Icon: ParkingSquare }, + campsite: { labelKey: 'roadtrip.poi.campsite', Icon: Tent }, + restaurant: { labelKey: 'roadtrip.poi.food', Icon: Utensils }, + sights: { labelKey: 'roadtrip.poi.sights', Icon: Camera }, +} -const CATEGORY_ICON: Record = Object.fromEntries( - CORRIDOR_CATEGORIES.map(c => [c.key, c.Icon]), -) - -const WIDTHS_KM = [2, 5, 10] +const LABEL = 'text-caption font-medium text-content-faint uppercase tracking-wide' function ResultRow({ poi, onAdd }: { poi: CorridorPoi; onAdd?: () => void }): React.ReactElement { const { t } = useTranslation() const distanceUnit = useSettingsStore(s => s.settings.distance_unit) - const Icon = CATEGORY_ICON[poi.category] ?? Camera return ( -
- +
  • -
    {poi.name}
    +
    {poi.name}
    {t('roadtrip.poi.offRoute', { distance: formatDistance(poi.offRouteKm, distanceUnit) })} · - {/* A candidate beside the first stop projects to the very start of the line; - printing that as "after 0 m" reads like a bug rather than "right here". */} {poi.alongKm < 0.5 ? t('roadtrip.poi.atStart') : t('roadtrip.poi.alongRoute', { distance: formatDistance(poi.alongKm, distanceUnit) })} @@ -59,13 +52,39 @@ function ResultRow({ poi, onAdd }: { poi: CorridorPoi; onAdd?: () => void }): Re ) : null} -
    +
  • + ) +} + +/** One category's hits, so a mixed search reads as several short lists instead of one long one. */ +function ResultGroup({ category, pois, dayId, onAddPoi }: { + category: string + pois: CorridorPoi[] + dayId: number | null + onAddPoi: RoadtripCorridorPanelProps['onAddPoi'] +}): React.ReactElement { + const { t } = useTranslation() + const meta = CATEGORY_META[category] + const Icon = meta?.Icon ?? MapPin + return ( +
    +
    + + {meta ? t(meta.labelKey) : category} + {pois.length} +
    +
      + {pois.map(poi => ( + onAddPoi(poi, dayId) : undefined} /> + ))} +
    +
    ) } @@ -73,77 +92,100 @@ function ResultRow({ poi, onAdd }: { poi: CorridorPoi; onAdd?: () => void }): Re * "What is on the way" — the right column while road trip mode is on. * * Searching is a button, not a side effect of panning: one run is several Overpass - * requests and the mirrors are shared infrastructure. The scope is one day's drive by - * default, which keeps a run to a handful of boxes and makes "add this" unambiguous — - * the stop lands on the day whose route it was found along. + * requests and the mirrors are shared infrastructure. The scope is one day's drive, which + * keeps a run to a handful of boxes and makes "add this" unambiguous — the stop lands on + * the day whose route it was found along. Hits are drawn on the map at the same time, so + * the list answers "which of these" and the map answers "which side of the road". */ -export default function RoadtripCorridorPanel({ routes, onAddPoi }: RoadtripCorridorPanelProps): React.ReactElement { +export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: RoadtripCorridorPanelProps): React.ReactElement { const { t } = useTranslation() const distanceUnit = useSettingsStore(s => s.settings.distance_unit) - const [dayId, setDayId] = useState(() => String(routes.days[0]?.dayId ?? '')) - const [active, setActive] = useState(['fuel']) - const [widthKm, setWidthKm] = useState(5) - - const day = routes.days.find(d => String(d.dayId) === dayId) ?? routes.days[0] - const line = useMemo(() => { - if (!day) return [] - // The stops themselves are enough of a spine: the corridor is kilometres wide, and - // the routed geometry per leg is not kept once its numbers are read. - return day.stops.map(s => ({ lat: s.lat, lng: s.lng })) - }, [day]) - - const search = useCorridorPois(line, active, widthKm) + const { search } = corridor const dayOptions = routes.days.map(d => ({ value: String(d.dayId), label: t('roadtrip.day', { number: d.dayNumber }), })) - const toggle = (key: string): void => - setActive(prev => (prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key])) + const grouped = CORRIDOR_CATEGORY_KEYS + .map(key => ({ key, pois: search.results.filter(p => p.category === key) })) + .filter(g => g.pois.length > 0) + + const canSearch = !search.loading && corridor.categories.length > 0 && (corridor.day?.stops.length ?? 0) > 1 + const progressPct = search.progress.total + ? Math.round((search.progress.done / search.progress.total) * 100) + : 0 return ( -
    -
    +
    + {/* Header — the rhythm of the places sidebar this replaces. */} +

    {t('roadtrip.poi.title')}

    +

    {t('roadtrip.poi.hint')}

    + {corridor.day ? ( + // Under the title rather than beside it: the rail is narrow enough that a + // second element on the heading line pushes the heading into two. +

    + {formatDistance(corridor.day.distance / 1000, distanceUnit)} + · + {formatDurationShort(corridor.day.duration)} +

    + ) : null} +
    + {/* Search controls */} +
    {dayOptions.length > 1 ? ( - setDayId(String(value))} options={dayOptions} size="sm" /> +
    + {t('roadtrip.poi.scope')} +
    + corridor.setDayId(String(value))} + options={dayOptions} + size="sm" + /> +
    +
    ) : null} -
    - {CORRIDOR_CATEGORIES.map(({ key, labelKey, Icon }) => { - const on = active.includes(key) - return ( - - ) - })} +
    + {t('roadtrip.poi.looking')} +
    + {CORRIDOR_CATEGORY_KEYS.map(key => { + const { labelKey, Icon } = CATEGORY_META[key] + const on = corridor.categories.includes(key) + return ( + + ) + })} +
    - {t('roadtrip.poi.within')} -
    - {WIDTHS_KM.map(km => ( + {t('roadtrip.poi.within')} +
    + {CORRIDOR_WIDTHS_KM.map(km => ( + {search.loading ? ( +
    +
    +
    + ) : null} + {search.capped ? (

    @@ -178,17 +228,30 @@ export default function RoadtripCorridorPanel({ routes, onAddPoi }: RoadtripCorr ) : null}

    -
    - {search.results.length === 0 && !search.loading ? ( -

    {t('roadtrip.poi.empty')}

    + {/* Results */} +
    + {grouped.length === 0 ? ( +
    + +

    + {search.loading ? t('roadtrip.poi.searchingHint') : t('roadtrip.poi.empty')} +

    +
    ) : ( - search.results.map(poi => ( - onAddPoi(poi, day?.dayId ?? null) : undefined} - /> - )) + <> +

    + {t('roadtrip.poi.found', { count: search.results.length })} +

    + {grouped.map(g => ( + + ))} + )}
    diff --git a/client/src/components/Roadtrip/roadtripModel.test.ts b/client/src/components/Roadtrip/roadtripModel.test.ts index 51a8ebaeb..6170a8639 100644 --- a/client/src/components/Roadtrip/roadtripModel.test.ts +++ b/client/src/components/Roadtrip/roadtripModel.test.ts @@ -4,6 +4,7 @@ import { formatClock, formatDurationShort, parseClock, + splitIntoRuns, sumLegSeconds, } from './roadtripModel' @@ -176,6 +177,41 @@ describe('computeSchedule', () => { }) }) +describe('splitIntoRuns', () => { + const drive = (): string => 'driving' + + it('asks for a whole day travelled one way in a single run', () => { + const runs = splitIntoRuns(['a', 'b', 'c', 'd'], drive) + expect(runs).toEqual([{ stops: ['a', 'b', 'c', 'd'], mode: 'driving' }]) + }) + + it('splits where the travel mode changes, and repeats the stop on both sides', () => { + // Drive to b, walk to c, drive on to d: the walk is its own request, and b and c + // each belong to two runs because they are the ends of neighbouring legs. + const modes = ['driving', 'walking', 'driving'] + const runs = splitIntoRuns(['a', 'b', 'c', 'd'], (from) => modes[['a', 'b', 'c'].indexOf(from)]) + expect(runs).toEqual([ + { stops: ['a', 'b'], mode: 'driving' }, + { stops: ['b', 'c'], mode: 'walking' }, + { stops: ['c', 'd'], mode: 'driving' }, + ]) + }) + + it('keeps consecutive legs of the same mode together across a change and back', () => { + const modes = ['driving', 'driving', 'walking', 'walking'] + const runs = splitIntoRuns(['a', 'b', 'c', 'd', 'e'], (from) => modes[['a', 'b', 'c', 'd'].indexOf(from)]) + expect(runs).toEqual([ + { stops: ['a', 'b', 'c'], mode: 'driving' }, + { stops: ['c', 'd', 'e'], mode: 'walking' }, + ]) + }) + + it('has nothing to ask for when there is no leg', () => { + expect(splitIntoRuns([], drive)).toEqual([]) + expect(splitIntoRuns(['a'], drive)).toEqual([]) + }) +}) + describe('sumLegSeconds', () => { it('adds only the legs that routed', () => { expect(sumLegSeconds([600, undefined, 1200])).toBe(1800) diff --git a/client/src/components/Roadtrip/roadtripModel.ts b/client/src/components/Roadtrip/roadtripModel.ts index fe1a5762e..437527c83 100644 --- a/client/src/components/Roadtrip/roadtripModel.ts +++ b/client/src/components/Roadtrip/roadtripModel.ts @@ -126,6 +126,37 @@ export function computeSchedule(stops: ScheduleStop[], legSeconds: (number | und return { entries, warnings } } +/** + * Splits a day's stops into the stretches that can be asked for in one routing request: + * consecutive stops whose leg shares a travel mode. + * + * The router returns a leg for every consecutive pair of the waypoints it is handed, so + * a day travelled one way is a single request rather than one per leg — the difference + * between a road trip appearing at once and crawling in over ten seconds. A day that + * mixes walking and driving splits at the change, because one request carries one mode. + */ +export function splitIntoRuns(stops: T[], modeOfLeg: (from: T, to: T) => string): { stops: T[]; mode: string }[] { + const runs: { stops: T[]; mode: string }[] = [] + let current: T[] = [] + let currentMode: string | null = null + + for (let i = 0; i < stops.length - 1; i++) { + const mode = modeOfLeg(stops[i], stops[i + 1]) + if (currentMode === null) { + current = [stops[i], stops[i + 1]] + currentMode = mode + } else if (mode === currentMode) { + current.push(stops[i + 1]) + } else { + runs.push({ stops: current, mode: currentMode }) + current = [stops[i], stops[i + 1]] + currentMode = mode + } + } + if (currentMode !== null && current.length > 1) runs.push({ stops: current, mode: currentMode }) + return runs +} + /** Total driving seconds of the legs that actually routed. */ export function sumLegSeconds(legSeconds: (number | undefined)[]): number { return legSeconds.reduce((sum, s) => sum + (s ?? 0), 0) diff --git a/client/src/components/Roadtrip/useRoadtripCorridor.ts b/client/src/components/Roadtrip/useRoadtripCorridor.ts new file mode 100644 index 000000000..7c3debb70 --- /dev/null +++ b/client/src/components/Roadtrip/useRoadtripCorridor.ts @@ -0,0 +1,63 @@ +import { useCallback, useMemo, useState } from 'react' +import { useCorridorPois, type CorridorSearch } from './useCorridorPois' +import type { LatLng } from './corridor' +import type { RoadtripDay, RoadtripRoutes } from './useRoadtripRoutes' + +/** The road-trip categories, in the order the panel shows them. */ +export const CORRIDOR_CATEGORY_KEYS = ['fuel', 'charging', 'rest_area', 'campsite', 'restaurant', 'sights'] as const + +/** Corridor widths offered, in kilometres. */ +export const CORRIDOR_WIDTHS_KM = [2, 5, 10] + +export interface RoadtripCorridor { + /** Which day's drive is being searched. */ + dayId: string + setDayId: (value: string) => void + day: RoadtripDay | undefined + categories: string[] + toggleCategory: (key: string) => void + widthKm: number + setWidthKm: (km: number) => void + search: CorridorSearch +} + +/** + * The corridor search as trip state rather than panel state. + * + * The map has to draw what the search found — a list of petrol stations is only half an + * answer if you cannot see which one is on your side of the road — so the results have to + * live above both the panel and the map rather than inside the panel. + */ +export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor { + const [dayId, setDayId] = useState('') + const [categories, setCategories] = useState(['fuel']) + const [widthKm, setWidthKm] = useState(5) + + // Falls back to the first day with a drive, so the panel is useful before the user + // has picked anything — and follows along when that day disappears. + const day = routes.days.find(d => String(d.dayId) === dayId) ?? routes.days[0] + + const line = useMemo( + // The stops are spine enough: the corridor is kilometres wide, and the routed + // geometry per leg is not kept once its numbers have been read. + () => (day ? day.stops.map(s => ({ lat: s.lat, lng: s.lng })) : []), + [day], + ) + + const search = useCorridorPois(line, categories, widthKm) + + const toggleCategory = useCallback((key: string) => { + setCategories(prev => (prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key])) + }, []) + + return { + dayId: day ? String(day.dayId) : '', + setDayId, + day, + categories, + toggleCategory, + widthKm, + setWidthKm, + search, + } +} diff --git a/client/src/components/Roadtrip/useRoadtripRoutes.ts b/client/src/components/Roadtrip/useRoadtripRoutes.ts index 47928e207..9a5924b53 100644 --- a/client/src/components/Roadtrip/useRoadtripRoutes.ts +++ b/client/src/components/Roadtrip/useRoadtripRoutes.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { calculateRouteWithLegs } from '../Map/RouteCalculator' import { resolveLegMode } from '../Planner/legMode' -import { computeSchedule, type Schedule } from './roadtripModel' +import { computeSchedule, splitIntoRuns, type Schedule } from './roadtripModel' import { useSettingsStore } from '../../store/settingsStore' import type { Assignment, AssignmentsMap, Day, RouteSegment } from '../../types' @@ -54,6 +54,8 @@ export interface RoadtripRoutes { * the first handful answer and the rest come back 429. */ const REQUEST_SPACING_MS = 1100 +/** Anything answered faster than this came out of RouteCalculator cache, not the network. */ +const CACHE_HIT_MS = 60 /** How often a leg that failed (usually a rate limit) is tried again, and how long after. */ const RETRY_DELAYS_MS = [1500, 4000] @@ -95,10 +97,13 @@ const asStop = (a: Assignment): RoadtripStop | null => { * Distance and driving time for every leg of every day of the trip. * * The day plan sidebar routes one day at a time because that is all it shows; a road - * trip is the whole chain, which is exactly what #435 asks for. Legs are fetched - * pairwise so RouteCalculator's cache is shared with the sidebar — a day already drawn - * on the map costs nothing here — and the worker pool stays small because the routing - * host is usually someone else's OSRM. + * trip is the whole chain, which is exactly what #435 asks for. + * + * A day is one request, not one per leg: the router already returns a leg for every + * consecutive pair of the waypoints it is handed. Requests go out one after another + * with a gap, because the public routing hosts TREK ships with allow about one per + * second — but only the ones that actually reach the network are paced, so returning + * to this view costs nothing. */ export function useRoadtripRoutes( tripId: number | string | null, @@ -166,27 +171,33 @@ export function useRoadtripRoutes( const dayLegs: Record = {} collected[day.dayId] = dayLegs const dfMode = dayDefault(day.dayId) - for (let i = 0; i < day.stops.length - 1; i++) { - const from = day.stops[i] - const to = day.stops[i + 1] - const mode = resolveLegMode( + + const runs = splitIntoRuns(day.stops, (from, to) => + resolveLegMode( { isPlace: true, leg_transport_mode: from.legMode }, { isPlace: true, incoming_leg_transport_mode: to.incomingLegMode }, dfMode, - ) + )) + + for (const { stops: run, mode } of runs) { tasks.push(async () => { for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) { if (controller.signal.aborted) return try { const r = await calculateRouteWithLegs( - [{ lat: from.lat, lng: from.lng }, { lat: to.lat, lng: to.lng }], + run.map(s => ({ lat: s.lat, lng: s.lng })), { signal: controller.signal, profile: mode, tripId: tripId ?? null, dayId: day.dayId }, ) - if (r.legs[0]) dayLegs[from.assignmentId] = { seg: { ...r.legs[0], mode }, line: r.coordinates } + // legs[i] is the drive from run[i] to run[i+1]; the geometry comes back for + // the run as a whole, which is what the map draws. + r.legs.forEach((leg, i) => { + const from = run[i] + if (from) dayLegs[from.assignmentId] = { seg: { ...leg, mode }, line: i === 0 ? r.coordinates : [] } + }) return } catch { // Almost always a rate limit on the shared routing host. Back off and try - // again; a leg that still won't route (island hop, dead router) simply stays + // again; a run that still won't route (island hop, dead router) simply stays // blank, and the totals say so by being partial rather than wrong. const delay = RETRY_DELAYS_MS[attempt] if (delay === undefined) return @@ -202,12 +213,17 @@ export function useRoadtripRoutes( // public routing hosts answer that with 429 after the first handful. Results are // published as they land so the rail fills in instead of sitting empty. void (async () => { - for (const task of tasks) { + for (let i = 0; i < tasks.length; i++) { if (controller.signal.aborted) return - await task() + const startedAt = performance.now() + await tasks[i]() if (controller.signal.aborted) return setLegsByDay({ ...collected }) - await sleep(REQUEST_SPACING_MS, controller.signal) + // Only pace what actually went out. RouteCalculator answers a repeat from its + // cache in well under a millisecond, and switching back into road trip mode is + // all repeats — waiting a second between those made a warm view feel broken. + const wasNetwork = performance.now() - startedAt > CACHE_HIT_MS + if (wasNetwork && i < tasks.length - 1) await sleep(REQUEST_SPACING_MS, controller.signal) } if (!controller.signal.aborted) setLoading(false) })() diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index c55250439..ed5e0bc4d 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -246,7 +246,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null { selectedDayId, isLoading, tripActions, can, canUploadFiles, pushUndo, undo, canUndo, lastActionLabel, handleUndo, enabledAddons, collabFeatures, tripAccommodations, setTripAccommodations, - roadtripMode, toggleRoadtripMode, roadtripActive, roadtripRoutes, + roadtripMode, toggleRoadtripMode, roadtripActive, roadtripRoutes, roadtripCorridor, allowedFileTypes, tripMembers, setTripMembers, refreshMembers, loadAccommodations, TRANSPORT_TYPES, TRIP_TABS, activeTab, setActiveTab, handleTabChange, leftWidth, rightWidth, @@ -303,7 +303,9 @@ function TripPlannerPageDesktop(): React.ReactElement | null { const poi = usePoiExplore() const [glMap, setGlMap] = useState(null) - const poiPillEnabled = useSettingsStore(s => s.settings.map_poi_pill_enabled) !== false + // The corridor search draws into the same map channel and answers the same question for + // a drive, so the explore pill stands down while road trip mode is on. + const poiPillEnabled = useSettingsStore(s => s.settings.map_poi_pill_enabled) !== false && !roadtripActive // Costs expense editor opened from a booking modal (save-then-open). Lives at the // page level so it has tripMembers / base currency / current user available. @@ -405,7 +407,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null { const r = reservations.find(x => x.id === rid) if (r) setMapTransportDetail(r) }} - pois={poi.pois} + pois={roadtripActive ? roadtripCorridor.search.results : poi.pois} onPoiClick={openAddPlaceFromPoi} onViewportChange={poi.onViewportChange} onMapReady={setGlMap} @@ -588,7 +590,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
    {roadtripActive ? ( - + ) : ( { tripActions.setSelectedDay(dayId) @@ -1104,7 +1107,7 @@ export function useTripPlanner() { selectedDayId, isLoading, tripActions, can, canUploadFiles, pushUndo, undo, canUndo, lastActionLabel, handleUndo, enabledAddons, collabFeatures, tripAccommodations, setTripAccommodations, - roadtripMode, toggleRoadtripMode, roadtripActive, roadtripRoutes, + roadtripMode, toggleRoadtripMode, roadtripActive, roadtripRoutes, roadtripCorridor, allowedFileTypes, tripMembers, setTripMembers, refreshMembers, loadAccommodations, TRANSPORT_TYPES, TRIP_TABS, activeTab, setActiveTab, handleTabChange, leftWidth, rightWidth, leftCollapsed, rightCollapsed, setLeftCollapsed, setRightCollapsed, diff --git a/shared/src/i18n/ar/roadtrip.ts b/shared/src/i18n/ar/roadtrip.ts index ace357abe..809b0c9df 100644 --- a/shared/src/i18n/ar/roadtrip.ts +++ b/shared/src/i18n/ar/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'محطات على الطريق', 'roadtrip.stops.remove': 'إزالة المحطة', 'roadtrip.poi.title': 'على طول المسار', + 'roadtrip.poi.hint': 'ابحث على طول مسار اليوم عمّا تحتاجه في الطريق.', + 'roadtrip.poi.scope': 'اليوم', + 'roadtrip.poi.looking': 'المطلوب', + 'roadtrip.poi.searchingHint': 'جارٍ البحث على طول المسار…', + 'roadtrip.poi.found': '{count} على الطريق', 'roadtrip.poi.fuel': 'وقود', 'roadtrip.poi.charging': 'شحن', 'roadtrip.poi.rest': 'استراحة', diff --git a/shared/src/i18n/br/roadtrip.ts b/shared/src/i18n/br/roadtrip.ts index ca3bd237d..cc70bb216 100644 --- a/shared/src/i18n/br/roadtrip.ts +++ b/shared/src/i18n/br/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Paradas pelo caminho', 'roadtrip.stops.remove': 'Remover parada', 'roadtrip.poi.title': 'Ao longo da rota', + 'roadtrip.poi.hint': 'Pesquise ao longo do trajeto de um dia o que precisa no caminho.', + 'roadtrip.poi.scope': 'Dia', + 'roadtrip.poi.looking': 'Procurando por', + 'roadtrip.poi.searchingHint': 'Pesquisando a rota…', + 'roadtrip.poi.found': '{count} no caminho', 'roadtrip.poi.fuel': 'Combustível', 'roadtrip.poi.charging': 'Carregamento', 'roadtrip.poi.rest': 'Área de descanso', diff --git a/shared/src/i18n/ca/roadtrip.ts b/shared/src/i18n/ca/roadtrip.ts index c4fe4f8e4..68dd741eb 100644 --- a/shared/src/i18n/ca/roadtrip.ts +++ b/shared/src/i18n/ca/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Parades pel camí', 'roadtrip.stops.remove': 'Elimina la parada', 'roadtrip.poi.title': 'Al llarg de la ruta', + 'roadtrip.poi.hint': 'Cerca al llarg del trajecte d’un dia el que necessitis.', + 'roadtrip.poi.scope': 'Dia', + 'roadtrip.poi.looking': 'Es busca', + 'roadtrip.poi.searchingHint': 'S’està cercant la ruta…', + 'roadtrip.poi.found': '{count} pel camí', 'roadtrip.poi.fuel': 'Combustible', 'roadtrip.poi.charging': 'Càrrega', 'roadtrip.poi.rest': 'Àrea de descans', diff --git a/shared/src/i18n/cs/roadtrip.ts b/shared/src/i18n/cs/roadtrip.ts index d03a52ce7..e9d0220c9 100644 --- a/shared/src/i18n/cs/roadtrip.ts +++ b/shared/src/i18n/cs/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Zastávky po cestě', 'roadtrip.stops.remove': 'Odebrat zastávku', 'roadtrip.poi.title': 'Podél trasy', + 'roadtrip.poi.hint': 'Prohledejte denní jízdu podle toho, co po cestě potřebujete.', + 'roadtrip.poi.scope': 'Den', + 'roadtrip.poi.looking': 'Hledá se', + 'roadtrip.poi.searchingHint': 'Prohledávám trasu…', + 'roadtrip.poi.found': '{count} po cestě', 'roadtrip.poi.fuel': 'Palivo', 'roadtrip.poi.charging': 'Nabíjení', 'roadtrip.poi.rest': 'Odpočívadlo', diff --git a/shared/src/i18n/de/roadtrip.ts b/shared/src/i18n/de/roadtrip.ts index 3c6823e7f..406c410f6 100644 --- a/shared/src/i18n/de/roadtrip.ts +++ b/shared/src/i18n/de/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Stopps unterwegs', 'roadtrip.stops.remove': 'Stopp entfernen', 'roadtrip.poi.title': 'Entlang der Route', + 'roadtrip.poi.hint': 'Durchsuche die Fahrt eines Tages nach dem, was du unterwegs brauchst.', + 'roadtrip.poi.scope': 'Tag', + 'roadtrip.poi.looking': 'Gesucht wird', + 'roadtrip.poi.searchingHint': 'Die Route wird durchsucht…', + 'roadtrip.poi.found': '{count} unterwegs', 'roadtrip.poi.fuel': 'Tanken', 'roadtrip.poi.charging': 'Laden', 'roadtrip.poi.rest': 'Rastplatz', diff --git a/shared/src/i18n/en/roadtrip.ts b/shared/src/i18n/en/roadtrip.ts index 3bed15197..06fd3d964 100644 --- a/shared/src/i18n/en/roadtrip.ts +++ b/shared/src/i18n/en/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Stops along the way', 'roadtrip.stops.remove': 'Remove stop', 'roadtrip.poi.title': 'Along the route', + 'roadtrip.poi.hint': 'Search a day’s drive for what you need along it.', + 'roadtrip.poi.scope': 'Day', + 'roadtrip.poi.looking': 'Looking for', + 'roadtrip.poi.searchingHint': 'Searching the route…', + 'roadtrip.poi.found': '{count} on the way', 'roadtrip.poi.fuel': 'Fuel', 'roadtrip.poi.charging': 'Charging', 'roadtrip.poi.rest': 'Rest area', diff --git a/shared/src/i18n/es/roadtrip.ts b/shared/src/i18n/es/roadtrip.ts index 09040ee31..ba8a9a948 100644 --- a/shared/src/i18n/es/roadtrip.ts +++ b/shared/src/i18n/es/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Paradas por el camino', 'roadtrip.stops.remove': 'Eliminar parada', 'roadtrip.poi.title': 'A lo largo de la ruta', + 'roadtrip.poi.hint': 'Busca a lo largo del trayecto de un día lo que necesites.', + 'roadtrip.poi.scope': 'Día', + 'roadtrip.poi.looking': 'Se busca', + 'roadtrip.poi.searchingHint': 'Buscando por la ruta…', + 'roadtrip.poi.found': '{count} por el camino', 'roadtrip.poi.fuel': 'Gasolina', 'roadtrip.poi.charging': 'Recarga', 'roadtrip.poi.rest': 'Área de descanso', diff --git a/shared/src/i18n/fr/roadtrip.ts b/shared/src/i18n/fr/roadtrip.ts index e00f2241c..91d2e861c 100644 --- a/shared/src/i18n/fr/roadtrip.ts +++ b/shared/src/i18n/fr/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Étapes en chemin', 'roadtrip.stops.remove': 'Supprimer l’étape', 'roadtrip.poi.title': 'Le long de l’itinéraire', + 'roadtrip.poi.hint': 'Cherchez le long du trajet d’une journée ce dont vous avez besoin.', + 'roadtrip.poi.scope': 'Jour', + 'roadtrip.poi.looking': 'Recherche de', + 'roadtrip.poi.searchingHint': 'Recherche le long de l’itinéraire…', + 'roadtrip.poi.found': '{count} sur le trajet', 'roadtrip.poi.fuel': 'Carburant', 'roadtrip.poi.charging': 'Recharge', 'roadtrip.poi.rest': 'Aire de repos', diff --git a/shared/src/i18n/gr/roadtrip.ts b/shared/src/i18n/gr/roadtrip.ts index 6182bed4c..c87eb7c1a 100644 --- a/shared/src/i18n/gr/roadtrip.ts +++ b/shared/src/i18n/gr/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Στάσεις στη διαδρομή', 'roadtrip.stops.remove': 'Αφαίρεση στάσης', 'roadtrip.poi.title': 'Κατά μήκος της διαδρομής', + 'roadtrip.poi.hint': 'Αναζητήστε κατά μήκος της διαδρομής μιας ημέρας ό,τι χρειάζεστε.', + 'roadtrip.poi.scope': 'Ημέρα', + 'roadtrip.poi.looking': 'Αναζητείται', + 'roadtrip.poi.searchingHint': 'Αναζήτηση κατά μήκος της διαδρομής…', + 'roadtrip.poi.found': '{count} στη διαδρομή', 'roadtrip.poi.fuel': 'Καύσιμα', 'roadtrip.poi.charging': 'Φόρτιση', 'roadtrip.poi.rest': 'Χώρος ανάπαυσης', diff --git a/shared/src/i18n/hu/roadtrip.ts b/shared/src/i18n/hu/roadtrip.ts index fe0a8a9c0..9dd2d5555 100644 --- a/shared/src/i18n/hu/roadtrip.ts +++ b/shared/src/i18n/hu/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Megállók útközben', 'roadtrip.stops.remove': 'Megálló eltávolítása', 'roadtrip.poi.title': 'Az útvonal mentén', + 'roadtrip.poi.hint': 'Keresd meg egy nap útvonalán, amire útközben szükséged van.', + 'roadtrip.poi.scope': 'Nap', + 'roadtrip.poi.looking': 'Amit keresel', + 'roadtrip.poi.searchingHint': 'Az útvonal keresése…', + 'roadtrip.poi.found': '{count} az úton', 'roadtrip.poi.fuel': 'Üzemanyag', 'roadtrip.poi.charging': 'Töltés', 'roadtrip.poi.rest': 'Pihenőhely', diff --git a/shared/src/i18n/id/roadtrip.ts b/shared/src/i18n/id/roadtrip.ts index 932c31468..48a1f9792 100644 --- a/shared/src/i18n/id/roadtrip.ts +++ b/shared/src/i18n/id/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Perhentian di sepanjang jalan', 'roadtrip.stops.remove': 'Hapus perhentian', 'roadtrip.poi.title': 'Sepanjang rute', + 'roadtrip.poi.hint': 'Cari di sepanjang perjalanan satu hari apa yang Anda butuhkan.', + 'roadtrip.poi.scope': 'Hari', + 'roadtrip.poi.looking': 'Mencari', + 'roadtrip.poi.searchingHint': 'Mencari di sepanjang rute…', + 'roadtrip.poi.found': '{count} di jalan', 'roadtrip.poi.fuel': 'Bahan bakar', 'roadtrip.poi.charging': 'Pengisian daya', 'roadtrip.poi.rest': 'Tempat istirahat', diff --git a/shared/src/i18n/it/roadtrip.ts b/shared/src/i18n/it/roadtrip.ts index b4b5d7d20..fa71d952c 100644 --- a/shared/src/i18n/it/roadtrip.ts +++ b/shared/src/i18n/it/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Soste lungo il tragitto', 'roadtrip.stops.remove': 'Rimuovi sosta', 'roadtrip.poi.title': 'Lungo il percorso', + 'roadtrip.poi.hint': 'Cerca lungo il tragitto di una giornata ciò che ti serve.', + 'roadtrip.poi.scope': 'Giorno', + 'roadtrip.poi.looking': 'Sto cercando', + 'roadtrip.poi.searchingHint': 'Ricerca lungo il percorso…', + 'roadtrip.poi.found': '{count} lungo il tragitto', 'roadtrip.poi.fuel': 'Carburante', 'roadtrip.poi.charging': 'Ricarica', 'roadtrip.poi.rest': 'Area di sosta', diff --git a/shared/src/i18n/ja/roadtrip.ts b/shared/src/i18n/ja/roadtrip.ts index 564467713..2f8038df1 100644 --- a/shared/src/i18n/ja/roadtrip.ts +++ b/shared/src/i18n/ja/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': '途中の立ち寄り先', 'roadtrip.stops.remove': '立ち寄り先を削除', 'roadtrip.poi.title': 'ルート沿い', + 'roadtrip.poi.hint': 'その日のドライブ沿いに必要なものを探します。', + 'roadtrip.poi.scope': '日', + 'roadtrip.poi.looking': '探すもの', + 'roadtrip.poi.searchingHint': 'ルートを検索しています…', + 'roadtrip.poi.found': '道中に {count} 件', 'roadtrip.poi.fuel': '給油', 'roadtrip.poi.charging': '充電', 'roadtrip.poi.rest': 'サービスエリア', diff --git a/shared/src/i18n/ko/roadtrip.ts b/shared/src/i18n/ko/roadtrip.ts index bbe47d037..1cc5fe61f 100644 --- a/shared/src/i18n/ko/roadtrip.ts +++ b/shared/src/i18n/ko/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': '가는 길의 경유지', 'roadtrip.stops.remove': '경유지 삭제', 'roadtrip.poi.title': '경로 주변', + 'roadtrip.poi.hint': '하루 주행 경로를 따라 필요한 것을 찾습니다.', + 'roadtrip.poi.scope': '날짜', + 'roadtrip.poi.looking': '찾는 항목', + 'roadtrip.poi.searchingHint': '경로를 검색하는 중…', + 'roadtrip.poi.found': '경로에 {count}곳', 'roadtrip.poi.fuel': '주유', 'roadtrip.poi.charging': '충전', 'roadtrip.poi.rest': '휴게소', diff --git a/shared/src/i18n/nl/roadtrip.ts b/shared/src/i18n/nl/roadtrip.ts index 1c2822ff7..f298d5be8 100644 --- a/shared/src/i18n/nl/roadtrip.ts +++ b/shared/src/i18n/nl/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Stops onderweg', 'roadtrip.stops.remove': 'Stop verwijderen', 'roadtrip.poi.title': 'Langs de route', + 'roadtrip.poi.hint': 'Doorzoek de rit van een dag op wat je onderweg nodig hebt.', + 'roadtrip.poi.scope': 'Dag', + 'roadtrip.poi.looking': 'Gezocht wordt', + 'roadtrip.poi.searchingHint': 'De route wordt doorzocht…', + 'roadtrip.poi.found': '{count} onderweg', 'roadtrip.poi.fuel': 'Tanken', 'roadtrip.poi.charging': 'Laden', 'roadtrip.poi.rest': 'Rustplaats', diff --git a/shared/src/i18n/pl/roadtrip.ts b/shared/src/i18n/pl/roadtrip.ts index 8efaf8b75..abc886531 100644 --- a/shared/src/i18n/pl/roadtrip.ts +++ b/shared/src/i18n/pl/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Przystanki po drodze', 'roadtrip.stops.remove': 'Usuń przystanek', 'roadtrip.poi.title': 'Wzdłuż trasy', + 'roadtrip.poi.hint': 'Przeszukaj przejazd danego dnia pod kątem tego, czego potrzebujesz.', + 'roadtrip.poi.scope': 'Dzień', + 'roadtrip.poi.looking': 'Szukam', + 'roadtrip.poi.searchingHint': 'Przeszukiwanie trasy…', + 'roadtrip.poi.found': '{count} po drodze', 'roadtrip.poi.fuel': 'Paliwo', 'roadtrip.poi.charging': 'Ładowanie', 'roadtrip.poi.rest': 'Miejsce odpoczynku', diff --git a/shared/src/i18n/ru/roadtrip.ts b/shared/src/i18n/ru/roadtrip.ts index 8d11bb1af..5ae312afe 100644 --- a/shared/src/i18n/ru/roadtrip.ts +++ b/shared/src/i18n/ru/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Остановки в пути', 'roadtrip.stops.remove': 'Удалить остановку', 'roadtrip.poi.title': 'Вдоль маршрута', + 'roadtrip.poi.hint': 'Ищите вдоль дневного перегона то, что нужно в пути.', + 'roadtrip.poi.scope': 'День', + 'roadtrip.poi.looking': 'Ищем', + 'roadtrip.poi.searchingHint': 'Идёт поиск вдоль маршрута…', + 'roadtrip.poi.found': '{count} по пути', 'roadtrip.poi.fuel': 'Заправка', 'roadtrip.poi.charging': 'Зарядка', 'roadtrip.poi.rest': 'Зона отдыха', diff --git a/shared/src/i18n/sv/roadtrip.ts b/shared/src/i18n/sv/roadtrip.ts index 2ecf911f8..e3e49939b 100644 --- a/shared/src/i18n/sv/roadtrip.ts +++ b/shared/src/i18n/sv/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Stopp längs vägen', 'roadtrip.stops.remove': 'Ta bort stopp', 'roadtrip.poi.title': 'Längs rutten', + 'roadtrip.poi.hint': 'Sök längs en dags körning efter det du behöver på vägen.', + 'roadtrip.poi.scope': 'Dag', + 'roadtrip.poi.looking': 'Söker efter', + 'roadtrip.poi.searchingHint': 'Söker längs rutten…', + 'roadtrip.poi.found': '{count} längs vägen', 'roadtrip.poi.fuel': 'Bensin', 'roadtrip.poi.charging': 'Laddning', 'roadtrip.poi.rest': 'Rastplats', diff --git a/shared/src/i18n/tr/roadtrip.ts b/shared/src/i18n/tr/roadtrip.ts index 5776bd282..2fcb4aa50 100644 --- a/shared/src/i18n/tr/roadtrip.ts +++ b/shared/src/i18n/tr/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Yol üzerindeki duraklar', 'roadtrip.stops.remove': 'Durağı kaldır', 'roadtrip.poi.title': 'Rota boyunca', + 'roadtrip.poi.hint': 'Bir günün sürüşü boyunca yolda ihtiyacın olanı ara.', + 'roadtrip.poi.scope': 'Gün', + 'roadtrip.poi.looking': 'Aranan', + 'roadtrip.poi.searchingHint': 'Rota aranıyor…', + 'roadtrip.poi.found': 'yol üzerinde {count}', 'roadtrip.poi.fuel': 'Yakıt', 'roadtrip.poi.charging': 'Şarj', 'roadtrip.poi.rest': 'Dinlenme alanı', diff --git a/shared/src/i18n/uk/roadtrip.ts b/shared/src/i18n/uk/roadtrip.ts index db9c685f8..23918c9f5 100644 --- a/shared/src/i18n/uk/roadtrip.ts +++ b/shared/src/i18n/uk/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Зупинки в дорозі', 'roadtrip.stops.remove': 'Видалити зупинку', 'roadtrip.poi.title': 'Уздовж маршруту', + 'roadtrip.poi.hint': 'Шукайте вздовж денного перегону те, що потрібно в дорозі.', + 'roadtrip.poi.scope': 'День', + 'roadtrip.poi.looking': 'Шукаємо', + 'roadtrip.poi.searchingHint': 'Триває пошук уздовж маршруту…', + 'roadtrip.poi.found': '{count} у дорозі', 'roadtrip.poi.fuel': 'Заправка', 'roadtrip.poi.charging': 'Зарядка', 'roadtrip.poi.rest': 'Зона відпочинку', diff --git a/shared/src/i18n/vi/roadtrip.ts b/shared/src/i18n/vi/roadtrip.ts index 230788766..7ea51ccd3 100644 --- a/shared/src/i18n/vi/roadtrip.ts +++ b/shared/src/i18n/vi/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': 'Điểm dừng dọc đường', 'roadtrip.stops.remove': 'Xóa điểm dừng', 'roadtrip.poi.title': 'Dọc theo lộ trình', + 'roadtrip.poi.hint': 'Tìm dọc chặng đường trong ngày những thứ bạn cần.', + 'roadtrip.poi.scope': 'Ngày', + 'roadtrip.poi.looking': 'Đang tìm', + 'roadtrip.poi.searchingHint': 'Đang tìm dọc lộ trình…', + 'roadtrip.poi.found': '{count} trên đường', 'roadtrip.poi.fuel': 'Xăng dầu', 'roadtrip.poi.charging': 'Sạc điện', 'roadtrip.poi.rest': 'Trạm dừng nghỉ', diff --git a/shared/src/i18n/zh-TW/roadtrip.ts b/shared/src/i18n/zh-TW/roadtrip.ts index ebb977654..6c56b5dee 100644 --- a/shared/src/i18n/zh-TW/roadtrip.ts +++ b/shared/src/i18n/zh-TW/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': '沿途停靠點', 'roadtrip.stops.remove': '移除停靠點', 'roadtrip.poi.title': '沿線', + 'roadtrip.poi.hint': '沿某一天的行程搜尋路上需要的東西。', + 'roadtrip.poi.scope': '日期', + 'roadtrip.poi.looking': '尋找', + 'roadtrip.poi.searchingHint': '正在沿路線搜尋…', + 'roadtrip.poi.found': '沿途 {count} 處', 'roadtrip.poi.fuel': '加油', 'roadtrip.poi.charging': '充電', 'roadtrip.poi.rest': '休息站', diff --git a/shared/src/i18n/zh/roadtrip.ts b/shared/src/i18n/zh/roadtrip.ts index d68e04979..4b8b110bb 100644 --- a/shared/src/i18n/zh/roadtrip.ts +++ b/shared/src/i18n/zh/roadtrip.ts @@ -11,6 +11,11 @@ const roadtrip: TranslationStrings = { 'roadtrip.stops.label': '沿途停靠点', 'roadtrip.stops.remove': '移除停靠点', 'roadtrip.poi.title': '沿线', + 'roadtrip.poi.hint': '沿某一天的行程搜索路上需要的东西。', + 'roadtrip.poi.scope': '日期', + 'roadtrip.poi.looking': '查找', + 'roadtrip.poi.searchingHint': '正在沿路线搜索…', + 'roadtrip.poi.found': '沿途 {count} 处', 'roadtrip.poi.fuel': '加油', 'roadtrip.poi.charging': '充电', 'roadtrip.poi.rest': '服务区',