feat(roadtrip): narrow the corridor to one stretch of the drive

A seven-hundred-kilometre day turns up seventy hits spread over the whole of
it, and the traveller wanting a break near Berlin has to read all seventy and
work out from "245 km along the route" which ones are anywhere near. That is
the wrong way round: you decide roughly where to stop, and then look at what is
there.

Picking a point narrows the list to what lies within 25, 50 or 100 km of it.
This is a subtraction rather than a second search, because the stops and the
hits are already measured along the same thinned line.

The midpoints of the legs are offered alongside the stops, and they are the
point of the feature: a break on a five-hour drive belongs in the middle of the
drive, not at either end. Asking only about stops would answer "what is near
Berlin" when the question is "what is near the halfway mark".

The chosen point is stored as which anchor it is rather than as a distance.
alongKm is measured against a line thinned with a tolerance derived from the
corridor width, so a kilometre figure kept here would quietly drift when the
width goes from 5 to 10; resolved through the stops it lands on the same place
again. It also names its day, so switching days cannot apply yesterday's break
to today's drive.
This commit is contained in:
Maurice
2026-09-04 23:44:16 +02:00
committed by Maurice
parent 061259b3d9
commit 36ac5de84c
29 changed files with 254 additions and 33 deletions
@@ -91,6 +91,11 @@ function corridor(over: Partial<RoadtripCorridor> = {}, search: Partial<Roadtrip
widthKm: 5,
setWidthKm: vi.fn(),
nameFilter: '',
anchors: [],
section: null,
setSection: vi.fn(),
sectionKm: 50,
setSectionKm: vi.fn(),
socketFilter: '',
setSocketFilter: vi.fn(),
minKw: 0,
@@ -12,7 +12,7 @@ import RoadtripCategoryPicker from './RoadtripCategoryPicker'
import { serviceColor } from './roadtripModel'
import { CORRIDOR_CATEGORY_BY_KEY } from './stopKinds'
import { FS } from './typeScale'
import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor'
import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_SECTION_KM, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor'
import type { CorridorPoi } from './useCorridorPois'
import type { RoadtripRoutes } from './useRoadtripRoutes'
@@ -399,6 +399,55 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro
) : null}
</div>
{/* Around one point of the drive, which is how a break is actually planned: you
decide roughly where to stop and then look at what is there, rather than
reading seventy hits spread over seven hundred kilometres. The midpoint of
a leg is offered alongside the stops, because a break on a five-hour drive
belongs in the middle of it and not at either end. */}
{corridor.anchors.length > 1 && corridor.day ? (
<div className="mt-2 flex gap-2">
<CustomSelect
value={corridor.section ? `${corridor.section.kind}:${corridor.section.index}` : ''}
onChange={value => {
const raw = String(value)
if (!raw) { corridor.setSection(null); return }
const [kind, index] = raw.split(':')
corridor.setSection({
dayId: corridor.day!.dayId,
kind: kind as 'stop' | 'leg',
index: Number(index),
})
}}
options={[
{ value: '', label: t('roadtrip.poi.wholeDay') },
...corridor.anchors.map(a => ({
value: `${a.kind}:${a.index}`,
label: a.kind === 'stop'
? (corridor.day!.stops[a.index]?.name ?? t('roadtrip.poi.wholeDay'))
: t('roadtrip.poi.midLeg', {
from: corridor.day!.stops[a.index]?.name ?? '',
to: corridor.day!.stops[a.index + 1]?.name ?? '',
}),
})),
]}
style={{ flex: 1 }}
size="sm"
/>
{corridor.section ? (
<CustomSelect
value={String(corridor.sectionKm)}
onChange={value => corridor.setSectionKm(Number(value))}
options={CORRIDOR_SECTION_KM.map(km => ({
value: String(km),
label: `± ${formatDistance(km, distanceUnit)}`,
}))}
style={{ width: 110 }}
size="sm"
/>
) : null}
</div>
) : null}
{/* Only where there is a charger to narrow. Two answers OSM actually carries
often enough to filter on: which plug, and how fast. A station that states
neither stays in the list, because roughly two thirds of them state no
@@ -7,6 +7,7 @@ import {
deriveDriveWarnings,
legIndexForAlong,
refuelsRange,
sectionAnchors,
parseClock,
splitIntoRuns,
sumLegSeconds,
@@ -421,3 +422,22 @@ describe('deriveDriveWarnings', () => {
expect(out.carryKm).toBe(0)
})
})
describe('sectionAnchors', () => {
it('FE-ROADTRIP-MODEL-080: every stop, and the middle of every leg between them', () => {
// The leg midpoints are the point of the feature: a break on a five-hour drive is
// planned in the middle of the drive, not at either end of it.
expect(sectionAnchors([0, 100, 250])).toEqual([
{ kind: 'stop', index: 0, alongKm: 0 },
{ kind: 'leg', index: 0, alongKm: 50 },
{ kind: 'stop', index: 1, alongKm: 100 },
{ kind: 'leg', index: 1, alongKm: 175 },
{ kind: 'stop', index: 2, alongKm: 250 },
])
})
it('FE-ROADTRIP-MODEL-081: a single stop has no leg to sit in the middle of', () => {
expect(sectionAnchors([42])).toEqual([{ kind: 'stop', index: 0, alongKm: 42 }])
expect(sectionAnchors([])).toEqual([])
})
})
@@ -373,6 +373,32 @@ export function legIndexForAlong(legEndMeters: number[], alongMeters: number): n
return legEndMeters.length - 1
}
/**
* The places on a day somebody would plan a break around.
*
* Every stop, and the middle of every leg between two of them. The leg midpoints are the
* point of the whole thing: a break on a five-hour drive is planned in the middle of the
* drive, not at either end of it, and offering only the stops would mean asking "what is
* near Berlin" when the question is "what is near the halfway mark".
*/
export interface SectionAnchor {
kind: 'stop' | 'leg'
/** Index of the stop, or of the stop the leg leaves. */
index: number
alongKm: number
}
export function sectionAnchors(stopsAlongKm: number[]): SectionAnchor[] {
const out: SectionAnchor[] = []
for (let i = 0; i < stopsAlongKm.length; i++) {
out.push({ kind: 'stop', index: i, alongKm: stopsAlongKm[i] })
if (i + 1 < stopsAlongKm.length) {
out.push({ kind: 'leg', index: i, alongKm: (stopsAlongKm[i] + stopsAlongKm[i + 1]) / 2 })
}
}
return out
}
export function insertIndexForAlong(stopsAlongKm: number[], hitAlongKm: number): number {
if (stopsAlongKm.length < 2) return stopsAlongKm.length
let i = 0
@@ -142,4 +142,33 @@ describe('useRoadtripCorridor', () => {
// Both map renderers rebuild every marker when this array's identity changes.
expect(result.current.visible).toBe(before)
})
it('FE-ROADTRIP-CORRIDORSTATE-020: narrowing to one point keeps only what is near it', () => {
// A seven-hundred-kilometre day turns up hits along the whole of it. Picking the point
// you mean to break at is a subtraction, because both figures are measured against
// the same thinned line rather than being asked for separately.
useCorridorPois.mockReturnValue(searchWith([
poi({ osm_id: 'near', name: 'Near', alongKm: 30 }),
poi({ osm_id: 'far', name: 'Far', alongKm: 600 }),
]))
const { result } = renderHook(() => useRoadtripCorridor(routes([day()])))
act(() => { result.current.setSectionKm(50) })
act(() => { result.current.setSection({ dayId: 1, kind: 'stop', index: 0 }) })
// Stop 0 sits at the start of the drive, so only the hit within 50 km survives.
expect(result.current.visible.map(p => p.osm_id)).toEqual(['near'])
})
it('FE-ROADTRIP-CORRIDORSTATE-021: an anchor belonging to another day narrows nothing', () => {
// The section names a day as well as an index, so switching days cannot silently
// apply yesterday's break to today's drive.
const hits = [poi({ osm_id: 'a', name: 'A', alongKm: 10 }), poi({ osm_id: 'b', name: 'B', alongKm: 600 })]
useCorridorPois.mockReturnValue(searchWith(hits))
const { result } = renderHook(() => useRoadtripCorridor(routes([day()])))
act(() => { result.current.setSection({ dayId: 99, kind: 'stop', index: 0 }) })
expect(result.current.visible).toBe(hits)
})
})
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCorridorPois, type CorridorPoi, type CorridorSearch } from './useCorridorPois'
import { insertIndexForAlong } from './roadtripModel'
import { sectionAnchors, insertIndexForAlong } from './roadtripModel'
import type { SectionAnchor } from './roadtripModel'
import { projectOntoRoute, type LatLng } from './corridor'
import type { RoadtripDay, RoadtripRoutes } from './useRoadtripRoutes'
@@ -11,6 +12,9 @@ export { CORRIDOR_CATEGORY_KEYS } from './stopKinds'
/** Corridor widths offered, in kilometres. */
export const CORRIDOR_WIDTHS_KM = [2, 5, 10]
/** How far either side of a chosen point the list is narrowed to, in kilometres. */
export const CORRIDOR_SECTION_KM = [25, 50, 100]
export interface RoadtripCorridor {
/** Which day's drive is being searched. */
dayId: string
@@ -23,6 +27,14 @@ export interface RoadtripCorridor {
search: CorridorSearch
/** Narrows what was found by name or brand. Empty means everything. */
nameFilter: string
/** The stops and leg midpoints a break can be planned around. */
anchors: SectionAnchor[]
/** Which of them the list is narrowed to, or null for the whole day. */
section: { dayId: number; kind: 'stop' | 'leg'; index: number } | null
setSection: (value: { dayId: number; kind: 'stop' | 'leg'; index: number } | null) => void
/** How far either side of it, in kilometres. */
sectionKm: number
setSectionKm: (value: number) => void
/** Socket family the charging hits are narrowed to, or empty for any. */
socketFilter: string
setSocketFilter: (value: string) => void
@@ -56,6 +68,17 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
const [categories, setCategories] = useState<string[]>(['fuel'])
const [widthKm, setWidthKm] = useState<number>(5)
const [nameFilter, setNameFilter] = useState('')
/**
* Which point of the day the list is narrowed around, if any.
*
* Stored as the anchor's identity rather than as a distance, on purpose: `alongKm` is
* measured against the thinned spine, whose length depends on the corridor width
* (`useCorridorPois` thins with a tolerance derived from it). A kilometre figure kept
* here would quietly shift when the width changes from 5 to 10; resolved through
* `stopsAlongKm` it lands on the same stop again.
*/
const [section, setSection] = useState<{ dayId: number; kind: 'stop' | 'leg'; index: number } | null>(null)
const [sectionKm, setSectionKm] = useState(50)
/** A socket family, or empty for any. Only ever applied to charging hits. */
const [socketFilter, setSocketFilter] = useState('')
/** Minimum kW, or 0 for any. */
@@ -92,37 +115,6 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
wasSearching.current = searching
}, [searching])
/**
* Memoised on purpose, not filtered where it is drawn: both map renderers tear down and
* rebuild every POI marker whenever the array's identity changes, so a `.filter()` in a
* render body would make the pins flicker on each keystroke and on every unrelated
* re-render.
*
* Brand counts as a name here because the server folds `operator` into `brand`, and
* "Shell" is what someone types when the OSM name is "Shell Autohof Nord".
*/
const visible = useMemo(() => {
const needle = nameFilter.trim().toLowerCase()
// The same array when nothing narrows it, not a copy of it: the map redraws off this
// reference, and handing it a fresh array on every render moves every pin.
if (!needle && !socketFilter && !minKw) return search.results
return search.results.filter(p => {
if (needle && !(p.name.toLowerCase().includes(needle) || (p.brand ?? '').toLowerCase().includes(needle))) {
return false
}
// The charging filters only ever hide charging hits. A rest area does not have a
// socket and is not answering the question, so filtering the whole list by one
// would empty it of everything the search also found.
if (p.category !== 'charging') return true
if (socketFilter && !p.charging?.sockets.some(s => s.type === socketFilter)) return false
// A station that does not state its power is kept. Roughly two thirds of them do
// not, and reading silence as "too slow" would throw away most of the map.
if (minKw && p.charging?.sockets.some(s => s.kw != null) && !p.charging.sockets.some(s => (s.kw ?? 0) >= minKw)) {
return false
}
return true
})
}, [search.results, nameFilter, socketFilter, minKw])
/**
* Where a hit belongs in the day's chain, as an index among its stops.
@@ -142,6 +134,55 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
})
}, [day, search.spine])
/**
* The stops and leg midpoints of the current day, and the distance of the chosen one.
*
* Resolved here rather than stored: `section` names an anchor, and its kilometre figure
* is looked up fresh, so changing the corridor width does not move the point the list
* is narrowed around.
*/
const anchors = useMemo(() => sectionAnchors(stopsAlongKm), [stopsAlongKm])
const anchorKm = useMemo(() => {
if (!section || !day || section.dayId !== day.dayId) return null
return anchors.find(a => a.kind === section.kind && a.index === section.index)?.alongKm ?? null
}, [section, day, anchors])
/**
* Memoised on purpose, not filtered where it is drawn: both map renderers tear down and
* rebuild every POI marker whenever the array's identity changes, so a `.filter()` in a
* render body would make the pins flicker on each keystroke and on every unrelated
* re-render.
*
* Brand counts as a name here because the server folds `operator` into `brand`, and
* "Shell" is what someone types when the OSM name is "Shell Autohof Nord".
*/
const visible = useMemo(() => {
const needle = nameFilter.trim().toLowerCase()
// The same array when nothing narrows it, not a copy of it: the map redraws off this
// reference, and handing it a fresh array on every render moves every pin.
if (!needle && !socketFilter && !minKw && anchorKm === null) return search.results
return search.results.filter(p => {
// Around one point of the drive, when one has been picked. The two figures are
// measured against the same thinned line, so this really is a subtraction and not
// a second search.
if (anchorKm !== null && Math.abs(p.alongKm - anchorKm) > sectionKm) return false
if (needle && !(p.name.toLowerCase().includes(needle) || (p.brand ?? '').toLowerCase().includes(needle))) {
return false
}
// The charging filters only ever hide charging hits. A rest area does not have a
// socket and is not answering the question, so filtering the whole list by one
// would empty it of everything the search also found.
if (p.category !== 'charging') return true
if (socketFilter && !p.charging?.sockets.some(s => s.type === socketFilter)) return false
// A station that does not state its power is kept. Roughly two thirds of them do
// not, and reading silence as "too slow" would throw away most of the map.
if (minKw && p.charging?.sockets.some(s => s.kw != null) && !p.charging.sockets.some(s => (s.kw ?? 0) >= minKw)) {
return false
}
return true
})
}, [search.results, nameFilter, socketFilter, minKw, anchorKm, sectionKm])
const insertIndexFor = useCallback(
(poi: CorridorPoi) => insertIndexForAlong(stopsAlongKm, poi.alongKm),
[stopsAlongKm],
@@ -157,6 +198,11 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
setWidthKm,
search,
nameFilter,
anchors,
section,
setSection,
sectionKm,
setSectionKm,
socketFilter,
setSocketFilter,
minKw,
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'أي قابس',
'roadtrip.poi.anyPower': 'أي قدرة',
'roadtrip.poi.free': 'مجاني',
'roadtrip.poi.wholeDay': 'اليوم كله',
'roadtrip.poi.midLeg': 'في المنتصف، {from} إلى {to}',
'roadtrip.poi.rest': 'استراحة',
'roadtrip.poi.campsite': 'مخيم',
'roadtrip.poi.food': 'طعام',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Qualquer tomada',
'roadtrip.poi.anyPower': 'Qualquer potência',
'roadtrip.poi.free': 'Gratuito',
'roadtrip.poi.wholeDay': 'O dia todo',
'roadtrip.poi.midLeg': 'Na metade, {from} ate {to}',
'roadtrip.poi.rest': 'Área de descanso',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Comida',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Qualsevol connector',
'roadtrip.poi.anyPower': 'Qualsevol potència',
'roadtrip.poi.free': 'Gratuït',
'roadtrip.poi.wholeDay': 'Tot el dia',
'roadtrip.poi.midLeg': 'A mig cami, {from} a {to}',
'roadtrip.poi.rest': 'Àrea de descans',
'roadtrip.poi.campsite': 'Càmping',
'roadtrip.poi.food': 'Menjar',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Libovolná zásuvka',
'roadtrip.poi.anyPower': 'Libovolný výkon',
'roadtrip.poi.free': 'Zdarma',
'roadtrip.poi.wholeDay': 'Cely den',
'roadtrip.poi.midLeg': 'V polovine, {from} do {to}',
'roadtrip.poi.rest': 'Odpočívadlo',
'roadtrip.poi.campsite': 'Kemp',
'roadtrip.poi.food': 'Jídlo',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Alle Stecker',
'roadtrip.poi.anyPower': 'Alle Leistungen',
'roadtrip.poi.free': 'Kostenlos',
'roadtrip.poi.wholeDay': 'Ganzer Tag',
'roadtrip.poi.midLeg': 'Auf halber Strecke, {from} bis {to}',
'roadtrip.poi.rest': 'Rastplatz',
'roadtrip.poi.campsite': 'Campingplatz',
'roadtrip.poi.food': 'Essen',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Any plug',
'roadtrip.poi.anyPower': 'Any power',
'roadtrip.poi.free': 'Free',
'roadtrip.poi.wholeDay': 'The whole day',
'roadtrip.poi.midLeg': 'Halfway, {from} to {to}',
'roadtrip.poi.rest': 'Rest area',
'roadtrip.poi.campsite': 'Campsite',
'roadtrip.poi.food': 'Food',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Cualquier conector',
'roadtrip.poi.anyPower': 'Cualquier potencia',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.wholeDay': 'Todo el dia',
'roadtrip.poi.midLeg': 'A mitad, {from} a {to}',
'roadtrip.poi.rest': 'Área de descanso',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Comida',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Toute prise',
'roadtrip.poi.anyPower': 'Toute puissance',
'roadtrip.poi.free': 'Gratuit',
'roadtrip.poi.wholeDay': 'Toute la journee',
'roadtrip.poi.midLeg': 'A mi-chemin, {from} vers {to}',
'roadtrip.poi.rest': 'Aire de repos',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Restauration',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Κάθε βύσμα',
'roadtrip.poi.anyPower': 'Κάθε ισχύς',
'roadtrip.poi.free': 'Δωρεάν',
'roadtrip.poi.wholeDay': 'Ολη η ημερα',
'roadtrip.poi.midLeg': 'Στα μισα, {from} προς {to}',
'roadtrip.poi.rest': 'Χώρος ανάπαυσης',
'roadtrip.poi.campsite': 'Κάμπινγκ',
'roadtrip.poi.food': 'Φαγητό',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Bármely csatlakozó',
'roadtrip.poi.anyPower': 'Bármely teljesítmény',
'roadtrip.poi.free': 'Ingyenes',
'roadtrip.poi.wholeDay': 'A teljes nap',
'roadtrip.poi.midLeg': 'Felúton, {from} és {to} kozott',
'roadtrip.poi.rest': 'Pihenőhely',
'roadtrip.poi.campsite': 'Kemping',
'roadtrip.poi.food': 'Étkezés',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Semua colokan',
'roadtrip.poi.anyPower': 'Semua daya',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.wholeDay': 'Sepanjang hari',
'roadtrip.poi.midLeg': 'Di tengah, {from} ke {to}',
'roadtrip.poi.rest': 'Tempat istirahat',
'roadtrip.poi.campsite': 'Perkemahan',
'roadtrip.poi.food': 'Makanan',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Qualsiasi presa',
'roadtrip.poi.anyPower': 'Qualsiasi potenza',
'roadtrip.poi.free': 'Gratuito',
'roadtrip.poi.wholeDay': 'Tutta la giornata',
'roadtrip.poi.midLeg': 'A meta strada, {from} verso {to}',
'roadtrip.poi.rest': 'Area di sosta',
'roadtrip.poi.campsite': 'Campeggio',
'roadtrip.poi.food': 'Ristoro',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'すべてのプラグ',
'roadtrip.poi.anyPower': 'すべての出力',
'roadtrip.poi.free': '無料',
'roadtrip.poi.wholeDay': '一日全体',
'roadtrip.poi.midLeg': '中間地点、{from}から{to}',
'roadtrip.poi.rest': 'サービスエリア',
'roadtrip.poi.campsite': 'キャンプ場',
'roadtrip.poi.food': '食事',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': '모든 플러그',
'roadtrip.poi.anyPower': '모든 출력',
'roadtrip.poi.free': '무료',
'roadtrip.poi.wholeDay': '하루 전체',
'roadtrip.poi.midLeg': '중간 지점, {from}에서 {to}',
'roadtrip.poi.rest': '휴게소',
'roadtrip.poi.campsite': '캠핑장',
'roadtrip.poi.food': '식사',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Elke stekker',
'roadtrip.poi.anyPower': 'Elk vermogen',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.wholeDay': 'De hele dag',
'roadtrip.poi.midLeg': 'Halverwege, {from} naar {to}',
'roadtrip.poi.rest': 'Rustplaats',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Eten',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Dowolna wtyczka',
'roadtrip.poi.anyPower': 'Dowolna moc',
'roadtrip.poi.free': 'Bezpłatnie',
'roadtrip.poi.wholeDay': 'Caly dzien',
'roadtrip.poi.midLeg': 'W polowie, {from} do {to}',
'roadtrip.poi.rest': 'Miejsce odpoczynku',
'roadtrip.poi.campsite': 'Kemping',
'roadtrip.poi.food': 'Jedzenie',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Любой разъём',
'roadtrip.poi.anyPower': 'Любая мощность',
'roadtrip.poi.free': 'Бесплатно',
'roadtrip.poi.wholeDay': 'Весь день',
'roadtrip.poi.midLeg': 'На полпути, {from} до {to}',
'roadtrip.poi.rest': 'Зона отдыха',
'roadtrip.poi.campsite': 'Кемпинг',
'roadtrip.poi.food': 'Еда',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Alla kontakter',
'roadtrip.poi.anyPower': 'Alla effekter',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.wholeDay': 'Hela dagen',
'roadtrip.poi.midLeg': 'Halvvags, {from} till {to}',
'roadtrip.poi.rest': 'Rastplats',
'roadtrip.poi.campsite': 'Campingplats',
'roadtrip.poi.food': 'Mat',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Tum soketler',
'roadtrip.poi.anyPower': 'Tum gucler',
'roadtrip.poi.free': 'Ucretsiz',
'roadtrip.poi.wholeDay': 'Tum gun',
'roadtrip.poi.midLeg': 'Yolun yarisi, {from} - {to}',
'roadtrip.poi.rest': 'Dinlenme alanı',
'roadtrip.poi.campsite': 'Kamp alanı',
'roadtrip.poi.food': 'Yemek',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Будь-який роз’єм',
'roadtrip.poi.anyPower': 'Будь-яка потужність',
'roadtrip.poi.free': 'Безкоштовно',
'roadtrip.poi.wholeDay': 'Весь день',
'roadtrip.poi.midLeg': 'На пiвдорозi, {from} до {to}',
'roadtrip.poi.rest': 'Зона відпочинку',
'roadtrip.poi.campsite': 'Кемпінг',
'roadtrip.poi.food': 'Їжа',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': 'Mọi loại phích',
'roadtrip.poi.anyPower': 'Mọi công suất',
'roadtrip.poi.free': 'Miễn phí',
'roadtrip.poi.wholeDay': 'Cả ngày',
'roadtrip.poi.midLeg': 'Giữa đường, {from} đến {to}',
'roadtrip.poi.rest': 'Trạm dừng nghỉ',
'roadtrip.poi.campsite': 'Khu cắm trại',
'roadtrip.poi.food': 'Đồ ăn',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': '任意接口',
'roadtrip.poi.anyPower': '任意功率',
'roadtrip.poi.free': '免費',
'roadtrip.poi.wholeDay': '全天',
'roadtrip.poi.midLeg': '中途,{from} 至 {to}',
'roadtrip.poi.rest': '休息站',
'roadtrip.poi.campsite': '露營地',
'roadtrip.poi.food': '餐飲',
+2
View File
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.anySocket': '任意接口',
'roadtrip.poi.anyPower': '任意功率',
'roadtrip.poi.free': '免费',
'roadtrip.poi.wholeDay': '全天',
'roadtrip.poi.midLeg': '中途,{from} 至 {to}',
'roadtrip.poi.rest': '服务区',
'roadtrip.poi.campsite': '露营地',
'roadtrip.poi.food': '餐饮',