feat(maps): pass through what OSM knows about a charging station

The corridor query has always ended in `out center tags`, and the projection
has always kept six keys out of the whole tag set. Everything an electric car
needs to know was in the answer and went in the bin: which plug families the
station has, how many of each, at what power, how many vehicles fit, and whether
it charges. Reading them costs no extra request, no key and no second query.

The filters treat "not stated" as its own answer, which is the only honest way
to build them: OSM states a socket type on roughly a third of charging stations
and a power on fewer still, so a power filter keeps every station that says
nothing rather than reading silence as too slow. The plug dropdown offers only
the families this search actually turned up, because CHAdeMO in a list for a
corridor with three Type 2 posts on it is a filter that can only return nothing.

Socket names stay as they are printed on the plug. "Type 2" is not a string
anyone translates, and the lowercase tag key is not what is written on the
charger.

The tool description follows, as parity requires, including the coverage caveat
— a model that reads a missing power as "no fast charging here" would be wrong
about most of the map.
This commit is contained in:
Maurice
2026-09-04 23:25:38 +02:00
committed by Maurice
parent fc74991a15
commit 061259b3d9
31 changed files with 374 additions and 33 deletions
@@ -58,5 +58,17 @@ export interface Poi {
phone: string | null
opening_hours: string | null
cuisine: string | null
/**
* What a charging station offers, when it is one and OSM says so.
*
* Optional twice over: only charging hits carry it at all, and OSM knows a socket type
* for roughly a third of them, a capacity for about seven in ten. "Not stated" is its
* own answer here and must not be read as a no.
*/
charging?: {
sockets: { type: string; count: number | null; kw: number | null }[]
capacity: number | null
fee: boolean | null
} | null
source: 'openstreetmap'
}
@@ -91,6 +91,10 @@ function corridor(over: Partial<RoadtripCorridor> = {}, search: Partial<Roadtrip
widthKm: 5,
setWidthKm: vi.fn(),
nameFilter: '',
socketFilter: '',
setSocketFilter: vi.fn(),
minKw: 0,
setMinKw: vi.fn(),
setNameFilter: vi.fn(),
// Same as the hook with an empty filter: everything found is on show.
visible: searchState.results,
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import {
Search, Plus, RotateCw, AlertTriangle, X, BedDouble, MapPin,
} from 'lucide-react'
@@ -27,6 +27,30 @@ interface RoadtripCorridorPanelProps {
) => void
}
/**
* How each socket family is written on the plug.
*
* Not translated and not derived from the tag: these are proper nouns with an accepted
* spelling, and "Type 2" as a lowercase tag key is not what anybody looks for on a
* charger. Anything not listed falls through to the raw key rather than being hidden,
* because an unknown socket is still worth seeing.
*/
/** The steps worth offering: a household socket, a fast AC post, and the two DC tiers. */
const KW_STEPS = [11, 22, 50, 150]
const SOCKET_LABEL: Record<string, string> = {
type2: 'Type 2',
type2_combo: 'CCS',
type2_cable: 'Type 2 cable',
ccs: 'CCS',
chademo: 'CHAdeMO',
type1: 'Type 1',
type1_combo: 'CCS1',
schuko: 'Schuko',
tesla_supercharger: 'Supercharger',
tesla_destination: 'Tesla Destination',
}
/** Label and icon per category, from the one table every road-trip surface reads. */
const CATEGORY_META = CORRIDOR_CATEGORY_BY_KEY
@@ -85,6 +109,24 @@ function ResultRow({ poi, onAdd }: { poi: CorridorPoi; onAdd?: () => void }): Re
? t('roadtrip.poi.atStart')
: t('roadtrip.poi.alongRoute', { distance: formatDistance(poi.alongKm, distanceUnit) })}
</span>
{/* What the charger offers, where OSM says. Socket names are proper nouns and
stay as they are; the numbers around them are what decides whether a car can
use it at all. A station that says nothing shows nothing rather than a row
of dashes, because "not stated" is not "no". */}
{poi.charging?.sockets.length ? (
<span className="flex flex-wrap items-baseline gap-x-1.5">
{poi.charging.sockets.slice(0, 3).map(s => (
<span key={s.type} className="text-content-muted">
{SOCKET_LABEL[s.type] ?? s.type}
{s.kw ? ` ${s.kw} kW` : ''}
{s.count && s.count > 1 ? ` ×${s.count}` : ''}
</span>
))}
</span>
) : null}
{poi.charging?.fee === false ? (
<span className="text-success">{t('roadtrip.poi.free')}</span>
) : null}
</div>
</div>
{/* Always there, quiet until the row is under the pointer: a button that only
@@ -186,6 +228,18 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro
// Something was found, the filter just hides it — a different state from "not searched
// yet" and from "the drive really has none of these".
/**
* The socket families this search actually turned up, in the order the table lists
* them. Offering all ten would put CHAdeMO in the dropdown for a corridor that has
* three Type 2 posts on it, which is a filter that can only produce an empty list.
*/
const socketsFound = useMemo(() => {
const seen = new Set<string>()
for (const p of search.results) for (const s of p.charging?.sockets ?? []) seen.add(s.type)
return [...seen].sort()
}, [search.results])
const hasCharging = socketsFound.length > 0
const filteredToNothing = search.results.length > 0 && corridor.visible.length === 0
const canSearch = !search.loading && corridor.categories.length > 0 && (corridor.day?.stops.length ?? 0) > 1
@@ -258,16 +312,18 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro
</div>
</div>
{/* One word, so it can be read at the size a primary action deserves. The panel
it sits in is headed "along the route" and every control above it narrows the
same search; repeating that on the button only made it small. */}
<button
type="button"
onClick={search.search}
disabled={!canSearch}
style={{ fontSize: FS.control }}
className="flex h-[34px] w-full items-center justify-center gap-1.5 rounded-xl bg-accent font-semibold text-accent-text transition-opacity disabled:opacity-50"
className="flex h-[38px] w-full items-center justify-center gap-2 rounded-xl bg-accent text-body font-semibold text-accent-text transition-opacity disabled:opacity-50"
>
{search.loading
? <RotateCw size={13} className="animate-spin" aria-hidden />
: <Search size={13} strokeWidth={2} aria-hidden />}
? <RotateCw size={15} className="animate-spin" aria-hidden />
: <Search size={15} strokeWidth={2} aria-hidden />}
{search.loading
? t('roadtrip.poi.searching', { done: search.progress.done, total: search.progress.total })
: t('roadtrip.poi.search')}
@@ -342,6 +398,35 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro
</button>
) : null}
</div>
{/* 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
power at all and reading that silence as "too slow" would empty the map. */}
{hasCharging ? (
<div className="mt-2 flex gap-2">
<CustomSelect
value={corridor.socketFilter}
onChange={value => corridor.setSocketFilter(String(value))}
options={[
{ value: '', label: t('roadtrip.poi.anySocket') },
...socketsFound.map(s => ({ value: s, label: SOCKET_LABEL[s] ?? s })),
]}
style={{ flex: 1 }}
size="sm"
/>
<CustomSelect
value={String(corridor.minKw)}
onChange={value => corridor.setMinKw(Number(value))}
options={[
{ value: '0', label: t('roadtrip.poi.anyPower') },
...KW_STEPS.map(kw => ({ value: String(kw), label: `${kw}+ kW` })),
]}
style={{ flex: 1 }}
size="sm"
/>
</div>
) : null}
</div>
) : null}
@@ -23,6 +23,12 @@ export interface RoadtripCorridor {
search: CorridorSearch
/** Narrows what was found by name or brand. Empty means everything. */
nameFilter: string
/** Socket family the charging hits are narrowed to, or empty for any. */
socketFilter: string
setSocketFilter: (value: string) => void
/** Minimum kW for charging hits, or 0 for any. */
minKw: number
setMinKw: (value: number) => void
setNameFilter: (value: string) => void
/**
* What both the panel and the map show: the hits that match `nameFilter`.
@@ -50,6 +56,10 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
const [categories, setCategories] = useState<string[]>(['fuel'])
const [widthKm, setWidthKm] = useState<number>(5)
const [nameFilter, setNameFilter] = useState('')
/** A socket family, or empty for any. Only ever applied to charging hits. */
const [socketFilter, setSocketFilter] = useState('')
/** Minimum kW, or 0 for any. */
const [minKw, setMinKw] = useState(0)
// 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.
@@ -93,10 +103,26 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
*/
const visible = useMemo(() => {
const needle = nameFilter.trim().toLowerCase()
if (!needle) return search.results
return search.results.filter(p =>
p.name.toLowerCase().includes(needle) || (p.brand ?? '').toLowerCase().includes(needle))
}, [search.results, nameFilter])
// 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.
@@ -131,6 +157,10 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
setWidthKm,
search,
nameFilter,
socketFilter,
setSocketFilter,
minKw,
setMinKw,
setNameFilter,
visible,
insertIndexFor,
+68
View File
@@ -264,6 +264,8 @@ export interface OverpassPoi {
/** Brand name and its Wikidata id, when OSM carries them — the logo is looked up from the id. */
brand: string | null;
brand_wikidata: string | null;
/** What a charging station offers, when it is one and OSM says. */
charging: ChargingInfo | null;
/**
* Which index the row came from. Overture is not OpenStreetMap: it carries
* OSM among other sources under other licences, so a row from the TREK index
@@ -273,6 +275,72 @@ export interface OverpassPoi {
source: 'openstreetmap' | 'trek-places';
}
/**
* The part of a charging station that decides whether it is any use to a particular car.
*
* Read out of tags the query has always returned and the projection has always thrown
* away: `out center tags` hands back the whole tag set, and only six keys of it were ever
* passed on. Nothing here costs an extra request.
*
* Coverage is the reason this is all optional. Across the charging stations in OSM,
* roughly a third carry a socket type, about seven in ten a capacity, and about half say
* whether they charge a fee. A filter built on it has to treat "not stated" as its own
* answer rather than as a no.
*/
export interface ChargingInfo {
/** One entry per socket family the station lists, with how many and how fast. */
sockets: { type: string; count: number | null; kw: number | null }[];
/** How many vehicles can charge at once, across all sockets. */
capacity: number | null;
/** true = costs money, false = free, null = OSM does not say. */
fee: boolean | null;
}
/**
* OSM writes sockets as one key per family: `socket:type2=4` is the count, and
* `socket:type2:output=22 kW` the power. Both are free text in practice, so the count is
* only taken when it parses as a whole number and the power only when a number can be
* read off the front of it.
*
* The families are listed rather than derived from the tag names, because `socket:` also
* carries keys that are not a socket family at all.
*/
const SOCKET_FAMILIES = [
'type2', 'type2_combo', 'type2_cable', 'ccs', 'chademo', 'type1', 'type1_combo',
'schuko', 'tesla_supercharger', 'tesla_destination',
] as const;
/** Leading number out of a free-text value like "22 kW" or "50kw". */
function leadingNumber(raw: string | undefined): number | null {
if (!raw) return null;
const n = Number.parseFloat(raw.replace(',', '.'));
return Number.isFinite(n) && n > 0 ? n : null;
}
export function readChargingInfo(tags: Record<string, string>): ChargingInfo | null {
const sockets: ChargingInfo['sockets'] = [];
for (const family of SOCKET_FAMILIES) {
const raw = tags[`socket:${family}`];
if (raw === undefined) continue;
const count = Number.parseInt(raw, 10);
sockets.push({
type: family,
count: Number.isInteger(count) && count > 0 ? count : null,
kw: leadingNumber(tags[`socket:${family}:output`]),
});
}
const capacity = Number.parseInt(tags.capacity ?? '', 10);
const fee = tags.fee === 'yes' ? true : tags.fee === 'no' ? false : null;
const info: ChargingInfo = {
sockets,
capacity: Number.isInteger(capacity) && capacity > 0 ? capacity : null,
fee,
};
// Nothing said is null rather than an empty shell, so the client can tell "no data"
// from "no sockets" without inspecting three fields.
return sockets.length || info.capacity !== null || fee !== null ? info : null;
}
// Each pill category → the OSM tag selectors it searches. Keys here are the
// contract with the client's POI_CATEGORIES (same keys, label/icon/colour live
// client-side).
+1 -1
View File
@@ -89,7 +89,7 @@ export class MapsMcp {
*/
@Tool({
name: 'search_pois',
description: 'List points of interest of one category inside a map rectangle, with address, opening hours, website, phone and cuisine wherever the source carries them. Answers from the TREK place index where it can and from OpenStreetMap otherwise; each result names which one it came from in its `source` field. This is the discovery tool: use it to answer "what is around here" for a neighbourhood or a whole city district. Prefer search_place when the user already named the place they mean. Never calls Google, so it costs nothing and works on an instance with no Places key.',
description: 'List points of interest of one category inside a map rectangle, with address, opening hours, website, phone and cuisine wherever the source carries them. Answers from the TREK place index where it can and from OpenStreetMap otherwise; each result names which one it came from in its `source` field. A charging station also reports its sockets: which plug families it has, how many of each and at what power, plus how many vehicles fit and whether it charges a fee. OSM states a socket type on roughly a third of them and a power on fewer, so treat a missing figure as unknown rather than as absent. This is the discovery tool: use it to answer "what is around here" for a neighbourhood or a whole city district. Prefer search_place when the user already named the place they mean. Never calls Google, so it costs nothing and works on an instance with no Places key.',
inputSchema: {
category: z.union([
z.enum(POI_CATEGORY_KEYS),
+9
View File
@@ -46,6 +46,7 @@ import {
haversineMetres,
namesOverlap,
mergeSearchResults,
readChargingInfo,
type GoogleOpeningHours,
type OverpassPoi,
} from './maps.helpers';
@@ -869,6 +870,10 @@ export class MapsService {
// gets its own mark here exactly as it does on the Overpass path.
brand: p.brand?.name ?? null,
brand_wikidata: p.brand?.wikidata ?? null,
// Sockets are an OSM thing; the index has no charging fields, so
// a station answered from here reports "not stated" rather than
// claiming it offers nothing.
charging: null,
// The index has no cuisine field, so this null is the truth
// rather than a field being dropped on the way through.
cuisine: null,
@@ -1273,6 +1278,10 @@ export class MapsService {
// Only the plain Q-id form is passed on; anything else would be a lookup we
// would have to guess at.
brand_wikidata: /^Q[0-9]+$/.test(tags['brand:wikidata'] || '') ? tags['brand:wikidata'] : null,
// Only where it means something. Every POI carries `capacity` and `fee` for its
// own reasons — a restaurant's capacity is seats — so reading them as charging
// data anywhere else would be wrong on most of the map.
charging: categoryOfFilter.get(matched) === 'charging' ? readChargingInfo(tags) : null,
source: 'openstreetmap',
});
}
@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest';
import { readChargingInfo } from '../../../src/nest/maps/maps.helpers';
/**
* The tag reading that turns an Overpass answer into something a traveller can filter on.
*
* Pure, so it is tested on its own: no request, no cache, no service. The query already
* asks for the whole tag set, and every one of these fields was being dropped on the way
* through.
*/
describe('readChargingInfo', () => {
// The query has always asked for `out center tags` and the projection has always
// thrown these away. Nothing here costs an extra request.
it('MAPS-CHARGE-001: reads the socket families, their count and their power', () => {
expect(readChargingInfo({
'socket:type2': '4',
'socket:type2:output': '22 kW',
'socket:ccs': '2',
'socket:ccs:output': '150',
})).toEqual({
sockets: [
{ type: 'type2', count: 4, kw: 22 },
{ type: 'ccs', count: 2, kw: 150 },
],
capacity: null,
fee: null,
});
});
it('MAPS-CHARGE-002: a socket that states nothing beyond its presence still counts', () => {
// `socket:type2=yes` is common. The family is the useful part; the rest is unknown
// rather than zero, and a filter has to be able to tell those apart.
expect(readChargingInfo({ 'socket:type2': 'yes' })?.sockets).toEqual([
{ type: 'type2', count: null, kw: null },
]);
});
it('MAPS-CHARGE-003: free text power is read off the front, comma or not', () => {
expect(readChargingInfo({ 'socket:type2': '1', 'socket:type2:output': '11kW' })?.sockets[0].kw).toBe(11);
expect(readChargingInfo({ 'socket:type2': '1', 'socket:type2:output': '2,3 kW' })?.sockets[0].kw).toBe(2.3);
expect(readChargingInfo({ 'socket:type2': '1', 'socket:type2:output': 'unknown' })?.sockets[0].kw).toBeNull();
});
it('MAPS-CHARGE-004: fee is three answers, not two', () => {
expect(readChargingInfo({ fee: 'yes' })?.fee).toBe(true);
expect(readChargingInfo({ fee: 'no' })?.fee).toBe(false);
// Half the charging stations in OSM say nothing about it, and "not stated" is not
// "free". A value nobody can act on is read as unknown; with a socket alongside it
// the station still reports, it just does not claim a price.
expect(readChargingInfo({ 'socket:type2': '2', fee: 'interval' })?.fee).toBeNull();
// On its own it says nothing at all, so there is nothing to report.
expect(readChargingInfo({ fee: 'interval' })).toBeNull();
});
it('MAPS-CHARGE-005: a station that says nothing at all is null, not an empty shell', () => {
// So the client can tell "no data" from "no sockets" without inspecting three fields.
expect(readChargingInfo({ amenity: 'charging_station', name: 'Ionity' })).toBeNull();
});
it('MAPS-CHARGE-006: a nonsense count is dropped rather than written through', () => {
expect(readChargingInfo({ 'socket:type2': '0' })?.sockets[0].count).toBeNull();
expect(readChargingInfo({ capacity: '-2' })).toBeNull();
});
});
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'اجعله توقفاً في الطريق',
'roadtrip.stop.changeKind': 'تغيير نوع التوقف',
'roadtrip.stop.backToDestination': 'العودة إلى وجهة',
'roadtrip.poi.anySocket': 'أي قابس',
'roadtrip.poi.anyPower': 'أي قدرة',
'roadtrip.poi.free': 'مجاني',
'roadtrip.poi.rest': 'استراحة',
'roadtrip.poi.campsite': 'مخيم',
'roadtrip.poi.food': 'طعام',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'لا يوجد على الطريق ما يطابق «{name}».',
'roadtrip.poi.foundFiltered': '{count} من {total} على الطريق',
'roadtrip.poi.truncated': 'في {count} من المقاطع كان هناك أكثر مما تتسع له استجابة واحدة — قلّص الممر لرؤية البقية.',
'roadtrip.poi.search': 'ابحث على طول المسار',
'roadtrip.poi.search': 'بحث',
'roadtrip.poi.searching': 'جارٍ البحث {done} من {total}',
'roadtrip.poi.capped': 'المسار طويل — تم البحث في الجزء الأول فقط.',
'roadtrip.poi.partial': 'تعذّر البحث في {count} من المقاطع — لم يستجب البحث عن الأماكن.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Qualquer tomada',
'roadtrip.poi.anyPower': 'Qualquer potência',
'roadtrip.poi.free': 'Gratuito',
'roadtrip.poi.rest': 'Área de descanso',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Comida',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Nada no trajeto corresponde a “{name}”.',
'roadtrip.poi.foundFiltered': '{count} de {total} no trajeto',
'roadtrip.poi.truncated': 'Em {count} trechos havia mais do que cabe em uma resposta — estreite o corredor para ver o restante.',
'roadtrip.poi.search': 'Buscar ao longo da rota',
'roadtrip.poi.search': 'Buscar',
'roadtrip.poi.searching': 'Buscando {done} de {total}',
'roadtrip.poi.capped': 'A rota é longa — apenas o primeiro trecho foi pesquisado.',
'roadtrip.poi.partial': 'Não foi possível pesquisar {count} trechos — a busca de locais não respondeu.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Qualsevol connector',
'roadtrip.poi.anyPower': 'Qualsevol potència',
'roadtrip.poi.free': 'Gratuït',
'roadtrip.poi.rest': 'Àrea de descans',
'roadtrip.poi.campsite': 'Càmping',
'roadtrip.poi.food': 'Menjar',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Res del trajecte no coincideix amb «{name}».',
'roadtrip.poi.foundFiltered': '{count} de {total} al trajecte',
'roadtrip.poi.truncated': 'En {count} trams hi havia més del que cap en una resposta: estreny el corredor per veure la resta.',
'roadtrip.poi.search': 'Cerca al llarg de la ruta',
'roadtrip.poi.search': 'Cerca',
'roadtrip.poi.searching': 'Cercant {done} de {total}',
'roadtrip.poi.capped': 'La ruta és llarga: només s’ha cercat el primer tram.',
'roadtrip.poi.partial': 'No s’han pogut cercar {count} trams: la cerca de llocs no ha respost.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Libovolná zásuvka',
'roadtrip.poi.anyPower': 'Libovolný výkon',
'roadtrip.poi.free': 'Zdarma',
'roadtrip.poi.rest': 'Odpočívadlo',
'roadtrip.poi.campsite': 'Kemp',
'roadtrip.poi.food': 'Jídlo',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Na trase nic neodpovídá „{name}“.',
'roadtrip.poi.foundFiltered': '{count} z {total} na trase',
'roadtrip.poi.truncated': 'Na {count} úsecích jich bylo více, než se vejde do jedné odpovědi — zužte koridor, abyste viděli zbytek.',
'roadtrip.poi.search': 'Hledat podél trasy',
'roadtrip.poi.search': 'Hledat',
'roadtrip.poi.searching': 'Hledání {done} z {total}',
'roadtrip.poi.capped': 'Trasa je dlouhá — prohledán byl jen první úsek.',
'roadtrip.poi.partial': '{count} úseků se nepodařilo prohledat — vyhledávání míst neodpovědělo.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'Zum Halt unterwegs machen',
'roadtrip.stop.changeKind': 'Art des Halts ändern',
'roadtrip.stop.backToDestination': 'Wieder ein Ziel',
'roadtrip.poi.anySocket': 'Alle Stecker',
'roadtrip.poi.anyPower': 'Alle Leistungen',
'roadtrip.poi.free': 'Kostenlos',
'roadtrip.poi.rest': 'Rastplatz',
'roadtrip.poi.campsite': 'Campingplatz',
'roadtrip.poi.food': 'Essen',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Unterwegs passt nichts zu „{name}“.',
'roadtrip.poi.foundFiltered': '{count} von {total} unterwegs',
'roadtrip.poi.truncated': 'Auf {count} Abschnitten gab es mehr, als in eine Antwort passt — den Korridor schmaler machen, um den Rest zu sehen.',
'roadtrip.poi.search': 'Entlang der Route suchen',
'roadtrip.poi.search': 'Suchen',
'roadtrip.poi.searching': 'Suche {done} von {total}',
'roadtrip.poi.capped': 'Die Route ist lang — nur der erste Abschnitt wurde durchsucht.',
'roadtrip.poi.partial': '{count} Abschnitte konnten nicht durchsucht werden — die Ortssuche hat nicht geantwortet.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Any plug',
'roadtrip.poi.anyPower': 'Any power',
'roadtrip.poi.free': 'Free',
'roadtrip.poi.rest': 'Rest area',
'roadtrip.poi.campsite': 'Campsite',
'roadtrip.poi.food': 'Food',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Nothing on the way matches “{name}”.',
'roadtrip.poi.foundFiltered': '{count} of {total} on the way',
'roadtrip.poi.truncated': '{count} stretches had more than fits in one answer — narrow the corridor to see the rest.',
'roadtrip.poi.search': 'Search along the route',
'roadtrip.poi.search': 'Search',
'roadtrip.poi.searching': 'Searching {done} of {total}',
'roadtrip.poi.capped': 'The route is long — only the first stretch was searched.',
'roadtrip.poi.partial': '{count} stretches could not be searched — the place search did not answer.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Cualquier conector',
'roadtrip.poi.anyPower': 'Cualquier potencia',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.rest': 'Área de descanso',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Comida',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Nada en la ruta coincide con «{name}».',
'roadtrip.poi.foundFiltered': '{count} de {total} en la ruta',
'roadtrip.poi.truncated': 'En {count} tramos había más de lo que cabe en una respuesta: reduce el corredor para ver el resto.',
'roadtrip.poi.search': 'Buscar a lo largo de la ruta',
'roadtrip.poi.search': 'Buscar',
'roadtrip.poi.searching': 'Buscando {done} de {total}',
'roadtrip.poi.capped': 'La ruta es larga: solo se buscó en el primer tramo.',
'roadtrip.poi.partial': 'No se pudieron buscar {count} tramos: la búsqueda de lugares no respondió.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Toute prise',
'roadtrip.poi.anyPower': 'Toute puissance',
'roadtrip.poi.free': 'Gratuit',
'roadtrip.poi.rest': 'Aire de repos',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Restauration',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Rien sur le trajet ne correspond à « {name} ».',
'roadtrip.poi.foundFiltered': '{count} sur {total} sur le trajet',
'roadtrip.poi.truncated': 'Sur {count} tronçons, il y en avait plus que ce qu’une réponse peut contenir : réduisez le corridor pour voir le reste.',
'roadtrip.poi.search': 'Chercher le long du trajet',
'roadtrip.poi.search': 'Rechercher',
'roadtrip.poi.searching': 'Recherche {done} sur {total}',
'roadtrip.poi.capped': 'L’itinéraire est long : seule la première portion a été parcourue.',
'roadtrip.poi.partial': '{count} tronçons n’ont pas pu être parcourus : la recherche de lieux n’a pas répondu.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'Μετατροπή σε στάση στον δρόμο',
'roadtrip.stop.changeKind': 'Αλλαγή είδους στάσης',
'roadtrip.stop.backToDestination': 'Πίσω σε προορισμό',
'roadtrip.poi.anySocket': 'Κάθε βύσμα',
'roadtrip.poi.anyPower': 'Κάθε ισχύς',
'roadtrip.poi.free': 'Δωρεάν',
'roadtrip.poi.rest': 'Χώρος ανάπαυσης',
'roadtrip.poi.campsite': 'Κάμπινγκ',
'roadtrip.poi.food': 'Φαγητό',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Τίποτα στη διαδρομή δεν ταιριάζει με «{name}».',
'roadtrip.poi.foundFiltered': '{count} από {total} στη διαδρομή',
'roadtrip.poi.truncated': 'Σε {count} τμήματα υπήρχαν περισσότερα από όσα χωρούν σε μία απάντηση — στενέψτε τον διάδρομο για να δείτε τα υπόλοιπα.',
'roadtrip.poi.search': 'Αναζήτηση κατά μήκος της διαδρομής',
'roadtrip.poi.search': 'Αναζήτηση',
'roadtrip.poi.searching': 'Αναζήτηση {done} από {total}',
'roadtrip.poi.capped': 'Η διαδρομή είναι μεγάλη — αναζητήθηκε μόνο το πρώτο τμήμα.',
'roadtrip.poi.partial': 'Δεν ήταν δυνατή η αναζήτηση σε {count} τμήματα — η αναζήτηση τοποθεσιών δεν απάντησε.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Bármely csatlakozó',
'roadtrip.poi.anyPower': 'Bármely teljesítmény',
'roadtrip.poi.free': 'Ingyenes',
'roadtrip.poi.rest': 'Pihenőhely',
'roadtrip.poi.campsite': 'Kemping',
'roadtrip.poi.food': 'Étkezés',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Az útvonalon semmi sem egyezik ezzel: „{name}”.',
'roadtrip.poi.foundFiltered': '{count} / {total} az útvonalon',
'roadtrip.poi.truncated': '{count} szakaszon több volt, mint amennyi egy válaszba fér — szűkítse a folyosót a többi megtekintéséhez.',
'roadtrip.poi.search': 'Keresés az útvonal mentén',
'roadtrip.poi.search': 'Keresés',
'roadtrip.poi.searching': 'Keresés {done}/{total}',
'roadtrip.poi.capped': 'Az útvonal hosszú – csak az első szakaszon kerestünk.',
'roadtrip.poi.partial': '{count} szakaszon nem sikerült keresni – a helykeresés nem válaszolt.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'Jadikan perhentian di jalan',
'roadtrip.stop.changeKind': 'Ubah jenis perhentian',
'roadtrip.stop.backToDestination': 'Kembali jadi tujuan',
'roadtrip.poi.anySocket': 'Semua colokan',
'roadtrip.poi.anyPower': 'Semua daya',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.rest': 'Tempat istirahat',
'roadtrip.poi.campsite': 'Perkemahan',
'roadtrip.poi.food': 'Makanan',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Tidak ada di rute yang cocok dengan “{name}”.',
'roadtrip.poi.foundFiltered': '{count} dari {total} di rute',
'roadtrip.poi.truncated': 'Di {count} ruas ada lebih banyak daripada yang muat dalam satu jawaban — persempit koridor untuk melihat sisanya.',
'roadtrip.poi.search': 'Cari di sepanjang rute',
'roadtrip.poi.search': 'Cari',
'roadtrip.poi.searching': 'Mencari {done} dari {total}',
'roadtrip.poi.capped': 'Rute panjang — hanya bagian pertama yang dicari.',
'roadtrip.poi.partial': '{count} ruas tidak dapat dicari — pencarian tempat tidak merespons.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Qualsiasi presa',
'roadtrip.poi.anyPower': 'Qualsiasi potenza',
'roadtrip.poi.free': 'Gratuito',
'roadtrip.poi.rest': 'Area di sosta',
'roadtrip.poi.campsite': 'Campeggio',
'roadtrip.poi.food': 'Ristoro',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Lungo il percorso non c’è nulla che corrisponda a “{name}”.',
'roadtrip.poi.foundFiltered': '{count} di {total} lungo il percorso',
'roadtrip.poi.truncated': 'Su {count} tratti ce n’erano più di quanti ne stiano in una risposta: restringi il corridoio per vedere il resto.',
'roadtrip.poi.search': 'Cerca lungo il percorso',
'roadtrip.poi.search': 'Cerca',
'roadtrip.poi.searching': 'Ricerca {done} di {total}',
'roadtrip.poi.capped': 'Il percorso è lungo: è stato cercato solo il primo tratto.',
'roadtrip.poi.partial': 'Non è stato possibile cercare in {count} tratti: la ricerca dei luoghi non ha risposto.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': '途中の立ち寄りにする',
'roadtrip.stop.changeKind': '立ち寄りの種類を変更',
'roadtrip.stop.backToDestination': '目的地に戻す',
'roadtrip.poi.anySocket': 'すべてのプラグ',
'roadtrip.poi.anyPower': 'すべての出力',
'roadtrip.poi.free': '無料',
'roadtrip.poi.rest': 'サービスエリア',
'roadtrip.poi.campsite': 'キャンプ場',
'roadtrip.poi.food': '食事',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'ルート沿いに「{name}」に一致するものはありません。',
'roadtrip.poi.foundFiltered': 'ルート沿いの {total} 件中 {count} 件',
'roadtrip.poi.truncated': '{count} 区間で一度に返せる件数を超えました。回廊を狭めると残りを確認できます。',
'roadtrip.poi.search': 'ルート沿いを検索',
'roadtrip.poi.search': '検索',
'roadtrip.poi.searching': '検索中 {done}/{total}',
'roadtrip.poi.capped': 'ルートが長いため、最初の区間のみ検索しました。',
'roadtrip.poi.partial': '{count} 区間を検索できませんでした。場所の検索が応答しませんでした。',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': '경유 정차로 변경',
'roadtrip.stop.changeKind': '정차 유형 변경',
'roadtrip.stop.backToDestination': '다시 목적지로',
'roadtrip.poi.anySocket': '모든 플러그',
'roadtrip.poi.anyPower': '모든 출력',
'roadtrip.poi.free': '무료',
'roadtrip.poi.rest': '휴게소',
'roadtrip.poi.campsite': '캠핑장',
'roadtrip.poi.food': '식사',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': '경로상에 “{name}”와(과) 일치하는 곳이 없습니다.',
'roadtrip.poi.foundFiltered': '경로상 {total}곳 중 {count}곳',
'roadtrip.poi.truncated': '{count}개 구간에서 한 번에 반환할 수 있는 수를 넘었습니다. 회랑을 좁히면 나머지를 볼 수 있습니다.',
'roadtrip.poi.search': '경로 주변 검색',
'roadtrip.poi.search': '검색',
'roadtrip.poi.searching': '검색 중 {done}/{total}',
'roadtrip.poi.capped': '경로가 길어 앞부분만 검색했습니다.',
'roadtrip.poi.partial': '{count}개 구간을 검색하지 못했습니다. 장소 검색이 응답하지 않았습니다.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'Maak er een stop onderweg van',
'roadtrip.stop.changeKind': 'Soort stop wijzigen',
'roadtrip.stop.backToDestination': 'Weer een bestemming',
'roadtrip.poi.anySocket': 'Elke stekker',
'roadtrip.poi.anyPower': 'Elk vermogen',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.rest': 'Rustplaats',
'roadtrip.poi.campsite': 'Camping',
'roadtrip.poi.food': 'Eten',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Niets onderweg komt overeen met “{name}”.',
'roadtrip.poi.foundFiltered': '{count} van {total} onderweg',
'roadtrip.poi.truncated': 'Op {count} stukken waren er meer dan in één antwoord passen — maak de corridor smaller om de rest te zien.',
'roadtrip.poi.search': 'Langs de route zoeken',
'roadtrip.poi.search': 'Zoeken',
'roadtrip.poi.searching': 'Zoeken {done} van {total}',
'roadtrip.poi.capped': 'De route is lang — alleen het eerste deel is doorzocht.',
'roadtrip.poi.partial': '{count} stukken konden niet worden doorzocht — de plaatszoekfunctie antwoordde niet.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Dowolna wtyczka',
'roadtrip.poi.anyPower': 'Dowolna moc',
'roadtrip.poi.free': 'Bezpłatnie',
'roadtrip.poi.rest': 'Miejsce odpoczynku',
'roadtrip.poi.campsite': 'Kemping',
'roadtrip.poi.food': 'Jedzenie',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Nic po drodze nie pasuje do „{name}”.',
'roadtrip.poi.foundFiltered': '{count} z {total} po drodze',
'roadtrip.poi.truncated': 'Na {count} odcinkach było więcej, niż mieści się w jednej odpowiedzi — zawęź korytarz, aby zobaczyć resztę.',
'roadtrip.poi.search': 'Szukaj wzdłuż trasy',
'roadtrip.poi.search': 'Szukaj',
'roadtrip.poi.searching': 'Szukanie {done} z {total}',
'roadtrip.poi.capped': 'Trasa jest długa — przeszukano tylko pierwszy odcinek.',
'roadtrip.poi.partial': 'Nie udało się przeszukać {count} odcinków — wyszukiwanie miejsc nie odpowiedziało.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'Сделать остановкой в пути',
'roadtrip.stop.changeKind': 'Изменить тип остановки',
'roadtrip.stop.backToDestination': 'Снова пункт назначения',
'roadtrip.poi.anySocket': 'Любой разъём',
'roadtrip.poi.anyPower': 'Любая мощность',
'roadtrip.poi.free': 'Бесплатно',
'roadtrip.poi.rest': 'Зона отдыха',
'roadtrip.poi.campsite': 'Кемпинг',
'roadtrip.poi.food': 'Еда',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'По пути ничего не соответствует «{name}».',
'roadtrip.poi.foundFiltered': '{count} из {total} по пути',
'roadtrip.poi.truncated': 'На {count} участках нашлось больше, чем помещается в один ответ — сузьте коридор, чтобы увидеть остальное.',
'roadtrip.poi.search': 'Искать вдоль маршрута',
'roadtrip.poi.search': 'Искать',
'roadtrip.poi.searching': 'Поиск {done} из {total}',
'roadtrip.poi.capped': 'Маршрут длинный — просмотрен только первый участок.',
'roadtrip.poi.partial': 'Не удалось просмотреть участков: {count} — поиск мест не ответил.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Alla kontakter',
'roadtrip.poi.anyPower': 'Alla effekter',
'roadtrip.poi.free': 'Gratis',
'roadtrip.poi.rest': 'Rastplats',
'roadtrip.poi.campsite': 'Campingplats',
'roadtrip.poi.food': 'Mat',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Inget längs vägen matchar ”{name}”.',
'roadtrip.poi.foundFiltered': '{count} av {total} längs vägen',
'roadtrip.poi.truncated': 'På {count} sträckor fanns fler än vad som får plats i ett svar – gör korridoren smalare för att se resten.',
'roadtrip.poi.search': 'Sök längs rutten',
'roadtrip.poi.search': 'Sök',
'roadtrip.poi.searching': 'Söker {done} av {total}',
'roadtrip.poi.capped': 'Rutten är lång – bara första sträckan genomsöktes.',
'roadtrip.poi.partial': '{count} sträckor kunde inte genomsökas – platssökningen svarade inte.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Tum soketler',
'roadtrip.poi.anyPower': 'Tum gucler',
'roadtrip.poi.free': 'Ucretsiz',
'roadtrip.poi.rest': 'Dinlenme alanı',
'roadtrip.poi.campsite': 'Kamp alanı',
'roadtrip.poi.food': 'Yemek',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Yol boyunca “{name}” ile eşleşen bir şey yok.',
'roadtrip.poi.foundFiltered': 'yol üzerinde {total} içinden {count}',
'roadtrip.poi.truncated': '{count} bölümde bir yanıta sığandan fazlası vardı — geri kalanı görmek için koridoru daraltın.',
'roadtrip.poi.search': 'Rota boyunca ara',
'roadtrip.poi.search': 'Ara',
'roadtrip.poi.searching': 'Aranıyor {done}/{total}',
'roadtrip.poi.capped': 'Rota uzun — yalnızca ilk bölüm arandı.',
'roadtrip.poi.partial': '{count} bölüm aranamadı — yer araması yanıt vermedi.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': 'Зробити зупинкою в дорозі',
'roadtrip.stop.changeKind': 'Змінити тип зупинки',
'roadtrip.stop.backToDestination': 'Знову пункт призначення',
'roadtrip.poi.anySocket': 'Будь-який роз’єм',
'roadtrip.poi.anyPower': 'Будь-яка потужність',
'roadtrip.poi.free': 'Безкоштовно',
'roadtrip.poi.rest': 'Зона відпочинку',
'roadtrip.poi.campsite': 'Кемпінг',
'roadtrip.poi.food': 'Їжа',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Дорогою ніщо не відповідає «{name}».',
'roadtrip.poi.foundFiltered': '{count} з {total} дорогою',
'roadtrip.poi.truncated': 'На {count} відрізках знайшлося більше, ніж уміщується в одну відповідь — звузьте коридор, щоб побачити решту.',
'roadtrip.poi.search': 'Шукати вздовж маршруту',
'roadtrip.poi.search': 'Шукати',
'roadtrip.poi.searching': 'Пошук {done} з {total}',
'roadtrip.poi.capped': 'Маршрут довгий — переглянуто лише перший відрізок.',
'roadtrip.poi.partial': 'Не вдалося переглянути відрізків: {count} — пошук місць не відповів.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'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.anySocket': 'Mọi loại phích',
'roadtrip.poi.anyPower': 'Mọi công suất',
'roadtrip.poi.free': 'Miễn phí',
'roadtrip.poi.rest': 'Trạm dừng nghỉ',
'roadtrip.poi.campsite': 'Khu cắm trại',
'roadtrip.poi.food': 'Đồ ăn',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': 'Không có gì trên đường khớp với “{name}”.',
'roadtrip.poi.foundFiltered': '{count} trong {total} trên đường',
'roadtrip.poi.truncated': 'Có {count} đoạn nhiều hơn mức một phản hồi chứa được — thu hẹp hành lang để xem phần còn lại.',
'roadtrip.poi.search': 'Tìm dọc lộ trình',
'roadtrip.poi.search': 'Tìm kiếm',
'roadtrip.poi.searching': 'Đang tìm {done}/{total}',
'roadtrip.poi.capped': 'Lộ trình dài — chỉ đoạn đầu được tìm kiếm.',
'roadtrip.poi.partial': 'Không tìm được {count} đoạn — tìm kiếm địa điểm không phản hồi.',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': '設為途中停靠',
'roadtrip.stop.changeKind': '變更停靠類型',
'roadtrip.stop.backToDestination': '恢復為目的地',
'roadtrip.poi.anySocket': '任意接口',
'roadtrip.poi.anyPower': '任意功率',
'roadtrip.poi.free': '免費',
'roadtrip.poi.rest': '休息站',
'roadtrip.poi.campsite': '露營地',
'roadtrip.poi.food': '餐飲',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': '沿途沒有與「{name}」相符的地點。',
'roadtrip.poi.foundFiltered': '沿途 {total} 個中的 {count} 個',
'roadtrip.poi.truncated': '有 {count} 段的結果超出單次回應上限,縮小走廊寬度可查看其餘部分。',
'roadtrip.poi.search': '沿路線搜尋',
'roadtrip.poi.search': '搜尋',
'roadtrip.poi.searching': '搜尋中 {done}/{total}',
'roadtrip.poi.capped': '路線較長,僅搜尋了前段。',
'roadtrip.poi.partial': '有 {count} 段無法搜尋 — 地點搜尋沒有回應。',
+4 -1
View File
@@ -41,6 +41,9 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.makeService': '设为途中停靠',
'roadtrip.stop.changeKind': '更改停靠类型',
'roadtrip.stop.backToDestination': '恢复为目的地',
'roadtrip.poi.anySocket': '任意接口',
'roadtrip.poi.anyPower': '任意功率',
'roadtrip.poi.free': '免费',
'roadtrip.poi.rest': '服务区',
'roadtrip.poi.campsite': '露营地',
'roadtrip.poi.food': '餐饮',
@@ -50,7 +53,7 @@ const roadtrip: TranslationStrings = {
'roadtrip.poi.noMatch': '沿途没有与“{name}”匹配的地点。',
'roadtrip.poi.foundFiltered': '沿途 {total} 个中的 {count} 个',
'roadtrip.poi.truncated': '有 {count} 段的结果超出单次响应上限,缩小走廊宽度可查看其余部分。',
'roadtrip.poi.search': '沿路线搜索',
'roadtrip.poi.search': '搜索',
'roadtrip.poi.searching': '搜索中 {done}/{total}',
'roadtrip.poi.capped': '路线较长,仅搜索了前段。',
'roadtrip.poi.partial': '有 {count} 段无法搜索 — 地点搜索没有响应。',