feat(roadtrip): turn a place into a stop on the way from its own number

An ordinary place could not become a fuel stop without going through the full
form, and the number in the rail is exactly what changes when it does: a service
stop has none. So the number is the control. Click the 3, pick the pump, and the
3 becomes an orange disc while everything below renumbers itself. The disc of a
service stop opens the same picker the other way, with a row to make it an
ordinary destination again.

Six coloured discs rather than six lines of text: at that size the colour and
the shape are the label, and anyone who has seen one petrol stop on the map
knows the orange pump before reading anything.

Centred on the column rather than on the number, which sits hard against the
left edge and would hang the panel half over the map. The click stops at the
disc, so picking a kind never also selects the place underneath.

Also in this change, from the same round: the corridor search offers hotels, a
hit somebody could sleep at opens the popup in an overnight mode with a check-out
day instead of a dwell, and booking one writes the place, its day and the row in
day_accommodations together. A hotel keeps its number, because sleeping there
ends the day rather than interrupting the drive, which is why the category list
and the stop-kind list are now two lists instead of one.
This commit is contained in:
Maurice
2026-09-04 23:14:35 +02:00
committed by Maurice
parent 49f97af3fb
commit fc74991a15
28 changed files with 392 additions and 19 deletions
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState } from 'react'
import {
MapPin, CarFront, Footprints, Bike, Zap, AlertTriangle, Moon,
ParkingSquare, Shuffle, Fuel, Clock,
@@ -12,6 +12,8 @@ import { formatDate, formatClockTime } from '../../utils/formatters'
import { formatDurationShort, isServiceStopType, serviceColor, type ScheduleEntry, type ScheduleWarning } from './roadtripModel'
import { STOP_KIND_BY_KEY } from './stopKinds'
import { spurWorthLabelling } from './accessSpur'
import StopKindPicker from './StopKindPicker'
import type { RoadtripStopType } from '@trek/shared'
import type { QuietDay, RoadtripDay, RoadtripRoutes, RoadtripStop } from './useRoadtripRoutes'
import type { RouteVia } from '../../types'
import { FS } from './typeScale'
@@ -38,6 +40,12 @@ interface RoadtripSidebarProps {
* which is also what a viewer sees.
*/
onEditStay?: (stop: { placeId: number; name: string; minutes: number | null; arrival: string | null }) => void
/**
* Turns a stop into a pause on the drive, or back into a destination.
*
* Absent leaves every disc read-only, which is also what a viewer sees.
*/
onSetStopKind?: (placeId: number, kind: RoadtripStopType | null) => Promise<void> | void
}
const MODE_ICON: Record<string, LucideIcon> = {
@@ -352,12 +360,14 @@ function DriveBand({ leg, onAskAlternatives, alternativesOpen }: {
* the trip is actually for. Its own icon on one flat disc: three kinds of pause that all
* mean "we are still driving", and the icon is what tells them apart.
*/
function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditStay }: {
function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditStay, onPickKind }: {
stop: RoadtripStop
entry: ScheduleEntry | undefined
/** Findings about the drive that ARRIVES here. A charging halt is a stop like any other
* as far as the tank is concerned, so it carries them the same way a numbered one does. */
driveFindings?: ScheduleWarning[]
/** Opens the kind picker on the disc. Absent leaves the rail read-only. */
onPickKind?: (anchor: HTMLElement) => void
selected: boolean
onSelect?: () => void
/** Opens the dialog for how long this pause takes. Absent means the rail is read-only. */
@@ -371,15 +381,39 @@ function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditSta
<div className="grid items-stretch" style={RAIL_GRID}>
<span className="relative z-[1] flex flex-col items-center">
<span className="flex-1" style={RAIL_DASH} aria-hidden />
<span
className={DISC}
// theme-lint-disable — the road-signage palette in `roadtripModel`, shared with
// the corridor list and the map pin so one kind of stop looks like itself
// wherever it turns up.
style={{ background: serviceColor(stop.stopType), color: '#fff' }}
>
<Icon size={12} strokeWidth={2.1} aria-label={label} />
</span>
{onPickKind ? (
// The same control the other way round: the disc says what this is, and it is
// also where it stops being that.
<Tooltip label={t('roadtrip.stop.changeKind')}>
<span
role="button"
tabIndex={0}
aria-label={t('roadtrip.stop.changeKind')}
onClick={e => { e.stopPropagation(); onPickKind(e.currentTarget as HTMLElement) }}
onKeyDown={e => {
if (e.key !== 'Enter' && e.key !== ' ') return
e.preventDefault()
e.stopPropagation()
onPickKind(e.currentTarget as HTMLElement)
}}
className={`${DISC} cursor-pointer transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2`}
// theme-lint-disable — the road-signage palette in `roadtripModel`, shared
// with the corridor list and the map pin so one kind of stop looks like
// itself wherever it turns up.
style={{ background: serviceColor(stop.stopType), color: '#fff' }}
>
<Icon size={12} strokeWidth={2.1} aria-hidden />
</span>
</Tooltip>
) : (
<span
className={DISC}
// theme-lint-disable — same palette, read-only.
style={{ background: serviceColor(stop.stopType), color: '#fff' }}
>
<Icon size={12} strokeWidth={2.1} aria-label={label} />
</span>
)}
<span className="flex-1" style={RAIL_DASH} aria-hidden />
</span>
<button
@@ -576,7 +610,7 @@ function Arrival({ entry }: { entry: ScheduleEntry }): React.ReactElement {
)
}
function Stop({ stop, number, entry, late, driveFindings, selected, continues, starts, onSelect, onMove, canMove, onEditStay }: {
function Stop({ stop, number, entry, late, driveFindings, selected, continues, starts, onSelect, onMove, canMove, onEditStay, onPickKind }: {
stop: RoadtripStop
/** Position within the day — the same count the map badges its markers with. */
number: number
@@ -589,6 +623,8 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
continues: boolean
/** First row of the day: no line above it, because the chain starts here. */
starts?: boolean
/** Opens the kind picker on the number. Absent leaves the rail read-only. */
onPickKind?: (anchor: HTMLElement) => void
onSelect?: () => void
/** Moves this stop by one place. Absent means the chain is read-only. */
onMove?: (delta: number) => void
@@ -623,12 +659,36 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
unbroken from stop to stop. */}
<span className="flex flex-col items-center">
{starts ? null : <span className="w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden />}
<span
className={`${DISC} my-1 bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary`}
style={{ fontSize: FS.marker }}
>
{number}
</span>
{onPickKind ? (
// The number is the control, because the number is what changes: a service stop
// has none. Clicking the 3 and picking the pump turns the 3 into an orange disc
// and renumbers everything below it.
<Tooltip label={t('roadtrip.stop.makeService')}>
<span
role="button"
tabIndex={0}
aria-label={t('roadtrip.stop.makeService')}
onClick={e => { e.stopPropagation(); onPickKind(e.currentTarget as HTMLElement) }}
onKeyDown={e => {
if (e.key !== 'Enter' && e.key !== ' ') return
e.preventDefault()
e.stopPropagation()
onPickKind(e.currentTarget as HTMLElement)
}}
className={`${DISC} my-1 cursor-pointer bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary transition-colors hover:bg-accent hover:text-accent-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`}
style={{ fontSize: FS.marker }}
>
{number}
</span>
</Tooltip>
) : (
<span
className={`${DISC} my-1 bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary`}
style={{ fontSize: FS.marker }}
>
{number}
</span>
)}
{continues ? <span className="w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden /> : null}
</span>
@@ -685,7 +745,7 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
* (`dayOrderMap` numbers the selected day's assignments from 1). A rail counting across
* the trip would put "17" beside a pin the map calls "3".
*/
function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, drag, onAskAlternatives, openAlternatives, onEditStay }: {
function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, drag, onAskAlternatives, openAlternatives, onEditStay, onSetStopKind }: {
day: RoadtripDay
selectedAssignmentId?: number | null
onSelectStop?: (placeId: number, assignmentId: number) => void
@@ -696,9 +756,13 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
onAskAlternatives?: RoadtripSidebarProps['onAskAlternatives']
openAlternatives?: RoadtripSidebarProps['openAlternatives']
onEditStay?: RoadtripSidebarProps['onEditStay']
onSetStopKind?: RoadtripSidebarProps['onSetStopKind']
}): React.ReactElement {
const { from, setFrom, dropAt, setDropAt } = drag
const dragging = from?.dayId === day.dayId ? from.index : null
// Which disc the picker hangs under, and for which stop. One at a time: two open
// popovers over the same rail is two answers to one question.
const [picking, setPicking] = useState<{ anchor: HTMLElement; stop: RoadtripStop } | null>(null)
const { t, language } = useTranslation()
const distanceUnit = useSettingsStore(s => s.settings.distance_unit)
const last = day.stops.length - 1
@@ -806,6 +870,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
selected={selectedAssignmentId === stop.assignmentId}
onSelect={onSelectStop ? () => onSelectStop(stop.placeId, stop.assignmentId) : undefined}
onEditStay={onEditStay ? () => onEditStay({ placeId: stop.placeId, name: stop.name, minutes: stop.dwellMinutes, arrival: day.schedule.entries[i]?.arrival ?? null }) : undefined}
onPickKind={onSetStopKind ? anchor => setPicking({ anchor, stop }) : undefined}
/>
) : (
<Stop
@@ -821,6 +886,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
onMove={onReorderStop ? delta => onReorderStop(day.dayId, stop.assignmentId, i + delta) : undefined}
canMove={{ up: i > 0, down: i < last }}
onEditStay={onEditStay ? () => onEditStay({ placeId: stop.placeId, name: stop.name, minutes: stop.dwellMinutes, arrival: day.schedule.entries[i]?.arrival ?? null }) : undefined}
onPickKind={onSetStopKind ? anchor => setPicking({ anchor, stop }) : undefined}
/>
)}
{i < last ? (
@@ -839,6 +905,17 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
)
})}
</ol>
{picking ? (
<StopKindPicker
anchor={picking.anchor}
current={picking.stop.stopType}
onClose={() => setPicking(null)}
onPick={kind => {
setPicking(null)
void onSetStopKind?.(picking.stop.placeId, kind)
}}
/>
) : null}
</section>
)
}
@@ -917,6 +994,7 @@ function QuietDaySection({ day, onMoveStopToDay, drag }: {
*/
export default function RoadtripSidebar({
routes, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, onAskAlternatives, openAlternatives, onEditStay,
onSetStopKind,
}: RoadtripSidebarProps): React.ReactElement {
const { t } = useTranslation()
// One drag state for the whole rail rather than one per day: a stop that cannot leave
@@ -958,6 +1036,7 @@ export default function RoadtripSidebar({
onAskAlternatives={onAskAlternatives}
openAlternatives={openAlternatives}
onEditStay={onEditStay}
onSetStopKind={onSetStopKind}
/>
))}
{routes.quietDays.map(day => (
@@ -168,4 +168,78 @@ describe('RoadtripStopPopup', () => {
await act(async () => { release() })
expect(add).not.toBeDisabled()
})
// ── Overnight mode ─────────────────────────────────────────────────────────
//
// Somewhere to sleep is the one hit that ends the day rather than interrupting the
// drive, so the dialog grows a second mode instead of offering a hotel a dwell of
// thirty minutes.
const overnight = {
days: [
{ id: 4, number: 1, date: '2026-08-31' },
{ id: 5, number: 2, date: '2026-09-01' },
{ id: 6, number: 3, date: '2026-09-02' },
],
defaultEndDayId: 5,
}
const nightDraft = (category: string) => draft({
poi: poi({ category, name: 'Hotel Adlon' }),
overnight,
})
it('FE-ROADTRIP-STOPPOPUP-020: a hotel opens on the overnight mode', () => {
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} onSaveNight={vi.fn()} />)
expect(screen.getByRole('button', { name: 'Overnight' })).toHaveAttribute('aria-pressed', 'true')
expect(screen.getByText('Check out on')).toBeInTheDocument()
// The two questions a pause asks are gone: a hotel has no kind and no dwell.
expect(screen.queryByText('Kind of stop')).not.toBeInTheDocument()
})
it('FE-ROADTRIP-STOPPOPUP-021: a campsite opens on the pause mode but offers the night', () => {
// It is a service stop today and stays one, but people do sleep at campsites, so the
// choice is offered rather than decided.
wrap(<RoadtripStopPopup draft={nightDraft('campsite')} {...noop} onSaveNight={vi.fn()} />)
expect(screen.getByRole('button', { name: 'Pause' })).toHaveAttribute('aria-pressed', 'true')
expect(screen.getByText('Kind of stop')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Overnight' })).toBeInTheDocument()
})
it('FE-ROADTRIP-STOPPOPUP-022: a petrol station is never offered a night', () => {
// Nothing to choose between, so no switch at all.
wrap(<RoadtripStopPopup draft={draft()} {...noop} onSaveNight={vi.fn()} />)
expect(screen.queryByRole('button', { name: 'Overnight' })).not.toBeInTheDocument()
})
it('FE-ROADTRIP-STOPPOPUP-023: booking a night reports the check-out day, defaulting to the next', () => {
const onSaveNight = vi.fn()
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} onSaveNight={onSaveNight} />)
fireEvent.click(screen.getByRole('button', { name: 'Add' }))
expect(onSaveNight).toHaveBeenCalledWith({ endDayId: 5, checkIn: '', checkOut: '' })
})
it('FE-ROADTRIP-STOPPOPUP-024: switching back to a pause saves a pause', () => {
const onSave = vi.fn()
const onSaveNight = vi.fn()
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} onSave={onSave} onSaveNight={onSaveNight} />)
fireEvent.click(screen.getByRole('button', { name: 'Pause' }))
fireEvent.click(screen.getByRole('button', { name: 'Add' }))
expect(onSaveNight).not.toHaveBeenCalled()
expect(onSave).toHaveBeenCalled()
})
it('FE-ROADTRIP-STOPPOPUP-025: without a way to book a night the switch stays away', () => {
// An onSaveNight-less caller gets the dialog it always had, rather than a mode that
// leads nowhere.
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} />)
expect(screen.queryByRole('button', { name: 'Overnight' })).not.toBeInTheDocument()
expect(screen.getByText('Kind of stop')).toBeInTheDocument()
})
})
@@ -0,0 +1,133 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { MapPin } from 'lucide-react'
import { useTranslation } from '../../i18n/TranslationContext'
import { Tooltip } from '../shared/Tooltip'
import { STOP_KINDS } from './stopKinds'
import type { RoadtripStopType } from '@trek/shared'
/**
* Turning a place on the drive into a pause, and back.
*
* Opened from the stop's own number, because the number is exactly what changes: a
* service stop has none. That makes the control its own preview — click the 3, pick the
* pump, and the 3 becomes an orange disc while everything below renumbers itself.
*
* The kinds are the discs the rail already draws rather than a list of words. At this
* size the colour and the shape are the label: a traveller who has seen one petrol stop
* on the map knows the orange pump before reading anything, and six rows of text for six
* icons would be a menu where a palette does.
*
* Only for stops on a drive. A place with no coordinates never reaches the rail, and a
* hotel is not offered here at all, because sleeping somewhere is not a pause in the
* driving — that path goes through the accommodation it books.
*/
const DISC = 'grid place-items-center rounded-full transition-transform'
export default function StopKindPicker({ anchor, current, onPick, onClose }: {
/** The element the popover hangs under, usually the stop's number. */
anchor: HTMLElement | null
current: string | null
onPick: (kind: RoadtripStopType | null) => void
onClose: () => void
}): React.ReactElement | null {
const { t } = useTranslation()
const ref = useRef<HTMLDivElement>(null)
const [pos, setPos] = useState<{ top: number; left: number } | null>(null)
// Measured after paint, before the browser shows it: reading the size first and then
// placing it is what stops the panel appearing at 0,0 for one frame.
useLayoutEffect(() => {
if (!anchor || !ref.current) return
const a = anchor.getBoundingClientRect()
const p = ref.current.getBoundingClientRect()
const gap = 8
let top = a.bottom + gap
// Flips above when there is no room below, which is most of the rail on a short
// window: a panel clipped by the viewport is one that cannot be used at all.
if (top + p.height > window.innerHeight - 8) top = Math.max(8, a.top - p.height - gap)
// Centred on the column, not on the number. The number sits hard against the left
// edge of the rail, so a panel centred on it hangs half off the sidebar and points at
// the map. Vertically it still follows the row it belongs to.
const column = anchor.closest('section')?.getBoundingClientRect() ?? a
const left = Math.min(
Math.max(8, column.left + column.width / 2 - p.width / 2),
window.innerWidth - p.width - 8,
)
setPos({ top, left })
}, [anchor])
useEffect(() => {
if (!anchor) return
const onDown = (e: MouseEvent) => {
if (ref.current?.contains(e.target as Node)) return
if (anchor.contains(e.target as Node)) return
onClose()
}
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
document.addEventListener('mousedown', onDown)
document.addEventListener('keydown', onKey)
// Any scroll moves the anchor out from under the panel, and following it would mean
// measuring on every frame for a menu that is open for two seconds.
window.addEventListener('scroll', onClose, true)
return () => {
document.removeEventListener('mousedown', onDown)
document.removeEventListener('keydown', onKey)
window.removeEventListener('scroll', onClose, true)
}
}, [anchor, onClose])
if (!anchor) return null
return createPortal(
<div
ref={ref}
role="dialog"
aria-label={t('roadtrip.stop.kind')}
className="fixed z-[70] rounded-2xl border border-edge bg-surface-card p-2 shadow-xl"
style={{ top: pos?.top ?? -9999, left: pos?.left ?? -9999, visibility: pos ? 'visible' : 'hidden' }}
>
<div className="flex gap-1">
{STOP_KINDS.map(({ key, labelKey, Icon, color }) => {
const on = current === key
return (
<Tooltip key={key} label={t(labelKey)}>
<button
type="button"
aria-pressed={on}
aria-label={t(labelKey)}
onClick={() => onPick(on ? null : key)}
className="group grid h-11 w-11 place-items-center rounded-xl transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
<span
className={`${DISC} h-7 w-7 group-hover:scale-110 ${on ? 'ring-2 ring-offset-2 ring-offset-surface-card' : ''}`}
// theme-lint-disable — the road-signage palette from `roadtripModel`, the
// same one the rail disc and the map pin use, so the choice looks like
// what it will become.
style={{ background: color, color: '#fff', ...(on ? { boxShadow: `0 0 0 2px ${color}` } : {}) }}
>
<Icon size={14} strokeWidth={2.2} aria-hidden />
</span>
</button>
</Tooltip>
)
})}
</div>
{/* Only once there is something to undo. On an ordinary place it would be a button
that says "leave everything as it is". */}
{current ? (
<button
type="button"
onClick={() => onPick(null)}
className="mt-1 flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-caption text-content-secondary transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
<MapPin size={14} className="shrink-0 text-content-faint" aria-hidden />
{t('roadtrip.stop.backToDestination')}
</button>
) : null}
</div>,
document.body,
)
}
+2
View File
@@ -261,6 +261,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
showPlaceForm, setShowPlaceForm, editingPlace, setEditingPlace,
prefillCoords, setPrefillCoords, editingAssignmentId, setEditingAssignmentId,
stopDraft, setStopDraft, saveStopDraft, saveStopDraftAsNight, stopDraftToForm, stopDraftDuplicate, reorderRoadtripStop,
setRoadtripStopKind,
saveRoadtripLimit,
roadtripVias, addRoadtripVia, moveRoadtripVia, removeRoadtripVia,
routeAlternatives, askRouteAlternatives, chooseRouteAlternative, alternativeOverlays, alternativeFocusPoints,
@@ -536,6 +537,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
onAskAlternatives={can('day_edit', trip) ? askRouteAlternatives : undefined}
openAlternatives={routeAlternatives.open}
onEditStay={can('place_edit', trip) ? setStayDraft : undefined}
onSetStopKind={can('place_edit', trip) ? setRoadtripStopKind : undefined}
/>
</LazyPanel>
) : (
@@ -859,6 +859,21 @@ export function useTripPlanner() {
}
}, [stopDraft, tripId, tripActions, updateRouteForDay, toast, t, roadtripVias, viaLiesBefore, loadAccommodations])
/**
* Turns a stop on the drive into a pause, or back into a destination.
*
* The only difference between the two is `stop_type`, which decides whether the stop
* takes a number, counts in the day's total and appears in the printout. So this is one
* field on one place, and the rail redraws itself off the store the moment it lands.
*/
const setRoadtripStopKind = useCallback(async (placeId: number, kind: RoadtripStopType | null) => {
try {
await tripActions.updatePlace(tripId, placeId, { stop_type: kind })
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : t('common.unknownError'))
}
}, [tripId, tripActions, toast, t])
/**
* Moves a stop within its day, from the road trip rail.
*
@@ -1709,6 +1724,7 @@ export function useTripPlanner() {
prefillCoords, setPrefillCoords, editingAssignmentId, setEditingAssignmentId,
placeFormDayId, setPlaceFormDayId, reservationModalDayId, setReservationModalDayId,
stopDraft, setStopDraft, saveStopDraft, saveStopDraftAsNight, stopDraftToForm, stopDraftDuplicate, reorderRoadtripStop,
setRoadtripStopKind,
saveRoadtripLimit,
roadtripVias, addRoadtripVia, moveRoadtripVia, removeRoadtripVia,
routeAlternatives, askRouteAlternatives, chooseRouteAlternative, alternativeOverlays, alternativeFocusPoints,
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'اليوم {number}',
'roadtrip.stay.nightAdded': 'أُضيف كمبيت',
'roadtrip.stay.nightAction': 'إضافة كمبيت',
'roadtrip.stop.makeService': 'اجعله توقفاً في الطريق',
'roadtrip.stop.changeKind': 'تغيير نوع التوقف',
'roadtrip.stop.backToDestination': 'العودة إلى وجهة',
'roadtrip.poi.rest': 'استراحة',
'roadtrip.poi.campsite': 'مخيم',
'roadtrip.poi.food': 'طعام',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Dia {number}',
'roadtrip.stay.nightAdded': 'Adicionado como pernoite',
'roadtrip.stay.nightAction': 'Adicionar como pernoite',
'roadtrip.stop.makeService': 'Tornar uma parada no caminho',
'roadtrip.stop.changeKind': 'Alterar o tipo de parada',
'roadtrip.stop.backToDestination': 'Voltar a ser um destino',
'roadtrip.poi.rest': 'Área de descanso',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Comida',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Dia {number}',
'roadtrip.stay.nightAdded': 'Afegit com a pernoctació',
'roadtrip.stay.nightAction': 'Afegeix com a pernoctació',
'roadtrip.stop.makeService': 'Converteix en parada del camí',
'roadtrip.stop.changeKind': 'Canvia el tipus de parada',
'roadtrip.stop.backToDestination': 'Torna a ser una destinació',
'roadtrip.poi.rest': 'Àrea de descans',
'roadtrip.poi.campsite': 'Càmping',
'roadtrip.poi.food': 'Menjar',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Den {number}',
'roadtrip.stay.nightAdded': 'Přidáno jako nocleh',
'roadtrip.stay.nightAction': 'Přidat jako nocleh',
'roadtrip.stop.makeService': 'Změnit na zastávku po cestě',
'roadtrip.stop.changeKind': 'Změnit typ zastávky',
'roadtrip.stop.backToDestination': 'Zpět na cíl cesty',
'roadtrip.poi.rest': 'Odpočívadlo',
'roadtrip.poi.campsite': 'Kemp',
'roadtrip.poi.food': 'Jídlo',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Tag {number}',
'roadtrip.stay.nightAdded': 'Als Übernachtung hinzugefügt',
'roadtrip.stay.nightAction': 'Als Übernachtung hinzufügen',
'roadtrip.stop.makeService': 'Zum Halt unterwegs machen',
'roadtrip.stop.changeKind': 'Art des Halts ändern',
'roadtrip.stop.backToDestination': 'Wieder ein Ziel',
'roadtrip.poi.rest': 'Rastplatz',
'roadtrip.poi.campsite': 'Campingplatz',
'roadtrip.poi.food': 'Essen',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Day {number}',
'roadtrip.stay.nightAdded': 'Added as an overnight stay',
'roadtrip.stay.nightAction': 'Add as an overnight stay',
'roadtrip.stop.makeService': 'Make it a stop on the way',
'roadtrip.stop.changeKind': 'Change what kind of stop this is',
'roadtrip.stop.backToDestination': 'Back to a destination',
'roadtrip.poi.rest': 'Rest area',
'roadtrip.poi.campsite': 'Campsite',
'roadtrip.poi.food': 'Food',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Día {number}',
'roadtrip.stay.nightAdded': 'Añadido como pernoctación',
'roadtrip.stay.nightAction': 'Añadir como pernoctación',
'roadtrip.stop.makeService': 'Convertir en parada del camino',
'roadtrip.stop.changeKind': 'Cambiar el tipo de parada',
'roadtrip.stop.backToDestination': 'Volver a ser un destino',
'roadtrip.poi.rest': 'Área de descanso',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Comida',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Jour {number}',
'roadtrip.stay.nightAdded': 'Ajouté comme nuitée',
'roadtrip.stay.nightAction': 'Ajouter comme nuitée',
'roadtrip.stop.makeService': 'En faire un arrêt en chemin',
'roadtrip.stop.changeKind': 'Changer le type d’arrêt',
'roadtrip.stop.backToDestination': 'Redevenir une destination',
'roadtrip.poi.rest': 'Aire de repos',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Restauration',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Ημέρα {number}',
'roadtrip.stay.nightAdded': 'Προστέθηκε ως διανυκτέρευση',
'roadtrip.stay.nightAction': 'Προσθήκη ως διανυκτέρευση',
'roadtrip.stop.makeService': 'Μετατροπή σε στάση στον δρόμο',
'roadtrip.stop.changeKind': 'Αλλαγή είδους στάσης',
'roadtrip.stop.backToDestination': 'Πίσω σε προορισμό',
'roadtrip.poi.rest': 'Χώρος ανάπαυσης',
'roadtrip.poi.campsite': 'Κάμπινγκ',
'roadtrip.poi.food': 'Φαγητό',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': '{number}. nap',
'roadtrip.stay.nightAdded': 'Hozzáadva éjszakázásként',
'roadtrip.stay.nightAction': 'Hozzáadás éjszakázásként',
'roadtrip.stop.makeService': 'Legyen útközbeni megálló',
'roadtrip.stop.changeKind': 'Megálló típusának módosítása',
'roadtrip.stop.backToDestination': 'Vissza úti céllá',
'roadtrip.poi.rest': 'Pihenőhely',
'roadtrip.poi.campsite': 'Kemping',
'roadtrip.poi.food': 'Étkezés',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Hari {number}',
'roadtrip.stay.nightAdded': 'Ditambahkan sebagai menginap',
'roadtrip.stay.nightAction': 'Tambahkan sebagai menginap',
'roadtrip.stop.makeService': 'Jadikan perhentian di jalan',
'roadtrip.stop.changeKind': 'Ubah jenis perhentian',
'roadtrip.stop.backToDestination': 'Kembali jadi tujuan',
'roadtrip.poi.rest': 'Tempat istirahat',
'roadtrip.poi.campsite': 'Perkemahan',
'roadtrip.poi.food': 'Makanan',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Giorno {number}',
'roadtrip.stay.nightAdded': 'Aggiunto come pernottamento',
'roadtrip.stay.nightAction': 'Aggiungi come pernottamento',
'roadtrip.stop.makeService': 'Trasforma in sosta lungo il percorso',
'roadtrip.stop.changeKind': 'Cambia il tipo di sosta',
'roadtrip.stop.backToDestination': 'Torna a essere una destinazione',
'roadtrip.poi.rest': 'Area di sosta',
'roadtrip.poi.campsite': 'Campeggio',
'roadtrip.poi.food': 'Ristoro',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': '{number}日目',
'roadtrip.stay.nightAdded': '宿泊として追加しました',
'roadtrip.stay.nightAction': '宿泊として追加',
'roadtrip.stop.makeService': '途中の立ち寄りにする',
'roadtrip.stop.changeKind': '立ち寄りの種類を変更',
'roadtrip.stop.backToDestination': '目的地に戻す',
'roadtrip.poi.rest': 'サービスエリア',
'roadtrip.poi.campsite': 'キャンプ場',
'roadtrip.poi.food': '食事',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': '{number}일차',
'roadtrip.stay.nightAdded': '숙박으로 추가했습니다',
'roadtrip.stay.nightAction': '숙박으로 추가',
'roadtrip.stop.makeService': '경유 정차로 변경',
'roadtrip.stop.changeKind': '정차 유형 변경',
'roadtrip.stop.backToDestination': '다시 목적지로',
'roadtrip.poi.rest': '휴게소',
'roadtrip.poi.campsite': '캠핑장',
'roadtrip.poi.food': '식사',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Dag {number}',
'roadtrip.stay.nightAdded': 'Toegevoegd als overnachting',
'roadtrip.stay.nightAction': 'Toevoegen als overnachting',
'roadtrip.stop.makeService': 'Maak er een stop onderweg van',
'roadtrip.stop.changeKind': 'Soort stop wijzigen',
'roadtrip.stop.backToDestination': 'Weer een bestemming',
'roadtrip.poi.rest': 'Rustplaats',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Eten',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Dzień {number}',
'roadtrip.stay.nightAdded': 'Dodano jako nocleg',
'roadtrip.stay.nightAction': 'Dodaj jako nocleg',
'roadtrip.stop.makeService': 'Zamień w postój po drodze',
'roadtrip.stop.changeKind': 'Zmień rodzaj postoju',
'roadtrip.stop.backToDestination': 'Z powrotem cel podróży',
'roadtrip.poi.rest': 'Miejsce odpoczynku',
'roadtrip.poi.campsite': 'Kemping',
'roadtrip.poi.food': 'Jedzenie',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'День {number}',
'roadtrip.stay.nightAdded': 'Добавлено как ночёвка',
'roadtrip.stay.nightAction': 'Добавить как ночёвку',
'roadtrip.stop.makeService': 'Сделать остановкой в пути',
'roadtrip.stop.changeKind': 'Изменить тип остановки',
'roadtrip.stop.backToDestination': 'Снова пункт назначения',
'roadtrip.poi.rest': 'Зона отдыха',
'roadtrip.poi.campsite': 'Кемпинг',
'roadtrip.poi.food': 'Еда',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Dag {number}',
'roadtrip.stay.nightAdded': 'Tillagd som övernattning',
'roadtrip.stay.nightAction': 'Lägg till som övernattning',
'roadtrip.stop.makeService': 'Gör det till ett stopp på vägen',
'roadtrip.stop.changeKind': 'Byt typ av stopp',
'roadtrip.stop.backToDestination': 'Tillbaka till ett resmål',
'roadtrip.poi.rest': 'Rastplats',
'roadtrip.poi.campsite': 'Campingplats',
'roadtrip.poi.food': 'Mat',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': '{number}. gün',
'roadtrip.stay.nightAdded': 'Konaklama olarak eklendi',
'roadtrip.stay.nightAction': 'Konaklama olarak ekle',
'roadtrip.stop.makeService': 'Yol üzerinde molaya dönüştür',
'roadtrip.stop.changeKind': 'Mola türünü değiştir',
'roadtrip.stop.backToDestination': 'Yeniden varış noktası yap',
'roadtrip.poi.rest': 'Dinlenme alanı',
'roadtrip.poi.campsite': 'Kamp alanı',
'roadtrip.poi.food': 'Yemek',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'День {number}',
'roadtrip.stay.nightAdded': 'Додано як ночівлю',
'roadtrip.stay.nightAction': 'Додати як ночівлю',
'roadtrip.stop.makeService': 'Зробити зупинкою в дорозі',
'roadtrip.stop.changeKind': 'Змінити тип зупинки',
'roadtrip.stop.backToDestination': 'Знову пункт призначення',
'roadtrip.poi.rest': 'Зона відпочинку',
'roadtrip.poi.campsite': 'Кемпінг',
'roadtrip.poi.food': 'Їжа',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': 'Ngày {number}',
'roadtrip.stay.nightAdded': 'Đã thêm làm nơi nghỉ đêm',
'roadtrip.stay.nightAction': 'Thêm làm nơi nghỉ đêm',
'roadtrip.stop.makeService': 'Biến thành điểm dừng dọc đường',
'roadtrip.stop.changeKind': 'Đổi loại điểm dừng',
'roadtrip.stop.backToDestination': 'Trở lại là điểm đến',
'roadtrip.poi.rest': 'Trạm dừng nghỉ',
'roadtrip.poi.campsite': 'Khu cắm trại',
'roadtrip.poi.food': 'Đồ ăn',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': '第 {number} 天',
'roadtrip.stay.nightAdded': '已新增為過夜住宿',
'roadtrip.stay.nightAction': '新增為過夜住宿',
'roadtrip.stop.makeService': '設為途中停靠',
'roadtrip.stop.changeKind': '變更停靠類型',
'roadtrip.stop.backToDestination': '恢復為目的地',
'roadtrip.poi.rest': '休息站',
'roadtrip.poi.campsite': '露營地',
'roadtrip.poi.food': '餐飲',
+3
View File
@@ -38,6 +38,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stay.dayNumber': '第 {number} 天',
'roadtrip.stay.nightAdded': '已添加为过夜住宿',
'roadtrip.stay.nightAction': '添加为过夜住宿',
'roadtrip.stop.makeService': '设为途中停靠',
'roadtrip.stop.changeKind': '更改停靠类型',
'roadtrip.stop.backToDestination': '恢复为目的地',
'roadtrip.poi.rest': '服务区',
'roadtrip.poi.campsite': '露营地',
'roadtrip.poi.food': '餐饮',