mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
feat(roadtrip): state a range as the car it comes from
A range could only be typed as one number, and that is the number fewest travellers actually know. A tank size is on the filler flap, a consumption is in the manual, a battery capacity and a kWh per hundred are on the data sheet. All of them divide into the same figure, so the dialog now takes either and does the division itself. What the parts say wins over what was typed, because it is the more specific answer, and the range row goes read-only when they do so there is never a second answer to the same question. Of ABRP's list only battery wear is here. Plug type, reference speed, relative speed and drive style shape a consumption PREDICTION; this addon does not predict one, it divides a capacity by a consumption the traveller states, so those fields would add typing without moving a kilometre. Wear is different in kind: it shrinks the capacity itself, and on an older car by enough to decide whether the last leg of a day arrives. Storage stays metric throughout, the same rule the range itself follows. An imperial traveller types gallons, miles per gallon and kWh per 100 miles and gets them back unchanged; mpg is the inverse of litres per hundred rather than a scale factor, which a straight multiplication would have stored as a car four times as thirsty. The gauge under the picker is the argument that these two ways of stating a range are the same thing, so it answers while the figures are still being typed rather than after the field is left. It draws the tank the day it left the factory: the filled part is what a stop actually puts back in, the hatched tail is what age has taken off, and both are things the bare number cannot say. Also here, from the same round: a fill-up that stops short of full. Nobody charges to 100 % on the road, and counting every stop as a full tank overstated everything after it by that last fifth. Stops that refuel now carry a badge saying how far they fill.
This commit is contained in:
@@ -5,7 +5,7 @@ import { useSettingsStore } from '../../store/settingsStore'
|
||||
import RoadtripLimitsCard from './RoadtripLimitsCard'
|
||||
|
||||
/**
|
||||
* FE-ROADTRIP-LIMITS-001..008 — the three numbers that decide every warning.
|
||||
* FE-ROADTRIP-LIMITS-001..012 — the numbers that decide every warning.
|
||||
*
|
||||
* The one that matters is when a number is written. These fields had no local
|
||||
* state, so every keystroke was a settings PUT: typing "180" sent three, and
|
||||
@@ -18,7 +18,7 @@ function open(onSave?: (key: string, value: number) => void) {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
}
|
||||
|
||||
/** The three inputs, in the order the dialog lists them. */
|
||||
/** Every number field, in the order the dialog lists them. */
|
||||
const inputs = () => screen.getAllByRole('spinbutton') as HTMLInputElement[]
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -127,4 +127,75 @@ describe('RoadtripLimitsCard', () => {
|
||||
// handler to call, and must not assume one.
|
||||
expect(() => fireEvent.blur(leg)).not.toThrow()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-LIMITS-009: naming the kind of car brings out its own figures', () => {
|
||||
useSettingsStore.setState({
|
||||
settings: { roadtrip_vehicle: 'electric', distance_unit: 'metric' } as never,
|
||||
})
|
||||
open(vi.fn())
|
||||
|
||||
// Leg, day, battery, consumption, wear, range, fill. The three in the middle
|
||||
// do not exist until there is a kind of car to have them.
|
||||
expect(inputs()).toHaveLength(7)
|
||||
expect(screen.getByText('kWh')).toBeInTheDocument()
|
||||
expect(screen.getByText('kWh/100 km')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-LIMITS-010: a complete pair takes the range field over', () => {
|
||||
useSettingsStore.setState({
|
||||
settings: {
|
||||
roadtrip_vehicle: 'electric',
|
||||
roadtrip_battery_kwh: 58,
|
||||
roadtrip_kwh_per_100: 16,
|
||||
// Deliberately different from what the figures work out to: the point of
|
||||
// the row going read-only is that there is no longer a second answer.
|
||||
roadtrip_range_km: 500,
|
||||
distance_unit: 'metric',
|
||||
} as never,
|
||||
})
|
||||
open(vi.fn())
|
||||
|
||||
expect(screen.getByTestId('limit-derived')).toHaveTextContent('363')
|
||||
// Leg, day, battery, consumption, wear, fill — the range input is gone.
|
||||
expect(inputs()).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-LIMITS-011: gallons typed by an imperial reader are stored as litres', () => {
|
||||
useSettingsStore.setState({
|
||||
settings: { roadtrip_vehicle: 'combustion', distance_unit: 'imperial' } as never,
|
||||
})
|
||||
const onSave = vi.fn()
|
||||
open(onSave)
|
||||
|
||||
// Leg, day, tank, consumption, range, fill.
|
||||
const tank = inputs()[2]
|
||||
fireEvent.change(tank, { target: { value: '15' } })
|
||||
fireEvent.blur(tank)
|
||||
|
||||
// 15 US gallons, not 15 litres. Storing the number as typed would have made
|
||||
// every fuel warning fire at a quarter of the right distance.
|
||||
expect(onSave).toHaveBeenCalledWith('roadtrip_tank_litres', 56.78)
|
||||
})
|
||||
it('FE-ROADTRIP-LIMITS-012: the range answers while the figure is still being typed', () => {
|
||||
useSettingsStore.setState({
|
||||
settings: {
|
||||
roadtrip_vehicle: 'electric',
|
||||
roadtrip_battery_kwh: 58,
|
||||
roadtrip_kwh_per_100: 16,
|
||||
distance_unit: 'metric',
|
||||
} as never,
|
||||
})
|
||||
const onSave = vi.fn()
|
||||
open(onSave)
|
||||
|
||||
// Leg, day, battery, consumption, wear, fill — the range row is read-only here.
|
||||
const consumption = inputs()[3]
|
||||
fireEvent.change(consumption, { target: { value: '20' } })
|
||||
|
||||
// 58 kWh at 20 per hundred is 290 km, shown before anything is written: the gauge
|
||||
// is the argument that these figures ARE the range, and it only makes it if it
|
||||
// answers now rather than after the field is left.
|
||||
expect(screen.getAllByText('290').length).toBeGreaterThan(0)
|
||||
expect(onSave).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Clock, Fuel, CalendarClock, SlidersHorizontal, ChevronRight, Coins, Signpost, Ship, Car, Zap } from 'lucide-react'
|
||||
import {
|
||||
Clock, Fuel, CalendarClock, SlidersHorizontal, ChevronRight, Coins, Signpost, Ship,
|
||||
Car, Zap, BatteryCharging, BatteryFull, BatteryWarning, Gauge, Route, Sparkles,
|
||||
} from 'lucide-react'
|
||||
import Modal from '../shared/Modal'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import { useTranslation } from '../../i18n/TranslationContext'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { convertDistance, formatDistance } from '../../utils/units'
|
||||
import { formatDurationShort, parseAvoid, serializeAvoid, AVOIDABLE } from './roadtripModel'
|
||||
import { convertDistance, formatDistance, getDistanceUnitLabel } from '../../utils/units'
|
||||
import { formatDurationShort, parseAvoid, serializeAvoid, type VehicleKind } from './roadtripModel'
|
||||
import { effectiveRangeKm, rangeFromSpec, showSpec, storeSpec, specUnit, type SpecKey, type VehicleSpec } from './vehicleRange'
|
||||
import { valhallaAvailable } from '../Map/valhallaRoute'
|
||||
import ToggleSwitch from '../Settings/ToggleSwitch'
|
||||
import type { DistanceUnit, RouteAvoidClass } from '../../types'
|
||||
import { FS } from './typeScale'
|
||||
|
||||
/**
|
||||
* The three numbers that decide when the rail speaks up about the driving.
|
||||
* Everything that decides when the rail speaks up about the driving.
|
||||
*
|
||||
* Behind a button rather than laid out in the column, because this is set once and then
|
||||
* read never: a permanent card costs three rows of a narrow sidebar for something most
|
||||
* read never: a permanent card costs rows of a narrow sidebar for something most
|
||||
* travellers touch on the first day of planning and leave alone after. The button carries
|
||||
* the current answer, so what is set stays visible without the form being.
|
||||
* the current answers, so what is set stays visible without the form being.
|
||||
*
|
||||
* All three are personal rather than instance configuration: how long somebody is willing
|
||||
* to sit behind a wheel, and how far their car goes on a tank, belong to the traveller.
|
||||
* They are plain per-user settings, which is why this needs no migration.
|
||||
* All of it is personal rather than instance configuration: how long somebody is willing
|
||||
* to sit behind a wheel, and what their car is, belong to the traveller. They are plain
|
||||
* per-user settings, which is why this needs no migration.
|
||||
*
|
||||
* Nothing here suggests a stop, adds one, or knows anything about a vehicle. OSM has no
|
||||
* idea what car this is, and neither has TREK, so the range is a number the traveller
|
||||
* types. Furkot does exactly the same, for the same reason.
|
||||
* The dialog has two halves because the settings are two kinds. The left is the trip: how
|
||||
* long a day may be and what the road may not be. The right is the car, and it is the
|
||||
* half that earns the width — a range can be typed as one number or worked out from what
|
||||
* the vehicle is made of, and the gauge shows the second turning into the first as it is
|
||||
* typed. Nothing here asks the router about a vehicle; OSM has no idea what car this is,
|
||||
* and neither has TREK.
|
||||
*/
|
||||
|
||||
/** Empty means no limit, and so does zero — both are stored as 0 and read as off. */
|
||||
@@ -34,12 +41,41 @@ function parseLimit(raw: string): number {
|
||||
return Number.isFinite(n) && n > 0 ? n : 0
|
||||
}
|
||||
|
||||
function LimitRow({ icon: Icon, label, suffix, value, placeholder, onChange }: {
|
||||
/**
|
||||
* One number in the form, or one number the form worked out.
|
||||
*
|
||||
* `derived` turns the row read-only and states where the figure came from. The row keeps
|
||||
* its shape either way, because a range that switched between an input and a line of text
|
||||
* would move every row under it the moment a consumption was typed.
|
||||
*/
|
||||
function LimitRow({ icon: Icon, label, suffix, value, placeholder, step, derived, wide, onDraft, onChange }: {
|
||||
icon: typeof Clock
|
||||
label: string
|
||||
suffix: string
|
||||
value: number | undefined
|
||||
placeholder: string
|
||||
step?: number
|
||||
derived?: string
|
||||
/**
|
||||
* Room for a unit longer than a symbol, set per section rather than per row.
|
||||
*
|
||||
* "kWh/100 km" and "L/100 km" are units people actually quote, and a column sized for
|
||||
* "min" wrapped them onto a second line, which made one row of the vehicle section
|
||||
* taller than the rest. Sizing it from the string would have done the same thing
|
||||
* sideways: only the consumption row would widen, and the section's right edge would
|
||||
* come out ragged.
|
||||
*/
|
||||
wide?: boolean
|
||||
/**
|
||||
* What is in the field right now, on every keystroke, saved or not.
|
||||
*
|
||||
* Separate from `onChange` because the two answer different questions. `onChange` is
|
||||
* "this is the new setting", which must stay rare — it is an HTTP write. This one is
|
||||
* "this is what the traveller is looking at", which the gauge needs on every keystroke
|
||||
* or it would sit still until the field is left and the promise it makes, that these
|
||||
* figures are the range, would only pay out after the fact.
|
||||
*/
|
||||
onDraft?: (next: number) => void
|
||||
onChange: (next: number) => void
|
||||
}): React.ReactElement {
|
||||
// Typed here, saved on blur or Enter. Writing on every keystroke sent one
|
||||
@@ -60,21 +96,37 @@ function LimitRow({ icon: Icon, label, suffix, value, placeholder, onChange }: {
|
||||
<label className="flex items-center gap-3">
|
||||
<Icon size={16} className="shrink-0 text-content-faint" aria-hidden />
|
||||
<span className="min-w-0 flex-1 text-body text-content-secondary">{label}</span>
|
||||
<input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
value={shown}
|
||||
placeholder={placeholder}
|
||||
onChange={e => setDraft(e.target.value)}
|
||||
onBlur={commit}
|
||||
// Committed here rather than by blurring and letting onBlur do it: a
|
||||
// second commit is a no-op anyway, and going through blur made Enter
|
||||
// depend on focus handling instead of on the key.
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(); (e.target as HTMLInputElement).blur() } }}
|
||||
className="w-20 rounded-lg border border-edge bg-surface px-2 py-1 text-end text-body tabular-nums text-content focus:border-accent focus:outline-none"
|
||||
/>
|
||||
<span className="w-7 text-caption text-content-faint">{suffix}</span>
|
||||
{derived ? (
|
||||
// A mark rather than a word: "computed" is eight characters in English and
|
||||
// fourteen in half the locales, and the column is as wide as the input it
|
||||
// replaces. The sentence lives in the title and in the screen-reader text.
|
||||
<span
|
||||
className={`flex items-center justify-end gap-1.5 ${wide ? 'w-16' : 'w-20'}`}
|
||||
title={derived}
|
||||
data-testid="limit-derived"
|
||||
>
|
||||
<Sparkles size={12} className="shrink-0 text-accent-on" aria-hidden />
|
||||
<span className="text-body font-semibold tabular-nums text-content">{value ?? 0}</span>
|
||||
<span className="sr-only">{derived}</span>
|
||||
</span>
|
||||
) : (
|
||||
<input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
min={0}
|
||||
step={step ?? 1}
|
||||
value={shown}
|
||||
placeholder={placeholder}
|
||||
onChange={e => { setDraft(e.target.value); onDraft?.(parseLimit(e.target.value)) }}
|
||||
onBlur={commit}
|
||||
// Committed here rather than by blurring and letting onBlur do it: a
|
||||
// second commit is a no-op anyway, and going through blur made Enter
|
||||
// depend on focus handling instead of on the key.
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(); (e.target as HTMLInputElement).blur() } }}
|
||||
className={`${wide ? 'w-16' : 'w-20'} rounded-lg border border-edge bg-surface px-2 py-1 text-end text-body tabular-nums text-content focus:border-accent focus:outline-none`}
|
||||
/>
|
||||
)}
|
||||
<span className={`${wide ? 'w-[86px]' : 'w-7'} whitespace-nowrap text-caption text-content-faint`}>{suffix}</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -104,13 +156,160 @@ function AvoidRow({ icon: Icon, label, on, disabled, onToggle }: {
|
||||
)
|
||||
}
|
||||
|
||||
/** The name of one half of the form. */
|
||||
function SectionTitle({ icon: Icon, children }: {
|
||||
icon: typeof Clock
|
||||
children: React.ReactNode
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<p className="flex items-center gap-2 text-caption font-semibold uppercase tracking-wide text-content-faint">
|
||||
<Icon size={13} aria-hidden />
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The range, drawn as the thing it describes.
|
||||
*
|
||||
* The bar is the tank the day it left the factory, left to right, and it carries the two
|
||||
* figures a bare number cannot say on its own:
|
||||
*
|
||||
* - The filled part is what a stop actually puts back in. Nobody charges to 100 % on the
|
||||
* road, and a traveller who set 80 % is looking at four fifths of a bar, which is the
|
||||
* honest picture of what the next leg has.
|
||||
* - The hatched tail is what age has taken off an electric car's battery. It is already
|
||||
* out of the number above, so without the tail that figure looks like an unexplained
|
||||
* disagreement with the car's own data sheet.
|
||||
*
|
||||
* Widths animate because this sits directly under the fields that feed it: typing a
|
||||
* consumption and watching the bar move is what makes the two ways of stating a range
|
||||
* visibly the same thing.
|
||||
*/
|
||||
function RangeGauge({ rangeKm, fillPercent, wearPercent, unit, electric, t }: {
|
||||
rangeKm: number | null
|
||||
fillPercent: number | null
|
||||
wearPercent: number | null
|
||||
unit: DistanceUnit
|
||||
electric: boolean
|
||||
t: (key: string, params?: Record<string, string | number>) => string
|
||||
}): React.ReactElement {
|
||||
// Capped where the arithmetic caps it, so the picture cannot claim a battery is gone.
|
||||
const wear = rangeKm ? Math.min(90, Math.max(0, wearPercent ?? 0)) : 0
|
||||
const fill = fillPercent && fillPercent > 0 && fillPercent < 100 ? fillPercent : 100
|
||||
const usable = 100 - wear
|
||||
const filled = rangeKm ? (usable * fill) / 100 : 0
|
||||
const rest = 100 - wear - filled
|
||||
// Split rather than formatted whole, so the unit can sit smaller beside the figure.
|
||||
// The rounding is formatDistance's own, because the badge on the trigger uses that and
|
||||
// the two must not disagree by a tenth.
|
||||
const shown = rangeKm ? Math.round(convertDistance(rangeKm, unit) * 10) / 10 : null
|
||||
const stripes = 'repeating-linear-gradient(135deg, var(--border-primary) 0 3px, transparent 3px 6px)'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-edge-faint bg-surface-tertiary px-4 py-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-subtle text-accent-on">
|
||||
{electric ? <Zap size={18} aria-hidden /> : <Fuel size={18} aria-hidden />}
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="flex items-baseline gap-1">
|
||||
<span className="font-semibold tabular-nums leading-none text-content" style={{ fontSize: FS.total }}>
|
||||
{shown ?? '—'}
|
||||
</span>
|
||||
{shown !== null ? (
|
||||
<span className="font-semibold leading-none text-content-muted" style={{ fontSize: FS.totalUnit }}>
|
||||
{getDistanceUnitLabel(unit)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="truncate text-caption text-content-muted">
|
||||
{shown === null
|
||||
? t('roadtrip.limit.rangeEmpty')
|
||||
: t(electric ? 'roadtrip.limit.perCharge' : 'roadtrip.limit.perFill')}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* The quarter marks are painted over the whole bar rather than drawn into each
|
||||
segment, so they stay put while the segments under them move. They are the
|
||||
card's own colour, which makes them read as cut-outs: visible across the filled
|
||||
part, invisible over the empty track, where there is nothing to divide. */}
|
||||
<div className="relative h-3.5 w-full overflow-hidden rounded-full bg-surface">
|
||||
<div className="flex h-full w-full">
|
||||
<div
|
||||
className="h-full transition-[width] duration-500 ease-out"
|
||||
style={{ width: `${filled}%`, background: 'linear-gradient(90deg, var(--accent-hover), var(--accent))' }}
|
||||
/>
|
||||
<div className="h-full transition-[width] duration-500 ease-out" style={{ width: `${rest}%` }} />
|
||||
<div
|
||||
className="h-full transition-[width] duration-500 ease-out"
|
||||
style={{ width: `${wear}%`, backgroundImage: stripes }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'repeating-linear-gradient(90deg, transparent 0, transparent calc(25% - 1px), var(--bg-tertiary) calc(25% - 1px), var(--bg-tertiary) 25%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-caption leading-snug text-content-faint">
|
||||
{shown === null ? (
|
||||
<span>{t('roadtrip.limit.rangeEmptyHint')}</span>
|
||||
) : (
|
||||
<>
|
||||
<span>
|
||||
{fill < 100
|
||||
? t('roadtrip.limit.afterFill', { percent: fill, distance: formatDistance((rangeKm! * fill) / 100, unit) })
|
||||
: t('roadtrip.limit.fullNote')}
|
||||
</span>
|
||||
{/* The swatch is what ties the sentence to the tail of the bar. Without it the
|
||||
hatching is an unexplained stripe and the sentence an unexplained loss. */}
|
||||
{wear > 0 ? (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="h-2 w-3.5 shrink-0 rounded-sm" style={{ backgroundImage: stripes }} aria-hidden />
|
||||
{t('roadtrip.limit.wearNote', { percent: wear })}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Not said, petrol, electric. "Not said" first, because it is what everybody starts on. */
|
||||
const VEHICLES: { key: 'combustion' | 'electric' | null; labelKey: string; Icon: typeof Car }[] = [
|
||||
const VEHICLES: { key: VehicleKind | null; labelKey: string; Icon: typeof Car }[] = [
|
||||
{ key: null, labelKey: 'roadtrip.limit.vehicleAny', Icon: Car },
|
||||
{ key: 'combustion', labelKey: 'roadtrip.limit.vehicleCombustion', Icon: Fuel },
|
||||
{ key: 'electric', labelKey: 'roadtrip.limit.vehicleElectric', Icon: Zap },
|
||||
]
|
||||
|
||||
/**
|
||||
* What each kind of car is made of, and where each figure is kept.
|
||||
*
|
||||
* Two rows for petrol and three for electric, because that is the difference that
|
||||
* matters: a tank does not lose capacity to the years and a battery does. Everything else
|
||||
* ABRP asks for — plug type, reference speed, relative speed, drive style — shapes a
|
||||
* consumption PREDICTION. This dialog does not predict one; it divides a capacity by a
|
||||
* consumption the traveller states, so those fields would add typing without moving a
|
||||
* single kilometre of the answer.
|
||||
*/
|
||||
const SPEC_ROWS: Record<VehicleKind, { key: SpecKey; setting: string; Icon: typeof Fuel; labelKey: string; step: number }[]> = {
|
||||
combustion: [
|
||||
{ key: 'tankLitres', setting: 'roadtrip_tank_litres', Icon: Fuel, labelKey: 'roadtrip.limit.tankLabel', step: 1 },
|
||||
{ key: 'litresPer100', setting: 'roadtrip_litres_per_100', Icon: Gauge, labelKey: 'roadtrip.limit.useLabel', step: 0.1 },
|
||||
],
|
||||
electric: [
|
||||
{ key: 'batteryKwh', setting: 'roadtrip_battery_kwh', Icon: BatteryFull, labelKey: 'roadtrip.limit.batteryLabel', step: 1 },
|
||||
{ key: 'kwhPer100', setting: 'roadtrip_kwh_per_100', Icon: Gauge, labelKey: 'roadtrip.limit.useLabel', step: 0.1 },
|
||||
{ key: 'degradationPercent', setting: 'roadtrip_battery_degradation', Icon: BatteryWarning, labelKey: 'roadtrip.limit.wearLabel', step: 1 },
|
||||
],
|
||||
}
|
||||
|
||||
export default function RoadtripLimitsCard({ onSave }: {
|
||||
/**
|
||||
* Persists one setting. Absent leaves the dialog read-only.
|
||||
@@ -124,6 +323,7 @@ export default function RoadtripLimitsCard({ onSave }: {
|
||||
const settings = useSettingsStore(s => s.settings)
|
||||
const [open, setOpen] = useState(false)
|
||||
const distanceUnit: DistanceUnit = settings.distance_unit === 'imperial' ? 'imperial' : 'metric'
|
||||
const imperial = distanceUnit === 'imperial'
|
||||
|
||||
const legMinutes = settings.roadtrip_leg_minutes
|
||||
const dayMinutes = settings.roadtrip_day_minutes
|
||||
@@ -132,6 +332,42 @@ export default function RoadtripLimitsCard({ onSave }: {
|
||||
// Parsed rather than trusted: a per-user setting gets no server-side validation, and
|
||||
// an unknown word here would become a costing option the router does not have.
|
||||
const vehicle = settings.roadtrip_vehicle ?? ''
|
||||
const vehicleKind: VehicleKind | null =
|
||||
vehicle === 'combustion' || vehicle === 'electric' ? vehicle : null
|
||||
const saved: VehicleSpec = {
|
||||
tankLitres: settings.roadtrip_tank_litres,
|
||||
litresPer100: settings.roadtrip_litres_per_100,
|
||||
batteryKwh: settings.roadtrip_battery_kwh,
|
||||
kwhPer100: settings.roadtrip_kwh_per_100,
|
||||
degradationPercent: settings.roadtrip_battery_degradation,
|
||||
}
|
||||
/**
|
||||
* The one field being typed in, ahead of the write that will save it.
|
||||
*
|
||||
* One slot rather than a copy of the form, because only one field can be typed in at a
|
||||
* time, and a second copy of the settings is exactly the hand-mirrored state this
|
||||
* codebase refuses everywhere else. It exists so the gauge answers while the traveller
|
||||
* is still typing: the figures below it are only convincingly "the range" if changing
|
||||
* one moves the bar, and a settings PUT per keystroke is not on offer.
|
||||
*
|
||||
* The value arrives in whatever unit the field is labelled with, so it goes back
|
||||
* through the same conversion the save would have used.
|
||||
*/
|
||||
const [preview, setPreview] = useState<{ key: SpecKey | 'range' | 'fill'; value: number } | null>(null)
|
||||
const spec: VehicleSpec = preview && preview.key !== 'range' && preview.key !== 'fill'
|
||||
? { ...saved, [preview.key]: storeSpec(preview.key, preview.value, imperial) }
|
||||
: saved
|
||||
const typedKm = preview?.key === 'range'
|
||||
? (imperial ? preview.value / 0.621371 : preview.value)
|
||||
: rangeKm
|
||||
const shownFill = preview?.key === 'fill' ? preview.value : settings.roadtrip_fill_percent ?? null
|
||||
|
||||
// What the parts say, and what the trip will actually plan with. The second is the
|
||||
// first when there is one, which is the rule effectiveRangeKm states once for
|
||||
// everybody who needs it.
|
||||
const computedKm = rangeFromSpec(vehicleKind, spec)
|
||||
const planningKm = effectiveRangeKm(vehicleKind, spec, typedKm)
|
||||
|
||||
const avoiding = parseAvoid(settings.roadtrip_avoid)
|
||||
// No second engine, no avoidance. An instance pointed at its own OSRM has one, and its
|
||||
// car profile is built without excludable classes on every public host.
|
||||
@@ -149,22 +385,30 @@ export default function RoadtripLimitsCard({ onSave }: {
|
||||
|
||||
// Kilometres in storage, the traveller's own unit on screen. Without the round trip an
|
||||
// imperial user types 400 meaning miles, 400 km gets stored, and the warnings arrive a
|
||||
// third too early for ever after.
|
||||
const rangeShown = rangeKm ? Math.round(convertDistance(rangeKm, distanceUnit)) : undefined
|
||||
// third too early for ever after. The same rule runs through the vehicle's own figures,
|
||||
// where showSpec and storeSpec do it per field.
|
||||
const rangeShown = planningKm ? Math.round(convertDistance(planningKm, distanceUnit)) : undefined
|
||||
const setRange = (shown: number) => {
|
||||
const km = distanceUnit === 'imperial' ? shown / 0.621371 : shown
|
||||
const km = imperial ? shown / 0.621371 : shown
|
||||
onSave?.('roadtrip_range_km', Math.round(km))
|
||||
}
|
||||
|
||||
// What is set, on the button itself, so the form does not have to be open to read it.
|
||||
// Each one keeps its own icon, because three numbers in a row say nothing about which
|
||||
// is which: a clock, a calendar and a pump do. Distances go through formatDistance, so
|
||||
// an imperial traveller reads miles here and types miles in the dialog, while what is
|
||||
// Each one keeps its own icon, because numbers in a row say nothing about which is
|
||||
// which: a clock, a calendar and a pump do. Distances go through formatDistance, so an
|
||||
// imperial traveller reads miles here and types miles in the dialog, while what is
|
||||
// stored stays kilometres either way.
|
||||
const badges = [
|
||||
legMinutes ? { key: 'leg', Icon: Clock, text: formatDurationShort(legMinutes * 60) } : null,
|
||||
dayMinutes ? { key: 'day', Icon: CalendarClock, text: formatDurationShort(dayMinutes * 60) } : null,
|
||||
rangeKm ? { key: 'range', Icon: Fuel, text: formatDistance(rangeKm, distanceUnit) } : null,
|
||||
// The planning range, not the typed one: when the vehicle's figures win, the badge
|
||||
// has to say what the warnings will actually use.
|
||||
planningKm ? { key: 'range', Icon: vehicleKind === 'electric' ? Zap : Fuel, text: formatDistance(planningKm, distanceUnit) } : null,
|
||||
// Only worth a badge when it is NOT a full tank, which is the case that changes the
|
||||
// arithmetic. "100 %" on the trigger would be a badge for the default.
|
||||
settings.roadtrip_fill_percent && settings.roadtrip_fill_percent < 100
|
||||
? { key: 'fill', Icon: BatteryCharging, text: `${settings.roadtrip_fill_percent} %` }
|
||||
: null,
|
||||
// The avoidance rides here too, and as ONE badge rather than three: it is a single
|
||||
// decision, the three icons already say which parts of it are on, and a fourth,
|
||||
// fifth and sixth badge would wrap the row onto a second line in a narrow rail.
|
||||
@@ -184,7 +428,7 @@ export default function RoadtripLimitsCard({ onSave }: {
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex w-full items-center gap-3 rounded-xl border border-edge-faint bg-surface-card px-3.5 py-3 text-start transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||
>
|
||||
{/* No icon tile on the trigger: the three badges below already carry a clock, a
|
||||
{/* No icon tile on the trigger: the badges below already carry a clock, a
|
||||
calendar and a pump, and a fourth mark in front of the title only ate the width
|
||||
they need to sit on one line. */}
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
@@ -220,72 +464,45 @@ export default function RoadtripLimitsCard({ onSave }: {
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<Modal isOpen onClose={() => setOpen(false)} size="sm" title={
|
||||
<Modal isOpen onClose={() => { setOpen(false); setPreview(null) }} size="2xl" title={
|
||||
<span className="flex items-center gap-2">
|
||||
<SlidersHorizontal size={15} className="text-content-faint" aria-hidden />
|
||||
{t('roadtrip.limit.title')}
|
||||
</span>
|
||||
}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<LimitRow
|
||||
icon={Clock}
|
||||
label={t('roadtrip.limit.legLabel')}
|
||||
suffix={t('roadtrip.limit.minutes')}
|
||||
value={legMinutes}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
onChange={v => onSave?.('roadtrip_leg_minutes', v)}
|
||||
/>
|
||||
<LimitRow
|
||||
icon={CalendarClock}
|
||||
label={t('roadtrip.limit.dayLabel')}
|
||||
suffix={t('roadtrip.limit.minutes')}
|
||||
value={dayMinutes}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
onChange={v => onSave?.('roadtrip_day_minutes', v)}
|
||||
/>
|
||||
{/* Above the range, because it decides what the range even means: with no
|
||||
vehicle named, a petrol station and a charger both fill the tank, which
|
||||
is arithmetic that is wrong for everybody who drives just one of them. */}
|
||||
<label className="flex items-center gap-3">
|
||||
<Car size={16} className="shrink-0 text-content-faint" aria-hidden />
|
||||
<span className="min-w-0 flex-1 text-body text-content-secondary">{t('roadtrip.limit.vehicleLabel')}</span>
|
||||
<span className="w-[132px] shrink-0">
|
||||
<CustomSelect
|
||||
value={vehicle}
|
||||
onChange={next => onSave?.('roadtrip_vehicle', String(next))}
|
||||
size="sm"
|
||||
options={VEHICLES.map(({ key, labelKey, Icon }) => ({
|
||||
value: key ?? '',
|
||||
label: t(labelKey),
|
||||
icon: <Icon size={14} aria-hidden />,
|
||||
}))}
|
||||
{/* Two columns from the small breakpoint up. One narrow column stopped reading
|
||||
as a form and started reading as a wall, and the halves are different in
|
||||
kind anyway: the left is the trip, the right is the car. */}
|
||||
<div className="grid gap-x-8 gap-y-7 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-7">
|
||||
<section className="flex flex-col gap-4">
|
||||
<SectionTitle icon={Clock}>{t('roadtrip.limit.sectionDriving')}</SectionTitle>
|
||||
<LimitRow
|
||||
icon={Clock}
|
||||
label={t('roadtrip.limit.legLabel')}
|
||||
suffix={t('roadtrip.limit.minutes')}
|
||||
value={legMinutes}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
onChange={v => onSave?.('roadtrip_leg_minutes', v)}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<LimitRow
|
||||
icon={Fuel}
|
||||
label={t('roadtrip.limit.rangeLabel')}
|
||||
suffix={distanceUnit === 'imperial' ? 'mi' : 'km'}
|
||||
value={rangeShown}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
onChange={setRange}
|
||||
/>
|
||||
{/* Said out loud rather than left to be discovered: the day figure this is
|
||||
measured against is driving time only. It does not include how long anyone
|
||||
stands still, and it does not include the leg to the hotel, because the
|
||||
rail does not model one. A limit called "travel time per day" over that
|
||||
number would be a lie that looks tidy. */}
|
||||
<p className="text-caption text-content-faint">{t('roadtrip.limit.hint')}</p>
|
||||
<LimitRow
|
||||
icon={CalendarClock}
|
||||
label={t('roadtrip.limit.dayLabel')}
|
||||
suffix={t('roadtrip.limit.minutes')}
|
||||
value={dayMinutes}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
onChange={v => onSave?.('roadtrip_day_minutes', v)}
|
||||
/>
|
||||
{/* Said out loud rather than left to be discovered: the day figure this is
|
||||
measured against is driving time only. It does not include how long
|
||||
anyone stands still, and it does not include the leg to the hotel,
|
||||
because the rail does not model one. A limit called "travel time per
|
||||
day" over that number would be a lie that looks tidy. */}
|
||||
<p className="text-caption leading-snug text-content-faint">{t('roadtrip.limit.hint')}</p>
|
||||
</section>
|
||||
|
||||
{/* A heading and a rule, because six rows in one list stop reading as a form
|
||||
and start reading as a wall. The three above are numbers that warn; the
|
||||
three below change the road itself, which is a different kind of setting
|
||||
and worth the separation. */}
|
||||
<div className="mt-1 border-t border-edge-faint pt-4">
|
||||
<p className="mb-3 text-caption font-semibold uppercase tracking-wide text-content-faint">
|
||||
{t('roadtrip.avoid.section')}
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<section className="flex flex-col gap-3">
|
||||
<SectionTitle icon={Signpost}>{t('roadtrip.avoid.section')}</SectionTitle>
|
||||
{AVOID_ROWS.map(({ cls, icon, label }) => (
|
||||
<AvoidRow
|
||||
key={cls}
|
||||
@@ -296,15 +513,98 @@ export default function RoadtripLimitsCard({ onSave }: {
|
||||
onToggle={() => toggleAvoid(cls)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* "Where possible" is doing real work in the label, so it is spelled out
|
||||
here: the router weights a class down rather than banning it, and on a
|
||||
drive with no way round one the road still uses it. The day says so
|
||||
itself when that happens, which is the only honest way to offer this. */}
|
||||
<p className="mt-3 text-caption text-content-faint">
|
||||
{canAvoid ? t('roadtrip.avoid.hint') : t('roadtrip.avoid.unavailable')}
|
||||
</p>
|
||||
{/* "Where possible" is doing real work in the label, so it is spelled out
|
||||
here: the router weights a class down rather than banning it, and on a
|
||||
drive with no way round one the road still uses it. The day says so
|
||||
itself when that happens, which is the only honest way to offer this. */}
|
||||
<p className="text-caption leading-snug text-content-faint">
|
||||
{canAvoid ? t('roadtrip.avoid.hint') : t('roadtrip.avoid.unavailable')}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<SectionTitle icon={Car}>{t('roadtrip.limit.sectionVehicle')}</SectionTitle>
|
||||
{/* First, because it decides what everything under it means: with no vehicle
|
||||
named, a petrol station and a charger both fill the tank, which is
|
||||
arithmetic that is wrong for everybody who drives just one of them. */}
|
||||
<label className="flex items-center gap-3">
|
||||
<Car size={16} className="shrink-0 text-content-faint" aria-hidden />
|
||||
<span className="min-w-0 flex-1 text-body text-content-secondary">{t('roadtrip.limit.vehicleLabel')}</span>
|
||||
<span className="w-[132px] shrink-0">
|
||||
<CustomSelect
|
||||
value={vehicle}
|
||||
onChange={next => onSave?.('roadtrip_vehicle', String(next))}
|
||||
size="sm"
|
||||
options={VEHICLES.map(({ key, labelKey, Icon }) => ({
|
||||
value: key ?? '',
|
||||
label: t(labelKey),
|
||||
icon: <Icon size={14} aria-hidden />,
|
||||
}))}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<RangeGauge
|
||||
rangeKm={planningKm}
|
||||
fillPercent={shownFill}
|
||||
wearPercent={vehicleKind === 'electric' ? spec.degradationPercent ?? null : null}
|
||||
unit={distanceUnit}
|
||||
electric={vehicleKind === 'electric'}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* The parts, when there is a kind of car to have parts of. Nothing here is
|
||||
required: a traveller who knows their range types it below and never
|
||||
looks at these. */}
|
||||
{vehicleKind ? SPEC_ROWS[vehicleKind].map(({ key, setting, Icon, labelKey, step }) => (
|
||||
<LimitRow
|
||||
key={key}
|
||||
icon={Icon}
|
||||
label={t(labelKey)}
|
||||
suffix={specUnit(key, imperial)}
|
||||
step={step}
|
||||
wide
|
||||
value={showSpec(key, saved[key], imperial)}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
onDraft={v => setPreview({ key, value: v })}
|
||||
onChange={v => onSave?.(setting, storeSpec(key, v, imperial))}
|
||||
/>
|
||||
)) : null}
|
||||
|
||||
{/* Read-only once the parts add up, because then it IS the parts. Leaving it
|
||||
editable would offer a second answer to a question that already has
|
||||
one, and the traveller would have no way of telling which the warnings
|
||||
were using. Clearing a field above hands the row straight back. */}
|
||||
<LimitRow
|
||||
icon={Route}
|
||||
// "on one tank" is the wrong noun for half the travellers who set this.
|
||||
label={t(vehicleKind === 'electric' ? 'roadtrip.limit.rangeLabelCharge' : 'roadtrip.limit.rangeLabel')}
|
||||
suffix={imperial ? 'mi' : 'km'}
|
||||
value={rangeShown}
|
||||
placeholder={t('roadtrip.limit.off')}
|
||||
derived={computedKm ? t('roadtrip.limit.computed') : undefined}
|
||||
wide
|
||||
onDraft={v => setPreview({ key: 'range', value: v })}
|
||||
onChange={setRange}
|
||||
/>
|
||||
{/* Under the range, because it is a fraction OF it. Nobody charges to 100 %
|
||||
on the road: the last fifth takes as long as the first four, so a stop
|
||||
counted as a full tank overstates everything after it by that fifth. */}
|
||||
<LimitRow
|
||||
icon={BatteryCharging}
|
||||
label={t('roadtrip.limit.fillLabel')}
|
||||
suffix="%"
|
||||
value={settings.roadtrip_fill_percent}
|
||||
placeholder={t('roadtrip.limit.fillFull')}
|
||||
wide
|
||||
onDraft={v => setPreview({ key: 'fill', value: v > 100 ? 100 : v })}
|
||||
onChange={v => onSave?.('roadtrip_fill_percent', v > 100 ? 100 : v)}
|
||||
/>
|
||||
<p className="text-caption leading-snug text-content-faint">
|
||||
{vehicleKind ? t('roadtrip.limit.specHint') : t('roadtrip.limit.vehicleHint')}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
CarFront, Footprints, Bike, Zap, AlertTriangle,
|
||||
ParkingSquare, Shuffle, Fuel, Clock, Spline, Ban, Plus, Search, RotateCcw, X,
|
||||
ParkingSquare, Shuffle, Fuel, Clock, Spline, Ban, Plus, Search, RotateCcw, X, BatteryCharging,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
import MDancingTrek from '../../mobile/components/MDancingTrek'
|
||||
@@ -13,7 +13,7 @@ import { Tooltip } from '../shared/Tooltip'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { formatDistance } from '../../utils/units'
|
||||
import { formatDate, formatClockTime } from '../../utils/formatters'
|
||||
import { formatDurationShort, isServiceStopType, serviceColor, type ScheduleEntry, type ScheduleWarning } from './roadtripModel'
|
||||
import { formatDurationShort, isServiceStopType, serviceColor, type ScheduleEntry, type ScheduleWarning, refuelsRange, type VehicleKind} from './roadtripModel'
|
||||
import { STOP_KIND_BY_KEY } from './stopKinds'
|
||||
import { spurWorthLabelling } from './accessSpur'
|
||||
import StopKindPicker from './StopKindPicker'
|
||||
@@ -165,6 +165,35 @@ function OffRoadBadge({ meters }: { meters: number }): React.ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What a fill-up here actually puts in, beside the stay it takes.
|
||||
*
|
||||
* Only on a stop that refuels, and only when it is not a full tank: "100 %" would be a
|
||||
* badge for the default. It reads as the second half of the stay badge because that is
|
||||
* what it is — how long you stand here, and what you get for it.
|
||||
*/
|
||||
function FillBadge({ percent }: { percent: number }): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip label={t('roadtrip.limit.fillBadge', { percent })}>
|
||||
<span className="inline-flex h-[16px] items-stretch self-start overflow-hidden rounded border border-edge">
|
||||
<span
|
||||
className="flex items-center bg-surface-tertiary px-1 text-content-faint"
|
||||
style={{ fontSize: FS.micro }}
|
||||
>
|
||||
<BatteryCharging size={9} aria-hidden />
|
||||
</span>
|
||||
<span
|
||||
className="flex items-center border-s border-edge bg-surface-card px-1.5 font-semibold tabular-nums text-content-secondary"
|
||||
style={{ fontSize: FS.micro }}
|
||||
>
|
||||
{`${percent} %`}
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function StayBadge({ minutes, onEdit }: { minutes: number | null; onEdit?: () => void }): React.ReactElement | null {
|
||||
const { t } = useTranslation()
|
||||
const text = minutes ? formatDurationShort(minutes * 60) : null
|
||||
@@ -538,6 +567,12 @@ function ServiceStop({ stop, entry, late, driveFindings, selected, onSelect, onE
|
||||
onEditStay?: () => void
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
// Read here rather than threaded down: both stop shapes need the same two, and the
|
||||
// badge is the only thing in the rail that depends on them.
|
||||
const fillPercent = useSettingsStore(st => st.settings.roadtrip_fill_percent)
|
||||
const vehicleSetting = useSettingsStore(st => st.settings.roadtrip_vehicle)
|
||||
const vehicle: VehicleKind | null =
|
||||
vehicleSetting === 'combustion' || vehicleSetting === 'electric' ? vehicleSetting : null
|
||||
const kind = STOP_KIND_BY_KEY[stop.stopType ?? '']
|
||||
const Icon = kind?.Icon ?? ParkingSquare
|
||||
const label = t(kind?.labelKey ?? 'roadtrip.poi.rest')
|
||||
@@ -604,6 +639,7 @@ function ServiceStop({ stop, entry, late, driveFindings, selected, onSelect, onE
|
||||
</span>
|
||||
<span className="flex flex-wrap items-center gap-1">
|
||||
<StayBadge minutes={stop.dwellMinutes} onEdit={onEditStay} />
|
||||
{fillPercent && refuelsRange(stop.stopType, vehicle) ? <FillBadge percent={fillPercent} /> : null}
|
||||
{spurWorthLabelling(stop.offRoadMeters) ? <OffRoadBadge meters={stop.offRoadMeters ?? 0} /> : null}
|
||||
{(driveFindings ?? []).map(w => <DriveFindingBadge key={w.code} warning={w} />)}
|
||||
<LateBadge late={late} />
|
||||
@@ -821,6 +857,12 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
|
||||
onEditStay?: () => void
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
// Read here rather than threaded down: both stop shapes need the same two, and the
|
||||
// badge is the only thing in the rail that depends on them.
|
||||
const fillPercent = useSettingsStore(st => st.settings.roadtrip_fill_percent)
|
||||
const vehicleSetting = useSettingsStore(st => st.settings.roadtrip_vehicle)
|
||||
const vehicle: VehicleKind | null =
|
||||
vehicleSetting === 'combustion' || vehicleSetting === 'electric' ? vehicleSetting : null
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -901,6 +943,7 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
|
||||
one; the number is for luggage, a gate, a track a hire car should not be on. */}
|
||||
<span className="flex flex-wrap items-center gap-1">
|
||||
<StayBadge minutes={stop.dwellMinutes} onEdit={onEditStay} />
|
||||
{fillPercent && refuelsRange(stop.stopType, vehicle) ? <FillBadge percent={fillPercent} /> : null}
|
||||
{spurWorthLabelling(stop.offRoadMeters) ? <OffRoadBadge meters={stop.offRoadMeters ?? 0} /> : null}
|
||||
{(driveFindings ?? []).map(w => <DriveFindingBadge key={w.code} warning={w} />)}
|
||||
<LateBadge late={late} />
|
||||
|
||||
@@ -726,3 +726,46 @@ describe('refuelsRange — what fills which tank', () => {
|
||||
expect(refuelStopTypeFor(null)).toEqual(['fuel', 'charging'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deriveDriveWarnings — filling only part way', () => {
|
||||
const drive = (minutes: number, km: number) =>
|
||||
({ duration: minutes * 60, distance: km * 1000, mode: 'driving' })
|
||||
|
||||
it('FE-ROADTRIP-MODEL-101: a stop that fills to 80 % leaves a fifth already used', () => {
|
||||
// Nobody charges to 100 % on the road: the last fifth takes as long as the first
|
||||
// four. Counting a stop as a full tank overstates what comes after it by that fifth.
|
||||
const out = deriveDriveWarnings(
|
||||
[drive(60, 100), drive(60, 450)],
|
||||
[false, true, false],
|
||||
{ legMinutes: null, dayMinutes: null, rangeKm: 500, fillPercent: 80 },
|
||||
0,
|
||||
)
|
||||
// After the stop the budget restarts at 100 km rather than 0, so the tank is dry
|
||||
// 400 km into the second leg. Filled all the way it would have gone the whole 450.
|
||||
expect(out.emptyAt).toHaveLength(1)
|
||||
expect(out.emptyAt[0]).toMatchObject({ legIndex: 1, intoLegKm: 400 })
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-102: filling all the way is what absent, zero and 100 all mean', () => {
|
||||
const legs = [drive(60, 100), drive(60, 450)]
|
||||
const refuels = [false, true, false]
|
||||
const full = { legMinutes: null, dayMinutes: null, rangeKm: 500 }
|
||||
for (const fillPercent of [undefined, null, 0, 100]) {
|
||||
const out = deriveDriveWarnings(legs, refuels, { ...full, fillPercent }, 0)
|
||||
// A full tank after the stop covers the remaining 450 km without a finding.
|
||||
expect(out.emptyAt).toEqual([])
|
||||
expect(out.warnings).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-103: with no range set, the fill level changes nothing', () => {
|
||||
const out = deriveDriveWarnings(
|
||||
[drive(60, 900)],
|
||||
[true, false],
|
||||
{ legMinutes: null, dayMinutes: null, rangeKm: null, fillPercent: 50 },
|
||||
0,
|
||||
)
|
||||
expect(out.warnings).toEqual([])
|
||||
expect(out.emptyAt).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -156,6 +156,15 @@ export function refuelsRange(stopType: string | null | undefined, vehicle?: Vehi
|
||||
* rather than a sentinel: zero is a legal thing to type and means the same as unset.
|
||||
*/
|
||||
export interface DriveLimits {
|
||||
/**
|
||||
* How full a fill-up actually goes, 1 to 100, or null for "all the way".
|
||||
*
|
||||
* Nobody charges an electric car to 100 % on the road: the last fifth takes as long as
|
||||
* the first four and the manual says not to. Treating every stop as a full tank
|
||||
* overstates the range after it by exactly that fifth, which is a whole leg on a long
|
||||
* day.
|
||||
*/
|
||||
fillPercent?: number | null
|
||||
/** Longest single drive between two stops, in minutes. */
|
||||
legMinutes: number | null
|
||||
/** Longest total driving in one day, in minutes. */
|
||||
@@ -434,8 +443,15 @@ export function deriveDriveWarnings(
|
||||
// walk the driving legs only, which is why this is counted here rather than derived.
|
||||
let drivenMeters = 0
|
||||
|
||||
// What is left on the clock straight after a stop. A tank filled to 80 % has already
|
||||
// used a fifth of its range before the car moves, which is the honest way to say it in
|
||||
// a budget that counts upwards.
|
||||
const usedAfterFilling = limits.rangeKm && limits.fillPercent
|
||||
? limits.rangeKm * (1 - limits.fillPercent / 100)
|
||||
: 0
|
||||
|
||||
for (let i = 0; i < legs.length; i++) {
|
||||
if (refuelsAt[i]) budget = 0
|
||||
if (refuelsAt[i]) budget = usedAfterFilling
|
||||
const leg = legs[i]
|
||||
// The stop this leg arrives at. Both findings are about what is true on arrival.
|
||||
const at = i + 1
|
||||
@@ -486,7 +502,7 @@ export function deriveDriveWarnings(
|
||||
}
|
||||
// The last stop of the day counts too: filling up on arrival is what makes the next
|
||||
// morning start with a full tank.
|
||||
if (refuelsAt[legs.length]) budget = 0
|
||||
if (refuelsAt[legs.length]) budget = usedAfterFilling
|
||||
|
||||
const minutes = Math.round(totalSeconds / 60)
|
||||
const day = limits.dayMinutes && minutes > limits.dayMinutes
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type DayWarning, type DriveLimits, type Schedule, type ScheduleWarning, parseAvoid, type DryPoint, type VehicleKind } from './roadtripModel'
|
||||
import { projectOntoRoute, pointAtMeters} from './corridor'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { effectiveRangeKm } from './vehicleRange'
|
||||
import type { Assignment, AssignmentsMap, Day, RouteAvoidClass, RouteSegment, RouteVia, SnappedWaypoint } from '../../types'
|
||||
import { spurFor } from './accessSpur'
|
||||
import type { RoadtripVia } from '@trek/shared'
|
||||
@@ -275,6 +276,7 @@ export function useRoadtripRoutes(
|
||||
const legMinutes = useSettingsStore(s => s.settings.roadtrip_leg_minutes)
|
||||
const dayMinutes = useSettingsStore(s => s.settings.roadtrip_day_minutes)
|
||||
const rangeKm = useSettingsStore(s => s.settings.roadtrip_range_km)
|
||||
const fillPercent = useSettingsStore(s => s.settings.roadtrip_fill_percent)
|
||||
/**
|
||||
* Road classes to weight away, as the settings row stores them: a comma list.
|
||||
*
|
||||
@@ -290,12 +292,43 @@ export function useRoadtripRoutes(
|
||||
// server-side check, and an unknown word here would silently stop both kinds counting.
|
||||
const vehicleKind: VehicleKind | null =
|
||||
vehicle === 'combustion' || vehicle === 'electric' ? vehicle : null
|
||||
/**
|
||||
* What the vehicle is made of, when the traveller filled that in instead of a range.
|
||||
*
|
||||
* Read as five primitives rather than as one object so the memo below compares numbers.
|
||||
* An object rebuilt each render would give `limits` a new identity every time, and
|
||||
* `limits` is what the route effect watches — every keystroke anywhere in the app would
|
||||
* recalculate every day's driving.
|
||||
*/
|
||||
const tankLitres = useSettingsStore(s => s.settings.roadtrip_tank_litres)
|
||||
const litresPer100 = useSettingsStore(s => s.settings.roadtrip_litres_per_100)
|
||||
const batteryKwh = useSettingsStore(s => s.settings.roadtrip_battery_kwh)
|
||||
const kwhPer100 = useSettingsStore(s => s.settings.roadtrip_kwh_per_100)
|
||||
const degradationPercent = useSettingsStore(s => s.settings.roadtrip_battery_degradation)
|
||||
// The parts win over the typed number when they add up, because they are the more
|
||||
// specific answer; the dialog shows the result, so it is never a surprise. Zero and
|
||||
// absent both mean "no limit", which is why the falsy fold happens here once.
|
||||
const planningRangeKm = useMemo(
|
||||
() => effectiveRangeKm(
|
||||
vehicleKind,
|
||||
{ tankLitres, litresPer100, batteryKwh, kwhPer100, degradationPercent },
|
||||
rangeKm,
|
||||
) || null,
|
||||
[vehicleKind, tankLitres, litresPer100, batteryKwh, kwhPer100, degradationPercent, rangeKm],
|
||||
)
|
||||
const avoidKey = avoid.join(',')
|
||||
// Zero and absent both mean "no limit": zero is a legal thing to type and says the
|
||||
// same thing, so it is folded here rather than guarded at every reading.
|
||||
const limits = useMemo<DriveLimits>(
|
||||
() => ({ legMinutes: legMinutes || null, dayMinutes: dayMinutes || null, rangeKm: rangeKm || null }),
|
||||
[legMinutes, dayMinutes, rangeKm],
|
||||
() => ({
|
||||
legMinutes: legMinutes || null,
|
||||
dayMinutes: dayMinutes || null,
|
||||
rangeKm: planningRangeKm,
|
||||
// Only a real fraction counts. Zero, absent and 100 all mean "fills right up",
|
||||
// which is what the budget did before the setting existed.
|
||||
fillPercent: fillPercent && fillPercent > 0 && fillPercent < 100 ? fillPercent : null,
|
||||
}),
|
||||
[legMinutes, dayMinutes, planningRangeKm, fillPercent],
|
||||
)
|
||||
|
||||
const plan = useMemo<RoadtripDay[]>(() => {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { rangeFromSpec, effectiveRangeKm, showSpec, storeSpec, specUnit } from './vehicleRange'
|
||||
|
||||
/**
|
||||
* FE-ROADTRIP-VEHRANGE-001..013 — a range stated as the parts it is made of.
|
||||
*
|
||||
* Two things are worth guarding here. The arithmetic itself, because it decides where
|
||||
* every fuel warning lands, and the unit round trip, because an imperial traveller who
|
||||
* types 15 into a field labelled gallons must not end up with 15 litres in storage.
|
||||
*/
|
||||
|
||||
describe('rangeFromSpec', () => {
|
||||
it('FE-ROADTRIP-VEHRANGE-001: a tank divided by a consumption is a range', () => {
|
||||
expect(rangeFromSpec('combustion', { tankLitres: 55, litresPer100: 7 })).toBe(786)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-002: a battery divided by a consumption is a range', () => {
|
||||
expect(rangeFromSpec('electric', { batteryKwh: 58, kwhPer100: 16 })).toBe(363)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-003: age comes off the battery before the division', () => {
|
||||
// 58 kWh minus a tenth is 52.2, and 52.2 at 16 per hundred is 326.25 km.
|
||||
expect(rangeFromSpec('electric', { batteryKwh: 58, kwhPer100: 16, degradationPercent: 10 })).toBe(326)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-004: half a pair is not a range', () => {
|
||||
// Guessing the missing half would put the warnings at the wrong place while
|
||||
// looking exact, which is worse than saying nothing.
|
||||
expect(rangeFromSpec('combustion', { tankLitres: 55 })).toBeNull()
|
||||
expect(rangeFromSpec('combustion', { litresPer100: 7 })).toBeNull()
|
||||
expect(rangeFromSpec('electric', { batteryKwh: 58 })).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-005: figures belong to the kind of car they came from', () => {
|
||||
// A tank size on an electric car is a leftover from before the picker changed,
|
||||
// not a battery.
|
||||
expect(rangeFromSpec('electric', { tankLitres: 55, litresPer100: 7 })).toBeNull()
|
||||
expect(rangeFromSpec('combustion', { batteryKwh: 58, kwhPer100: 16 })).toBeNull()
|
||||
expect(rangeFromSpec(null, { tankLitres: 55, litresPer100: 7 })).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-006: a battery reported as entirely gone is a typo, not a car', () => {
|
||||
// Capped at 90 %, because zero usable capacity would put the dry point at the
|
||||
// first metre of the trip and divide by nothing.
|
||||
expect(rangeFromSpec('electric', { batteryKwh: 58, kwhPer100: 16, degradationPercent: 100 })).toBe(36)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-007: nonsense in a field is the same as an empty field', () => {
|
||||
expect(rangeFromSpec('combustion', { tankLitres: 0, litresPer100: 7 })).toBeNull()
|
||||
expect(rangeFromSpec('combustion', { tankLitres: -55, litresPer100: 7 })).toBeNull()
|
||||
expect(rangeFromSpec('combustion', { tankLitres: Number.NaN, litresPer100: 7 })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('effectiveRangeKm', () => {
|
||||
it('FE-ROADTRIP-VEHRANGE-008: the parts win over a typed round number', () => {
|
||||
// Somebody who gave a tank size and a consumption said something more exact
|
||||
// than "about 500", and the dialog shows them the result.
|
||||
expect(effectiveRangeKm('combustion', { tankLitres: 55, litresPer100: 7 }, 500)).toBe(786)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-009: an incomplete pair hands the typed number straight back', () => {
|
||||
expect(effectiveRangeKm('combustion', { tankLitres: 55 }, 500)).toBe(500)
|
||||
expect(effectiveRangeKm(null, {}, 500)).toBe(500)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-010: no figures anywhere is no limit', () => {
|
||||
expect(effectiveRangeKm(null, {}, 0)).toBeNull()
|
||||
expect(effectiveRangeKm('electric', {}, undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('unit round trip', () => {
|
||||
it('FE-ROADTRIP-VEHRANGE-011: metric figures are stored exactly as typed', () => {
|
||||
expect(storeSpec('tankLitres', 55, false)).toBe(55)
|
||||
expect(showSpec('tankLitres', 55, false)).toBe(55)
|
||||
expect(showSpec('litresPer100', 7.4, false)).toBe(7.4)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-012: gallons go in and come back out as the same gallons', () => {
|
||||
// 15 US gallons is 56.78 litres, and storage is litres — the same rule the
|
||||
// range itself follows, where storage is always kilometres.
|
||||
const stored = storeSpec('tankLitres', 15, true)
|
||||
expect(stored).toBe(56.78)
|
||||
expect(showSpec('tankLitres', stored, true)).toBe(15)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-013: miles per gallon is the inverse, and survives the trip', () => {
|
||||
// The one field that is not a scale factor: 30 mpg is 7.84 L/100 km, and a
|
||||
// straight multiplication would have stored a car four times as thirsty.
|
||||
const stored = storeSpec('litresPer100', 30, true)
|
||||
expect(stored).toBe(7.84)
|
||||
expect(showSpec('litresPer100', stored, true)).toBe(30)
|
||||
expect(specUnit('litresPer100', true)).toBe('mpg')
|
||||
expect(specUnit('litresPer100', false)).toBe('L/100 km')
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-014: electric consumption is quoted per 100 miles', () => {
|
||||
// What the EPA prints on the window sticker, so the figure is one a US
|
||||
// traveller can copy rather than convert.
|
||||
expect(showSpec('kwhPer100', 16, true)).toBe(25.7)
|
||||
expect(specUnit('kwhPer100', true)).toBe('kWh/100 mi')
|
||||
// A capacity is a capacity everywhere.
|
||||
expect(specUnit('batteryKwh', true)).toBe('kWh')
|
||||
expect(showSpec('batteryKwh', 58, true)).toBe(58)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-VEHRANGE-015: an empty field stays empty rather than becoming a zero', () => {
|
||||
expect(showSpec('tankLitres', undefined, false)).toBeUndefined()
|
||||
expect(showSpec('tankLitres', 0, true)).toBeUndefined()
|
||||
expect(storeSpec('tankLitres', 0, true)).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { VehicleKind } from './roadtripModel'
|
||||
|
||||
/**
|
||||
* How far the vehicle goes on one fill, worked out from what it is made of.
|
||||
*
|
||||
* Two ways to the same number, and the point of having both is that people know their
|
||||
* car in different ways. Some know "about 600 km"; some know the tank holds 55 litres
|
||||
* and it does 7 per hundred, and would have to do the division themselves. Whoever types
|
||||
* a range directly still gets exactly what they typed — this only fills that number in
|
||||
* when it can and the field is empty.
|
||||
*
|
||||
* Deliberately not a consumption model. ABRP asks for reference speed, relative speed and
|
||||
* a drive profile because it predicts a specific drive; this answers "how far on one
|
||||
* fill", which is a division. Adding speed terms would make the number look precise
|
||||
* without making it right, since the biggest factor by far is how the person drives.
|
||||
*
|
||||
* Pure: no React, no store, no network.
|
||||
*/
|
||||
|
||||
export interface VehicleSpec {
|
||||
/** Litres the tank holds. Petrol only. */
|
||||
tankLitres?: number | null
|
||||
/** Litres per 100 km. Petrol only. */
|
||||
litresPer100?: number | null
|
||||
/** Usable battery when new, in kWh. Electric only. */
|
||||
batteryKwh?: number | null
|
||||
/** kWh per 100 km. Electric only. */
|
||||
kwhPer100?: number | null
|
||||
/**
|
||||
* How much of the battery the years have taken, as a percentage.
|
||||
*
|
||||
* A five-year-old car does not carry the capacity on its data sheet, and the gap is
|
||||
* the difference between arriving and not on the last leg of a day.
|
||||
*/
|
||||
degradationPercent?: number | null
|
||||
}
|
||||
|
||||
/** A positive, finite number, or null. Everything here comes from a text field. */
|
||||
function positive(value: number | null | undefined): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null
|
||||
}
|
||||
|
||||
/**
|
||||
* The range in kilometres that the parts add up to, or null when they do not add up.
|
||||
*
|
||||
* Null rather than a guess whenever a piece is missing: half a specification is not a
|
||||
* range, and a made-up figure here becomes a fuel warning that fires at the wrong place.
|
||||
*/
|
||||
export function rangeFromSpec(vehicle: VehicleKind | null | undefined, spec: VehicleSpec): number | null {
|
||||
if (vehicle === 'combustion') {
|
||||
const tank = positive(spec.tankLitres)
|
||||
const per100 = positive(spec.litresPer100)
|
||||
if (!tank || !per100) return null
|
||||
return Math.round((tank / per100) * 100)
|
||||
}
|
||||
|
||||
if (vehicle === 'electric') {
|
||||
const battery = positive(spec.batteryKwh)
|
||||
const per100 = positive(spec.kwhPer100)
|
||||
if (!battery || !per100) return null
|
||||
// Degradation only ever takes away, and never all of it: a battery reported as 100 %
|
||||
// gone is a typo, not a car, and dividing by zero range would put the dry point at
|
||||
// the first metre of the trip.
|
||||
const lost = Math.min(90, Math.max(0, positive(spec.degradationPercent) ?? 0))
|
||||
const usable = battery * (1 - lost / 100)
|
||||
return Math.round((usable / per100) * 100)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The range to plan with: what the parts say, or what was typed, in that order.
|
||||
*
|
||||
* The computed one wins because it is the more specific answer — somebody who filled in
|
||||
* a tank size and a consumption said something more exact than a round number, and the
|
||||
* dialog shows them the result so it is never a surprise.
|
||||
*/
|
||||
export function effectiveRangeKm(
|
||||
vehicle: VehicleKind | null | undefined,
|
||||
spec: VehicleSpec,
|
||||
typedKm: number | null | undefined,
|
||||
): number | null {
|
||||
return rangeFromSpec(vehicle, spec) ?? positive(typedKm)
|
||||
}
|
||||
|
||||
/**
|
||||
* The unit each field is typed in, per measurement system.
|
||||
*
|
||||
* Storage is always metric, exactly as the range itself is always kilometres: an imperial
|
||||
* traveller who typed 15 into a field labelled "gallons" must not have 15 litres stored,
|
||||
* or every warning after it fires at a quarter of the right distance.
|
||||
*
|
||||
* The imperial pair is not a straight translation of the metric one. Fuel economy is
|
||||
* quoted as miles per gallon, which is the INVERSE of litres per hundred, while electric
|
||||
* economy is quoted by the EPA as kWh per 100 miles, which is not. Forcing one shape onto
|
||||
* both would leave one of the two reading as a figure nobody recognises.
|
||||
*/
|
||||
export type SpecKey = keyof VehicleSpec
|
||||
|
||||
const LITRES_PER_GALLON = 3.785411784
|
||||
/** litres/100 km × mpg = this. The one constant that makes the inversion exact. */
|
||||
const MPG_PRODUCT = 235.214583
|
||||
const KM_PER_MILE = 1.609344
|
||||
|
||||
export function specUnit(key: SpecKey, imperial: boolean): string {
|
||||
switch (key) {
|
||||
case 'tankLitres': return imperial ? 'gal' : 'L'
|
||||
case 'litresPer100': return imperial ? 'mpg' : 'L/100 km'
|
||||
case 'batteryKwh': return 'kWh'
|
||||
case 'kwhPer100': return imperial ? 'kWh/100 mi' : 'kWh/100 km'
|
||||
default: return '%'
|
||||
}
|
||||
}
|
||||
|
||||
/** How many decimals the field is worth typing. A tank is not measured to a tenth. */
|
||||
function places(key: SpecKey, imperial: boolean): number {
|
||||
if (key === 'degradationPercent') return 0
|
||||
if (key === 'tankLitres') return imperial ? 1 : 0
|
||||
if (key === 'litresPer100') return imperial ? 0 : 1
|
||||
return 1
|
||||
}
|
||||
|
||||
function convert(key: SpecKey, value: number, imperial: boolean, toStorage: boolean): number {
|
||||
if (!imperial) return value
|
||||
switch (key) {
|
||||
case 'tankLitres': return toStorage ? value * LITRES_PER_GALLON : value / LITRES_PER_GALLON
|
||||
// Inverse in both directions, which is why one expression covers the pair.
|
||||
case 'litresPer100': return MPG_PRODUCT / value
|
||||
case 'kwhPer100': return toStorage ? value / KM_PER_MILE : value * KM_PER_MILE
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
|
||||
function round(value: number, decimals: number): number {
|
||||
const f = 10 ** decimals
|
||||
return Math.round(value * f) / f
|
||||
}
|
||||
|
||||
/** The stored figure as it should appear in the field, or undefined when there is none. */
|
||||
export function showSpec(key: SpecKey, stored: number | null | undefined, imperial: boolean): number | undefined {
|
||||
const value = positive(stored)
|
||||
if (value === null) return undefined
|
||||
return round(convert(key, value, imperial, false), places(key, imperial))
|
||||
}
|
||||
|
||||
/**
|
||||
* A typed figure as it should be stored. Zero comes back as zero, meaning "not said".
|
||||
*
|
||||
* Two decimals rather than none: a gallon is 3.79 litres, so an imperial figure that
|
||||
* round-tripped through a whole number would drift by up to half a litre and show the
|
||||
* traveller a different number than the one they typed.
|
||||
*/
|
||||
export function storeSpec(key: SpecKey, shown: number, imperial: boolean): number {
|
||||
if (!(shown > 0)) return 0
|
||||
return round(convert(key, shown, imperial, true), 2)
|
||||
}
|
||||
@@ -167,6 +167,39 @@ export interface Settings {
|
||||
* opened the dialog.
|
||||
*/
|
||||
roadtrip_vehicle?: string
|
||||
/**
|
||||
* How full a fill-up goes, 1 to 100. Absent or 100 means all the way.
|
||||
*
|
||||
* Nobody charges to 100 % on the road, so an electric traveller who leaves this at full
|
||||
* gets a range figure a fifth too generous after every stop.
|
||||
*/
|
||||
roadtrip_fill_percent?: number
|
||||
/**
|
||||
* What the vehicle is made of, for travellers who would rather not do the division.
|
||||
*
|
||||
* Optional throughout, and stored METRIC throughout — litres and kilowatt-hours, per
|
||||
* 100 kilometres — exactly as roadtrip_range_km is always kilometres. The dialog does
|
||||
* the round trip into gallons, miles per gallon and kWh per 100 miles for an imperial
|
||||
* traveller; storage stays one system so the arithmetic never has to ask.
|
||||
*
|
||||
* When a pair is complete it WINS over roadtrip_range_km, because it is the more
|
||||
* specific answer. Half a pair computes nothing: a made-up range here would put the
|
||||
* fuel warnings at the wrong place while looking exact.
|
||||
*/
|
||||
roadtrip_tank_litres?: number
|
||||
roadtrip_litres_per_100?: number
|
||||
roadtrip_battery_kwh?: number
|
||||
roadtrip_kwh_per_100?: number
|
||||
/**
|
||||
* Percent of the battery the years have taken. Absent means none.
|
||||
*
|
||||
* The one figure of ABRP's list that changes the answer here. Plug type, reference
|
||||
* speed and drive style shape a consumption PREDICTION; this addon does not predict
|
||||
* one, it divides a capacity by a consumption the traveller states. Degradation is
|
||||
* different in kind: it shrinks the capacity itself, and on a five-year-old car by
|
||||
* enough to decide whether the last leg of a day arrives.
|
||||
*/
|
||||
roadtrip_battery_degradation?: number
|
||||
/** CARTO basemaps watermark keyless tiles; the key is appended as ?key= (#2054). */
|
||||
carto_api_key?: string
|
||||
mapbox_access_token?: string
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'أطول قيادة متواصلة',
|
||||
'roadtrip.limit.dayLabel': 'القيادة في اليوم',
|
||||
'roadtrip.limit.rangeLabel': 'المدى بخزان واحد',
|
||||
'roadtrip.limit.fillLabel': 'التعبئة حتى',
|
||||
'roadtrip.limit.fillFull': 'ممتلئ',
|
||||
'roadtrip.limit.fillBadge': 'يملأ حتى {percent} ٪',
|
||||
'roadtrip.limit.wearNote': '{percent} ٪ مفقودة بسبب التقادم',
|
||||
'roadtrip.limit.rangeLabelCharge': 'المدى لكل شحنة',
|
||||
'roadtrip.limit.sectionDriving': 'القيادة',
|
||||
'roadtrip.limit.sectionVehicle': 'المركبة',
|
||||
'roadtrip.limit.tankLabel': 'سعة الخزان',
|
||||
'roadtrip.limit.batteryLabel': 'البطارية',
|
||||
'roadtrip.limit.useLabel': 'الاستهلاك',
|
||||
'roadtrip.limit.wearLabel': 'تآكل البطارية',
|
||||
'roadtrip.limit.computed': 'محسوب من بيانات المركبة',
|
||||
'roadtrip.limit.perFill': 'لكل خزان',
|
||||
'roadtrip.limit.perCharge': 'لكل شحنة',
|
||||
'roadtrip.limit.afterFill': 'توقف حتى {percent} ٪: {distance}',
|
||||
'roadtrip.limit.fullNote': 'التوقف يملأ بالكامل',
|
||||
'roadtrip.limit.rangeEmpty': 'لا يوجد مدى',
|
||||
'roadtrip.limit.rangeEmptyHint': 'أدخل المدى، أو املأ الحقول ليحسبه TREK.',
|
||||
'roadtrip.limit.specHint': 'عند تعبئتها، تحدد هذه الحقول المدى أعلاه.',
|
||||
'roadtrip.limit.vehicleHint': 'حدد نوع سيارتك لتُدخل بياناتها بدل المدى.',
|
||||
'roadtrip.limit.minutes': 'دقيقة',
|
||||
'roadtrip.limit.off': 'معطل',
|
||||
'roadtrip.limit.hint': 'يحسب وقت القيادة فقط. لا يشمل التوقفات والمبيت.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Trecho mais longo seguido',
|
||||
'roadtrip.limit.dayLabel': 'Condução por dia',
|
||||
'roadtrip.limit.rangeLabel': 'Autonomia por tanque',
|
||||
'roadtrip.limit.fillLabel': 'Abastecer até',
|
||||
'roadtrip.limit.fillFull': 'cheio',
|
||||
'roadtrip.limit.fillBadge': 'abastece até {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % perdidos pelo desgaste',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Autonomia por carga',
|
||||
'roadtrip.limit.sectionDriving': 'Condução',
|
||||
'roadtrip.limit.sectionVehicle': 'Veículo',
|
||||
'roadtrip.limit.tankLabel': 'Tanque',
|
||||
'roadtrip.limit.batteryLabel': 'Bateria',
|
||||
'roadtrip.limit.useLabel': 'Consumo',
|
||||
'roadtrip.limit.wearLabel': 'Desgaste da bateria',
|
||||
'roadtrip.limit.computed': 'Calculado a partir dos dados do veículo',
|
||||
'roadtrip.limit.perFill': 'por tanque',
|
||||
'roadtrip.limit.perCharge': 'por carga',
|
||||
'roadtrip.limit.afterFill': 'Uma parada até {percent} % dá {distance}',
|
||||
'roadtrip.limit.fullNote': 'Uma parada enche por completo',
|
||||
'roadtrip.limit.rangeEmpty': 'Sem autonomia',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Digite uma autonomia, ou preencha os campos e o TREK a calcula.',
|
||||
'roadtrip.limit.specHint': 'Preenchidos, estes campos definem a autonomia acima.',
|
||||
'roadtrip.limit.vehicleHint': 'Diga o que você dirige e poderá informar os dados em vez da autonomia.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'desligado',
|
||||
'roadtrip.limit.hint': 'Conta apenas o tempo de condução. Paradas e pernoites não estão incluídos.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Trajecte més llarg seguit',
|
||||
'roadtrip.limit.dayLabel': 'Conducció per dia',
|
||||
'roadtrip.limit.rangeLabel': 'Autonomia per dipòsit',
|
||||
'roadtrip.limit.fillLabel': 'Omple fins a',
|
||||
'roadtrip.limit.fillFull': 'ple',
|
||||
'roadtrip.limit.fillBadge': 'omple al {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % perdut per l’edat',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Autonomia per càrrega',
|
||||
'roadtrip.limit.sectionDriving': 'Conducció',
|
||||
'roadtrip.limit.sectionVehicle': 'Vehicle',
|
||||
'roadtrip.limit.tankLabel': 'Dipòsit',
|
||||
'roadtrip.limit.batteryLabel': 'Bateria',
|
||||
'roadtrip.limit.useLabel': 'Consum',
|
||||
'roadtrip.limit.wearLabel': 'Desgast bateria',
|
||||
'roadtrip.limit.computed': 'Calculat a partir de les dades del vehicle',
|
||||
'roadtrip.limit.perFill': 'per dipòsit',
|
||||
'roadtrip.limit.perCharge': 'per càrrega',
|
||||
'roadtrip.limit.afterFill': 'Una parada al {percent} % dóna {distance}',
|
||||
'roadtrip.limit.fullNote': 'Una parada omple del tot',
|
||||
'roadtrip.limit.rangeEmpty': 'Sense autonomia',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Escriu una autonomia, o omple els camps i TREK la calcula.',
|
||||
'roadtrip.limit.specHint': 'Omplerts, aquests camps determinen l’autonomia de dalt.',
|
||||
'roadtrip.limit.vehicleHint': 'Digues què condueixes i podràs donar-ne les dades en lloc de l’autonomia.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'desactivat',
|
||||
'roadtrip.limit.hint': 'Només compta el temps de conducció. Les parades i les nits no hi són incloses.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Nejdelší jízda v kuse',
|
||||
'roadtrip.limit.dayLabel': 'Jízda za den',
|
||||
'roadtrip.limit.rangeLabel': 'Dojezd na jednu nádrž',
|
||||
'roadtrip.limit.fillLabel': 'Natankovat na',
|
||||
'roadtrip.limit.fillFull': 'plná',
|
||||
'roadtrip.limit.fillBadge': 'tankuje na {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % ubylo stárnutím',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Dojezd na nabití',
|
||||
'roadtrip.limit.sectionDriving': 'Jízda',
|
||||
'roadtrip.limit.sectionVehicle': 'Vozidlo',
|
||||
'roadtrip.limit.tankLabel': 'Objem nádrže',
|
||||
'roadtrip.limit.batteryLabel': 'Baterie',
|
||||
'roadtrip.limit.useLabel': 'Spotřeba',
|
||||
'roadtrip.limit.wearLabel': 'Opotřebení baterie',
|
||||
'roadtrip.limit.computed': 'Vypočteno z údajů vozidla',
|
||||
'roadtrip.limit.perFill': 'na nádrž',
|
||||
'roadtrip.limit.perCharge': 'na nabití',
|
||||
'roadtrip.limit.afterFill': 'Zastávka na {percent} % dá {distance}',
|
||||
'roadtrip.limit.fullNote': 'Zastávka doplní do plna',
|
||||
'roadtrip.limit.rangeEmpty': 'Bez dojezdu',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Zadej dojezd, nebo vyplň pole a TREK ho spočítá.',
|
||||
'roadtrip.limit.specHint': 'Vyplněná pole určují dojezd nahoře.',
|
||||
'roadtrip.limit.vehicleHint': 'Uveď, čím jezdíš, a místo dojezdu můžeš zadat údaje vozidla.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'vypnuto',
|
||||
'roadtrip.limit.hint': 'Počítá jen čas jízdy. Zastávky a noclehy nejsou zahrnuty.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Längste Fahrt am Stück',
|
||||
'roadtrip.limit.dayLabel': 'Fahrzeit pro Tag',
|
||||
'roadtrip.limit.rangeLabel': 'Reichweite pro Tankfüllung',
|
||||
'roadtrip.limit.fillLabel': 'Tanken bis',
|
||||
'roadtrip.limit.fillFull': 'voll',
|
||||
'roadtrip.limit.fillBadge': 'tankt auf {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % durch Alterung weg',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Reichweite pro Ladung',
|
||||
'roadtrip.limit.sectionDriving': 'Fahren',
|
||||
'roadtrip.limit.sectionVehicle': 'Fahrzeug',
|
||||
'roadtrip.limit.tankLabel': 'Tankinhalt',
|
||||
'roadtrip.limit.batteryLabel': 'Akkukapazität',
|
||||
'roadtrip.limit.useLabel': 'Verbrauch',
|
||||
'roadtrip.limit.wearLabel': 'Akku-Alterung',
|
||||
'roadtrip.limit.computed': 'Aus den Fahrzeugdaten berechnet',
|
||||
'roadtrip.limit.perFill': 'pro Tankfüllung',
|
||||
'roadtrip.limit.perCharge': 'pro Ladung',
|
||||
'roadtrip.limit.afterFill': 'Ein Stopp auf {percent} % bringt {distance}',
|
||||
'roadtrip.limit.fullNote': 'Ein Stopp macht wieder voll',
|
||||
'roadtrip.limit.rangeEmpty': 'Keine Reichweite',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Trag eine Reichweite ein oder fülle die Felder aus, dann rechnet TREK sie aus.',
|
||||
'roadtrip.limit.specHint': 'Ausgefüllt bestimmen diese Werte die Reichweite oben.',
|
||||
'roadtrip.limit.vehicleHint': 'Sag, was du fährst, dann kannst du statt der Reichweite die Fahrzeugdaten angeben.',
|
||||
'roadtrip.limit.minutes': 'Min',
|
||||
'roadtrip.limit.off': 'aus',
|
||||
'roadtrip.limit.hint': 'Zählt nur Fahrzeit. Standzeiten und Übernachtungen sind nicht enthalten.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Longest drive at once',
|
||||
'roadtrip.limit.dayLabel': 'Driving per day',
|
||||
'roadtrip.limit.rangeLabel': 'Range on one tank',
|
||||
'roadtrip.limit.fillLabel': 'Fill up to',
|
||||
'roadtrip.limit.fillFull': 'full',
|
||||
'roadtrip.limit.fillBadge': 'fills to {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % lost to age',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Range on one charge',
|
||||
'roadtrip.limit.sectionDriving': 'Driving',
|
||||
'roadtrip.limit.sectionVehicle': 'Vehicle',
|
||||
'roadtrip.limit.tankLabel': 'Tank size',
|
||||
'roadtrip.limit.batteryLabel': 'Battery',
|
||||
'roadtrip.limit.useLabel': 'Consumption',
|
||||
'roadtrip.limit.wearLabel': 'Battery wear',
|
||||
'roadtrip.limit.computed': 'Worked out from the vehicle’s figures',
|
||||
'roadtrip.limit.perFill': 'per tank',
|
||||
'roadtrip.limit.perCharge': 'per charge',
|
||||
'roadtrip.limit.afterFill': 'A {percent} % stop gives {distance}',
|
||||
'roadtrip.limit.fullNote': 'A stop fills right up',
|
||||
'roadtrip.limit.rangeEmpty': 'No range set',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Type a range, or fill in the figures below and TREK works it out.',
|
||||
'roadtrip.limit.specHint': 'Filled in, these decide the range above.',
|
||||
'roadtrip.limit.vehicleHint': 'Name the kind of car and you can state its figures instead of a range.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'off',
|
||||
'roadtrip.limit.hint': 'Counts driving time only. Time spent at stops and overnight legs are not included.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Trayecto más largo seguido',
|
||||
'roadtrip.limit.dayLabel': 'Conducción por día',
|
||||
'roadtrip.limit.rangeLabel': 'Autonomía por depósito',
|
||||
'roadtrip.limit.fillLabel': 'Repostar hasta',
|
||||
'roadtrip.limit.fillFull': 'lleno',
|
||||
'roadtrip.limit.fillBadge': 'reposta al {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % perdido por el desgaste',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Autonomía por carga',
|
||||
'roadtrip.limit.sectionDriving': 'Conducción',
|
||||
'roadtrip.limit.sectionVehicle': 'Vehículo',
|
||||
'roadtrip.limit.tankLabel': 'Depósito',
|
||||
'roadtrip.limit.batteryLabel': 'Batería',
|
||||
'roadtrip.limit.useLabel': 'Consumo',
|
||||
'roadtrip.limit.wearLabel': 'Desgaste batería',
|
||||
'roadtrip.limit.computed': 'Calculado con los datos del vehículo',
|
||||
'roadtrip.limit.perFill': 'por depósito',
|
||||
'roadtrip.limit.perCharge': 'por carga',
|
||||
'roadtrip.limit.afterFill': 'Una parada al {percent} % da {distance}',
|
||||
'roadtrip.limit.fullNote': 'Una parada llena del todo',
|
||||
'roadtrip.limit.rangeEmpty': 'Sin autonomía',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Escribe una autonomía, o rellena los campos y TREK la calcula.',
|
||||
'roadtrip.limit.specHint': 'Rellenos, estos campos deciden la autonomía de arriba.',
|
||||
'roadtrip.limit.vehicleHint': 'Indica el tipo de vehículo y podrás dar sus datos en vez de la autonomía.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'desactivado',
|
||||
'roadtrip.limit.hint': 'Solo cuenta el tiempo de conducción. Las paradas y las noches no se incluyen.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Plus long trajet d’affilée',
|
||||
'roadtrip.limit.dayLabel': 'Conduite par jour',
|
||||
'roadtrip.limit.rangeLabel': 'Autonomie par plein',
|
||||
'roadtrip.limit.fillLabel': 'Remplir jusqu’à',
|
||||
'roadtrip.limit.fillFull': 'plein',
|
||||
'roadtrip.limit.fillBadge': 'remplit à {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % perdus avec l’âge',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Autonomie par charge',
|
||||
'roadtrip.limit.sectionDriving': 'Conduite',
|
||||
'roadtrip.limit.sectionVehicle': 'Véhicule',
|
||||
'roadtrip.limit.tankLabel': 'Réservoir',
|
||||
'roadtrip.limit.batteryLabel': 'Batterie',
|
||||
'roadtrip.limit.useLabel': 'Consommation',
|
||||
'roadtrip.limit.wearLabel': 'Usure batterie',
|
||||
'roadtrip.limit.computed': 'Calculé à partir des données du véhicule',
|
||||
'roadtrip.limit.perFill': 'par plein',
|
||||
'roadtrip.limit.perCharge': 'par charge',
|
||||
'roadtrip.limit.afterFill': 'Un arrêt à {percent} % donne {distance}',
|
||||
'roadtrip.limit.fullNote': 'Un arrêt fait le plein',
|
||||
'roadtrip.limit.rangeEmpty': 'Aucune autonomie',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Saisissez une autonomie, ou remplissez les champs et TREK la calcule.',
|
||||
'roadtrip.limit.specHint': 'Remplis, ces champs déterminent l’autonomie ci-dessus.',
|
||||
'roadtrip.limit.vehicleHint': 'Indiquez le type de véhicule pour saisir ses données au lieu d’une autonomie.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'désactivé',
|
||||
'roadtrip.limit.hint': 'Compte uniquement le temps de conduite. Les arrêts et les nuits ne sont pas inclus.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Μεγαλύτερη συνεχής οδήγηση',
|
||||
'roadtrip.limit.dayLabel': 'Οδήγηση ανά ημέρα',
|
||||
'roadtrip.limit.rangeLabel': 'Αυτονομία με ένα γέμισμα',
|
||||
'roadtrip.limit.fillLabel': 'Γέμισμα έως',
|
||||
'roadtrip.limit.fillFull': 'γεμάτο',
|
||||
'roadtrip.limit.fillBadge': 'γεμίζει στο {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % χαμένο από τη φθορά',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Αυτονομία ανά φόρτιση',
|
||||
'roadtrip.limit.sectionDriving': 'Οδήγηση',
|
||||
'roadtrip.limit.sectionVehicle': 'Όχημα',
|
||||
'roadtrip.limit.tankLabel': 'Ρεζερβουάρ',
|
||||
'roadtrip.limit.batteryLabel': 'Μπαταρία',
|
||||
'roadtrip.limit.useLabel': 'Κατανάλωση',
|
||||
'roadtrip.limit.wearLabel': 'Φθορά μπαταρίας',
|
||||
'roadtrip.limit.computed': 'Υπολογίζεται από τα στοιχεία του οχήματος',
|
||||
'roadtrip.limit.perFill': 'ανά γέμισμα',
|
||||
'roadtrip.limit.perCharge': 'ανά φόρτιση',
|
||||
'roadtrip.limit.afterFill': 'Στάση στο {percent} %: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Μια στάση γεμίζει τελείως',
|
||||
'roadtrip.limit.rangeEmpty': 'Χωρίς αυτονομία',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Δώσε μια αυτονομία ή συμπλήρωσε τα πεδία και το TREK την υπολογίζει.',
|
||||
'roadtrip.limit.specHint': 'Συμπληρωμένα, αυτά καθορίζουν την αυτονομία πάνω.',
|
||||
'roadtrip.limit.vehicleHint': 'Δήλωσε τι οδηγείς και δώσε τα στοιχεία του αντί για αυτονομία.',
|
||||
'roadtrip.limit.minutes': 'λεπ',
|
||||
'roadtrip.limit.off': 'ανενεργό',
|
||||
'roadtrip.limit.hint': 'Μετρά μόνο τον χρόνο οδήγησης. Οι στάσεις και οι διανυκτερεύσεις δεν περιλαμβάνονται.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Leghosszabb egyhuzamban vezetés',
|
||||
'roadtrip.limit.dayLabel': 'Vezetés naponta',
|
||||
'roadtrip.limit.rangeLabel': 'Hatótáv egy tankkal',
|
||||
'roadtrip.limit.fillLabel': 'Feltöltés eddig',
|
||||
'roadtrip.limit.fillFull': 'tele',
|
||||
'roadtrip.limit.fillBadge': '{percent} %-ig tölt',
|
||||
'roadtrip.limit.wearNote': '{percent} % elveszett az öregedéssel',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Hatótáv egy töltéssel',
|
||||
'roadtrip.limit.sectionDriving': 'Vezetés',
|
||||
'roadtrip.limit.sectionVehicle': 'Jármű',
|
||||
'roadtrip.limit.tankLabel': 'Tank mérete',
|
||||
'roadtrip.limit.batteryLabel': 'Akkumulátor',
|
||||
'roadtrip.limit.useLabel': 'Fogyasztás',
|
||||
'roadtrip.limit.wearLabel': 'Akkumulátor kopása',
|
||||
'roadtrip.limit.computed': 'A jármű adataiból számítva',
|
||||
'roadtrip.limit.perFill': 'tankonként',
|
||||
'roadtrip.limit.perCharge': 'töltésenként',
|
||||
'roadtrip.limit.afterFill': '{percent} %-ig töltve: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Egy megálló teletölt',
|
||||
'roadtrip.limit.rangeEmpty': 'Nincs hatótáv',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Add meg a hatótávot, vagy töltsd ki a mezőket, és a TREK kiszámolja.',
|
||||
'roadtrip.limit.specHint': 'Kitöltve ezek adják a fenti hatótávot.',
|
||||
'roadtrip.limit.vehicleHint': 'Add meg, mivel utazol, és hatótáv helyett a jármű adatait is megadhatod.',
|
||||
'roadtrip.limit.minutes': 'perc',
|
||||
'roadtrip.limit.off': 'ki',
|
||||
'roadtrip.limit.hint': 'Csak a vezetési időt számolja. A megállók és az éjszakázások nincsenek benne.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Perjalanan terlama sekali jalan',
|
||||
'roadtrip.limit.dayLabel': 'Mengemudi per hari',
|
||||
'roadtrip.limit.rangeLabel': 'Jarak per tangki',
|
||||
'roadtrip.limit.fillLabel': 'Isi sampai',
|
||||
'roadtrip.limit.fillFull': 'penuh',
|
||||
'roadtrip.limit.fillBadge': 'isi sampai {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % hilang karena usia',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Jarak per pengisian',
|
||||
'roadtrip.limit.sectionDriving': 'Mengemudi',
|
||||
'roadtrip.limit.sectionVehicle': 'Kendaraan',
|
||||
'roadtrip.limit.tankLabel': 'Kapasitas tangki',
|
||||
'roadtrip.limit.batteryLabel': 'Baterai',
|
||||
'roadtrip.limit.useLabel': 'Konsumsi',
|
||||
'roadtrip.limit.wearLabel': 'Penurunan baterai',
|
||||
'roadtrip.limit.computed': 'Dihitung dari data kendaraan',
|
||||
'roadtrip.limit.perFill': 'per tangki',
|
||||
'roadtrip.limit.perCharge': 'per pengisian',
|
||||
'roadtrip.limit.afterFill': 'Berhenti sampai {percent} %: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Satu perhentian mengisi penuh',
|
||||
'roadtrip.limit.rangeEmpty': 'Belum ada jarak jangkau',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Isi jarak jangkau, atau isi kolomnya dan TREK yang menghitung.',
|
||||
'roadtrip.limit.specHint': 'Bila diisi, kolom ini menentukan jarak jangkau di atas.',
|
||||
'roadtrip.limit.vehicleHint': 'Sebutkan jenis kendaraanmu, lalu isi datanya alih-alih jarak jangkau.',
|
||||
'roadtrip.limit.minutes': 'mnt',
|
||||
'roadtrip.limit.off': 'mati',
|
||||
'roadtrip.limit.hint': 'Hanya menghitung waktu mengemudi. Waktu berhenti dan menginap tidak termasuk.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Tratto più lungo di fila',
|
||||
'roadtrip.limit.dayLabel': 'Guida al giorno',
|
||||
'roadtrip.limit.rangeLabel': 'Autonomia con un pieno',
|
||||
'roadtrip.limit.fillLabel': 'Riempi fino a',
|
||||
'roadtrip.limit.fillFull': 'pieno',
|
||||
'roadtrip.limit.fillBadge': 'riempie al {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % perso per l’età',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Autonomia per ricarica',
|
||||
'roadtrip.limit.sectionDriving': 'Guida',
|
||||
'roadtrip.limit.sectionVehicle': 'Veicolo',
|
||||
'roadtrip.limit.tankLabel': 'Serbatoio',
|
||||
'roadtrip.limit.batteryLabel': 'Batteria',
|
||||
'roadtrip.limit.useLabel': 'Consumo',
|
||||
'roadtrip.limit.wearLabel': 'Degrado batteria',
|
||||
'roadtrip.limit.computed': 'Calcolato dai dati del veicolo',
|
||||
'roadtrip.limit.perFill': 'per pieno',
|
||||
'roadtrip.limit.perCharge': 'per ricarica',
|
||||
'roadtrip.limit.afterFill': 'Una sosta al {percent} % dà {distance}',
|
||||
'roadtrip.limit.fullNote': 'Una sosta riempie del tutto',
|
||||
'roadtrip.limit.rangeEmpty': 'Nessuna autonomia',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Inserisci un’autonomia, oppure compila i campi e TREK la calcola.',
|
||||
'roadtrip.limit.specHint': 'Se compilati, questi campi determinano l’autonomia sopra.',
|
||||
'roadtrip.limit.vehicleHint': 'Indica il tipo di veicolo per inserire i suoi dati invece dell’autonomia.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'off',
|
||||
'roadtrip.limit.hint': 'Conta solo il tempo di guida. Le soste e i pernottamenti non sono inclusi.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': '連続運転の上限',
|
||||
'roadtrip.limit.dayLabel': '1日の運転',
|
||||
'roadtrip.limit.rangeLabel': '満タンでの航続',
|
||||
'roadtrip.limit.fillLabel': '補給する量',
|
||||
'roadtrip.limit.fillFull': '満タン',
|
||||
'roadtrip.limit.fillBadge': '{percent} % まで補給',
|
||||
'roadtrip.limit.wearNote': '劣化で {percent} % 減',
|
||||
'roadtrip.limit.rangeLabelCharge': '1回の充電での航続距離',
|
||||
'roadtrip.limit.sectionDriving': '運転',
|
||||
'roadtrip.limit.sectionVehicle': '車両',
|
||||
'roadtrip.limit.tankLabel': 'タンク容量',
|
||||
'roadtrip.limit.batteryLabel': 'バッテリー容量',
|
||||
'roadtrip.limit.useLabel': '消費量',
|
||||
'roadtrip.limit.wearLabel': 'バッテリー劣化',
|
||||
'roadtrip.limit.computed': '車両データから算出',
|
||||
'roadtrip.limit.perFill': '満タンあたり',
|
||||
'roadtrip.limit.perCharge': '1回の充電で',
|
||||
'roadtrip.limit.afterFill': '{percent} % まで補給で {distance}',
|
||||
'roadtrip.limit.fullNote': '停車で満タンまで補給',
|
||||
'roadtrip.limit.rangeEmpty': '航続距離なし',
|
||||
'roadtrip.limit.rangeEmptyHint': '航続距離を入力するか、下の項目を埋めれば TREK が計算します。',
|
||||
'roadtrip.limit.specHint': '入力すると、上の航続距離はここから決まります。',
|
||||
'roadtrip.limit.vehicleHint': '何で走るかを選ぶと、航続距離の代わりに車両データを入力できます。',
|
||||
'roadtrip.limit.minutes': '分',
|
||||
'roadtrip.limit.off': 'なし',
|
||||
'roadtrip.limit.hint': '運転時間のみを数えます。滞在時間と宿泊は含みません。',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': '연속 운전 최대',
|
||||
'roadtrip.limit.dayLabel': '하루 운전',
|
||||
'roadtrip.limit.rangeLabel': '1회 주유 주행거리',
|
||||
'roadtrip.limit.fillLabel': '충전 목표',
|
||||
'roadtrip.limit.fillFull': '가득',
|
||||
'roadtrip.limit.fillBadge': '{percent} %까지 충전',
|
||||
'roadtrip.limit.wearNote': '노후로 {percent} % 감소',
|
||||
'roadtrip.limit.rangeLabelCharge': '한 번 충전 주행거리',
|
||||
'roadtrip.limit.sectionDriving': '운전',
|
||||
'roadtrip.limit.sectionVehicle': '차량',
|
||||
'roadtrip.limit.tankLabel': '연료탱크',
|
||||
'roadtrip.limit.batteryLabel': '배터리 용량',
|
||||
'roadtrip.limit.useLabel': '소비량',
|
||||
'roadtrip.limit.wearLabel': '배터리 열화',
|
||||
'roadtrip.limit.computed': '차량 정보로 계산',
|
||||
'roadtrip.limit.perFill': '한 탱크당',
|
||||
'roadtrip.limit.perCharge': '한 번 충전당',
|
||||
'roadtrip.limit.afterFill': '{percent} %까지 충전 시 {distance}',
|
||||
'roadtrip.limit.fullNote': '정차 시 가득 충전',
|
||||
'roadtrip.limit.rangeEmpty': '주행거리 없음',
|
||||
'roadtrip.limit.rangeEmptyHint': '주행거리를 입력하거나 아래 항목을 채우면 TREK이 계산합니다.',
|
||||
'roadtrip.limit.specHint': '입력하면 위 주행거리는 이 값으로 정해집니다.',
|
||||
'roadtrip.limit.vehicleHint': '어떤 차인지 고르면 주행거리 대신 차량 정보를 입력할 수 있습니다.',
|
||||
'roadtrip.limit.minutes': '분',
|
||||
'roadtrip.limit.off': '끔',
|
||||
'roadtrip.limit.hint': '운전 시간만 계산합니다. 정차 시간과 숙박은 포함되지 않습니다.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Langste rit achtereen',
|
||||
'roadtrip.limit.dayLabel': 'Rijden per dag',
|
||||
'roadtrip.limit.rangeLabel': 'Bereik per tank',
|
||||
'roadtrip.limit.fillLabel': 'Vullen tot',
|
||||
'roadtrip.limit.fillFull': 'vol',
|
||||
'roadtrip.limit.fillBadge': 'vult tot {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % verloren door veroudering',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Actieradius per lading',
|
||||
'roadtrip.limit.sectionDriving': 'Rijden',
|
||||
'roadtrip.limit.sectionVehicle': 'Voertuig',
|
||||
'roadtrip.limit.tankLabel': 'Tankinhoud',
|
||||
'roadtrip.limit.batteryLabel': 'Accu',
|
||||
'roadtrip.limit.useLabel': 'Verbruik',
|
||||
'roadtrip.limit.wearLabel': 'Accuslijtage',
|
||||
'roadtrip.limit.computed': 'Berekend uit de voertuiggegevens',
|
||||
'roadtrip.limit.perFill': 'per tankbeurt',
|
||||
'roadtrip.limit.perCharge': 'per lading',
|
||||
'roadtrip.limit.afterFill': 'Een stop tot {percent} % geeft {distance}',
|
||||
'roadtrip.limit.fullNote': 'Een stop maakt weer helemaal vol',
|
||||
'roadtrip.limit.rangeEmpty': 'Geen actieradius',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Vul een actieradius in, of vul de velden in en TREK rekent hem uit.',
|
||||
'roadtrip.limit.specHint': 'Ingevuld bepalen deze velden de actieradius hierboven.',
|
||||
'roadtrip.limit.vehicleHint': 'Geef aan wat je rijdt, dan kun je de voertuiggegevens invullen in plaats van een actieradius.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'uit',
|
||||
'roadtrip.limit.hint': 'Telt alleen rijtijd. Tijd bij stops en overnachtingen tellen niet mee.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Najdłuższa jazda bez przerwy',
|
||||
'roadtrip.limit.dayLabel': 'Jazda dziennie',
|
||||
'roadtrip.limit.rangeLabel': 'Zasięg na jednym baku',
|
||||
'roadtrip.limit.fillLabel': 'Tankuj do',
|
||||
'roadtrip.limit.fillFull': 'pełny',
|
||||
'roadtrip.limit.fillBadge': 'tankuje do {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % utracone przez zużycie',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Zasięg na ładowaniu',
|
||||
'roadtrip.limit.sectionDriving': 'Jazda',
|
||||
'roadtrip.limit.sectionVehicle': 'Pojazd',
|
||||
'roadtrip.limit.tankLabel': 'Pojemność baku',
|
||||
'roadtrip.limit.batteryLabel': 'Akumulator',
|
||||
'roadtrip.limit.useLabel': 'Zużycie',
|
||||
'roadtrip.limit.wearLabel': 'Zużycie akumulatora',
|
||||
'roadtrip.limit.computed': 'Obliczone z danych pojazdu',
|
||||
'roadtrip.limit.perFill': 'na baku',
|
||||
'roadtrip.limit.perCharge': 'na ładowaniu',
|
||||
'roadtrip.limit.afterFill': 'Postój do {percent} % daje {distance}',
|
||||
'roadtrip.limit.fullNote': 'Postój napełnia do pełna',
|
||||
'roadtrip.limit.rangeEmpty': 'Brak zasięgu',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Wpisz zasięg albo wypełnij pola, a TREK go obliczy.',
|
||||
'roadtrip.limit.specHint': 'Wypełnione, te pola wyznaczają zasięg powyżej.',
|
||||
'roadtrip.limit.vehicleHint': 'Podaj rodzaj pojazdu, a zamiast zasięgu wpiszesz jego dane.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'wył.',
|
||||
'roadtrip.limit.hint': 'Liczy tylko czas jazdy. Postoje i noclegi nie są wliczane.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Самый долгий перегон',
|
||||
'roadtrip.limit.dayLabel': 'Вождение за день',
|
||||
'roadtrip.limit.rangeLabel': 'Запас хода на баке',
|
||||
'roadtrip.limit.fillLabel': 'Заправлять до',
|
||||
'roadtrip.limit.fillFull': 'полный',
|
||||
'roadtrip.limit.fillBadge': 'заправка до {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % потеряно из-за износа',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Запас хода на заряде',
|
||||
'roadtrip.limit.sectionDriving': 'Вождение',
|
||||
'roadtrip.limit.sectionVehicle': 'Автомобиль',
|
||||
'roadtrip.limit.tankLabel': 'Объём бака',
|
||||
'roadtrip.limit.batteryLabel': 'Батарея',
|
||||
'roadtrip.limit.useLabel': 'Расход',
|
||||
'roadtrip.limit.wearLabel': 'Износ батареи',
|
||||
'roadtrip.limit.computed': 'Рассчитано по данным автомобиля',
|
||||
'roadtrip.limit.perFill': 'на баке',
|
||||
'roadtrip.limit.perCharge': 'на заряде',
|
||||
'roadtrip.limit.afterFill': 'Остановка до {percent} %: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Остановка заправляет полностью',
|
||||
'roadtrip.limit.rangeEmpty': 'Запас хода не задан',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Укажите запас хода или заполните поля, и TREK посчитает сам.',
|
||||
'roadtrip.limit.specHint': 'Заполненные, эти поля задают запас хода выше.',
|
||||
'roadtrip.limit.vehicleHint': 'Укажите тип машины, и вместо запаса хода можно ввести её данные.',
|
||||
'roadtrip.limit.minutes': 'мин',
|
||||
'roadtrip.limit.off': 'выкл',
|
||||
'roadtrip.limit.hint': 'Учитывается только время за рулём. Остановки и ночёвки не входят.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Längsta körning i sträck',
|
||||
'roadtrip.limit.dayLabel': 'Körning per dag',
|
||||
'roadtrip.limit.rangeLabel': 'Räckvidd per tank',
|
||||
'roadtrip.limit.fillLabel': 'Fyll till',
|
||||
'roadtrip.limit.fillFull': 'fullt',
|
||||
'roadtrip.limit.fillBadge': 'fyller till {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % förlorat med åldern',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Räckvidd per laddning',
|
||||
'roadtrip.limit.sectionDriving': 'Körning',
|
||||
'roadtrip.limit.sectionVehicle': 'Fordon',
|
||||
'roadtrip.limit.tankLabel': 'Tankvolym',
|
||||
'roadtrip.limit.batteryLabel': 'Batteri',
|
||||
'roadtrip.limit.useLabel': 'Förbrukning',
|
||||
'roadtrip.limit.wearLabel': 'Batterislitage',
|
||||
'roadtrip.limit.computed': 'Beräknat från fordonets uppgifter',
|
||||
'roadtrip.limit.perFill': 'per tank',
|
||||
'roadtrip.limit.perCharge': 'per laddning',
|
||||
'roadtrip.limit.afterFill': 'Ett stopp till {percent} % ger {distance}',
|
||||
'roadtrip.limit.fullNote': 'Ett stopp fyller helt',
|
||||
'roadtrip.limit.rangeEmpty': 'Ingen räckvidd',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Ange en räckvidd, eller fyll i fälten så räknar TREK ut den.',
|
||||
'roadtrip.limit.specHint': 'Ifyllda bestämmer de här fälten räckvidden ovan.',
|
||||
'roadtrip.limit.vehicleHint': 'Ange vad du kör så kan du fylla i fordonets uppgifter i stället för en räckvidd.',
|
||||
'roadtrip.limit.minutes': 'min',
|
||||
'roadtrip.limit.off': 'av',
|
||||
'roadtrip.limit.hint': 'Räknar bara körtid. Tid vid stopp och övernattningar ingår inte.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Aralıksız en uzun sürüş',
|
||||
'roadtrip.limit.dayLabel': 'Günlük sürüş',
|
||||
'roadtrip.limit.rangeLabel': 'Bir depoyla menzil',
|
||||
'roadtrip.limit.fillLabel': 'Şu kadar doldur',
|
||||
'roadtrip.limit.fillFull': 'dolu',
|
||||
'roadtrip.limit.fillBadge': '%{percent} doldurur',
|
||||
'roadtrip.limit.wearNote': '%{percent} yaşlanmayla kayıp',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Şarj başına menzil',
|
||||
'roadtrip.limit.sectionDriving': 'Sürüş',
|
||||
'roadtrip.limit.sectionVehicle': 'Araç',
|
||||
'roadtrip.limit.tankLabel': 'Depo hacmi',
|
||||
'roadtrip.limit.batteryLabel': 'Batarya',
|
||||
'roadtrip.limit.useLabel': 'Tüketim',
|
||||
'roadtrip.limit.wearLabel': 'Batarya yıpranması',
|
||||
'roadtrip.limit.computed': 'Araç verilerinden hesaplandı',
|
||||
'roadtrip.limit.perFill': 'depo başına',
|
||||
'roadtrip.limit.perCharge': 'şarj başına',
|
||||
'roadtrip.limit.afterFill': '%{percent} dolum: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Bir mola tamamen doldurur',
|
||||
'roadtrip.limit.rangeEmpty': 'Menzil yok',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Bir menzil gir ya da alanları doldur, TREK hesaplasın.',
|
||||
'roadtrip.limit.specHint': 'Doldurulduğunda bu alanlar yukarıdaki menzili belirler.',
|
||||
'roadtrip.limit.vehicleHint': 'Ne kullandığını seç, menzil yerine araç verilerini girebilirsin.',
|
||||
'roadtrip.limit.minutes': 'dk',
|
||||
'roadtrip.limit.off': 'kapalı',
|
||||
'roadtrip.limit.hint': 'Yalnızca sürüş süresini sayar. Molalar ve geceleme dahil değildir.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Найдовший перегін',
|
||||
'roadtrip.limit.dayLabel': 'Водіння за день',
|
||||
'roadtrip.limit.rangeLabel': 'Запас ходу на баку',
|
||||
'roadtrip.limit.fillLabel': 'Заправляти до',
|
||||
'roadtrip.limit.fillFull': 'повний',
|
||||
'roadtrip.limit.fillBadge': 'заправка до {percent} %',
|
||||
'roadtrip.limit.wearNote': '{percent} % втрачено через знос',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Запас ходу на заряді',
|
||||
'roadtrip.limit.sectionDriving': 'Водіння',
|
||||
'roadtrip.limit.sectionVehicle': 'Автомобіль',
|
||||
'roadtrip.limit.tankLabel': 'Обʼєм бака',
|
||||
'roadtrip.limit.batteryLabel': 'Батарея',
|
||||
'roadtrip.limit.useLabel': 'Витрата',
|
||||
'roadtrip.limit.wearLabel': 'Знос батареї',
|
||||
'roadtrip.limit.computed': 'Розраховано за даними автомобіля',
|
||||
'roadtrip.limit.perFill': 'на баку',
|
||||
'roadtrip.limit.perCharge': 'на заряді',
|
||||
'roadtrip.limit.afterFill': 'Зупинка до {percent} %: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Зупинка заправляє повністю',
|
||||
'roadtrip.limit.rangeEmpty': 'Запас ходу не задано',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Вкажіть запас ходу або заповніть поля, і TREK порахує сам.',
|
||||
'roadtrip.limit.specHint': 'Заповнені, ці поля задають запас ходу вище.',
|
||||
'roadtrip.limit.vehicleHint': 'Вкажіть тип авто, і замість запасу ходу можна ввести його дані.',
|
||||
'roadtrip.limit.minutes': 'хв',
|
||||
'roadtrip.limit.off': 'вимк',
|
||||
'roadtrip.limit.hint': 'Рахує лише час за кермом. Зупинки та ночівлі не враховано.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': 'Chặng lái dài nhất',
|
||||
'roadtrip.limit.dayLabel': 'Lái xe mỗi ngày',
|
||||
'roadtrip.limit.rangeLabel': 'Quãng đường mỗi bình',
|
||||
'roadtrip.limit.fillLabel': 'Đổ đến',
|
||||
'roadtrip.limit.fillFull': 'đầy',
|
||||
'roadtrip.limit.fillBadge': 'đổ đến {percent} %',
|
||||
'roadtrip.limit.wearNote': 'mất {percent} % do chai',
|
||||
'roadtrip.limit.rangeLabelCharge': 'Quãng đường mỗi lần sạc',
|
||||
'roadtrip.limit.sectionDriving': 'Lái xe',
|
||||
'roadtrip.limit.sectionVehicle': 'Phương tiện',
|
||||
'roadtrip.limit.tankLabel': 'Dung tích bình',
|
||||
'roadtrip.limit.batteryLabel': 'Pin',
|
||||
'roadtrip.limit.useLabel': 'Mức tiêu thụ',
|
||||
'roadtrip.limit.wearLabel': 'Chai pin',
|
||||
'roadtrip.limit.computed': 'Tính từ thông số xe',
|
||||
'roadtrip.limit.perFill': 'mỗi bình',
|
||||
'roadtrip.limit.perCharge': 'mỗi lần sạc',
|
||||
'roadtrip.limit.afterFill': 'Dừng đến {percent} %: {distance}',
|
||||
'roadtrip.limit.fullNote': 'Một lần dừng đổ đầy',
|
||||
'roadtrip.limit.rangeEmpty': 'Chưa có quãng đường',
|
||||
'roadtrip.limit.rangeEmptyHint': 'Nhập quãng đường, hoặc điền các ô để TREK tự tính.',
|
||||
'roadtrip.limit.specHint': 'Khi điền, các ô này quyết định quãng đường ở trên.',
|
||||
'roadtrip.limit.vehicleHint': 'Chọn loại xe để nhập thông số thay cho quãng đường.',
|
||||
'roadtrip.limit.minutes': 'phút',
|
||||
'roadtrip.limit.off': 'tắt',
|
||||
'roadtrip.limit.hint': 'Chỉ tính thời gian lái. Thời gian dừng và nghỉ đêm không được tính.',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': '單次最長駕駛',
|
||||
'roadtrip.limit.dayLabel': '每天駕駛',
|
||||
'roadtrip.limit.rangeLabel': '單箱續航',
|
||||
'roadtrip.limit.fillLabel': '加到',
|
||||
'roadtrip.limit.fillFull': '滿',
|
||||
'roadtrip.limit.fillBadge': '加到 {percent} %',
|
||||
'roadtrip.limit.wearNote': '衰減損失 {percent} %',
|
||||
'roadtrip.limit.rangeLabelCharge': '每次充電續航',
|
||||
'roadtrip.limit.sectionDriving': '駕駛',
|
||||
'roadtrip.limit.sectionVehicle': '車輛',
|
||||
'roadtrip.limit.tankLabel': '油箱容量',
|
||||
'roadtrip.limit.batteryLabel': '電池容量',
|
||||
'roadtrip.limit.useLabel': '能耗',
|
||||
'roadtrip.limit.wearLabel': '電池衰減',
|
||||
'roadtrip.limit.computed': '根據車輛資料計算',
|
||||
'roadtrip.limit.perFill': '每箱油',
|
||||
'roadtrip.limit.perCharge': '每次充電',
|
||||
'roadtrip.limit.afterFill': '加到 {percent} % 可行駛 {distance}',
|
||||
'roadtrip.limit.fullNote': '每次停靠加滿',
|
||||
'roadtrip.limit.rangeEmpty': '未設定續航',
|
||||
'roadtrip.limit.rangeEmptyHint': '填寫續航里程,或填好下面的資料,TREK 會自己算。',
|
||||
'roadtrip.limit.specHint': '填寫後,上面的續航由這些資料決定。',
|
||||
'roadtrip.limit.vehicleHint': '選擇車輛類型後,可以填車輛資料代替續航里程。',
|
||||
'roadtrip.limit.minutes': '分鐘',
|
||||
'roadtrip.limit.off': '關閉',
|
||||
'roadtrip.limit.hint': '僅計算駕駛時間,不含停留與過夜。',
|
||||
|
||||
@@ -22,6 +22,26 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.limit.legLabel': '单次最长驾驶',
|
||||
'roadtrip.limit.dayLabel': '每天驾驶',
|
||||
'roadtrip.limit.rangeLabel': '单箱续航',
|
||||
'roadtrip.limit.fillLabel': '加到',
|
||||
'roadtrip.limit.fillFull': '满',
|
||||
'roadtrip.limit.fillBadge': '加到 {percent} %',
|
||||
'roadtrip.limit.wearNote': '衰减损失 {percent} %',
|
||||
'roadtrip.limit.rangeLabelCharge': '每次充电续航',
|
||||
'roadtrip.limit.sectionDriving': '驾驶',
|
||||
'roadtrip.limit.sectionVehicle': '车辆',
|
||||
'roadtrip.limit.tankLabel': '油箱容量',
|
||||
'roadtrip.limit.batteryLabel': '电池容量',
|
||||
'roadtrip.limit.useLabel': '能耗',
|
||||
'roadtrip.limit.wearLabel': '电池衰减',
|
||||
'roadtrip.limit.computed': '根据车辆数据计算',
|
||||
'roadtrip.limit.perFill': '每箱油',
|
||||
'roadtrip.limit.perCharge': '每次充电',
|
||||
'roadtrip.limit.afterFill': '加到 {percent} % 可行驶 {distance}',
|
||||
'roadtrip.limit.fullNote': '每次停靠加满',
|
||||
'roadtrip.limit.rangeEmpty': '未设置续航',
|
||||
'roadtrip.limit.rangeEmptyHint': '填写续航里程,或填好下面的数据,TREK 会自己算。',
|
||||
'roadtrip.limit.specHint': '填写后,上面的续航由这些数据决定。',
|
||||
'roadtrip.limit.vehicleHint': '选择车辆类型后,可以填车辆数据代替续航里程。',
|
||||
'roadtrip.limit.minutes': '分钟',
|
||||
'roadtrip.limit.off': '关闭',
|
||||
'roadtrip.limit.hint': '仅计算驾驶时间,不含停留和过夜。',
|
||||
|
||||
Reference in New Issue
Block a user