mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-07-09 15:05:59 +00:00
fix: address review feedback on the distance unit setting
- server: allow distance_unit as an admin default (+ value validation) so the Admin "Default User Settings" toggle persists instead of returning 400 - i18n: add settings.distance to all 20 locales and translate the labels through t() instead of hardcoding "Distance" - route legs: include the unit in the OSRM cache key and recompute on a unit switch, so map and sidebar distances refresh and never mix units - keep wind speed tied to the temperature unit — a distance setting must not silently flip existing Fahrenheit users from mph to km/h - restore the sub-1km metres reading for metric, convert GPX elevation to feet for imperial, and format distances with a '.' decimal in every locale - add units.test.ts
This commit is contained in:
@@ -214,7 +214,7 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
|
|||||||
</OptionRow>
|
</OptionRow>
|
||||||
|
|
||||||
{/* Distance */}
|
{/* Distance */}
|
||||||
<OptionRow label={<>Distance <ResetButton field="distance_unit" /></>}>
|
<OptionRow label={<>{t('settings.distance')} <ResetButton field="distance_unit" /></>}>
|
||||||
{([
|
{([
|
||||||
{ value: 'metric', label: 'km Metric' },
|
{ value: 'metric', label: 'km Metric' },
|
||||||
{ value: 'imperial', label: 'mi Imperial' },
|
{ value: 'imperial', label: 'mi Imperial' },
|
||||||
|
|||||||
@@ -240,7 +240,9 @@ export async function calculateRouteWithLegs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const coords = waypoints.map((p) => `${p.lng},${p.lat}`).join(';')
|
const coords = waypoints.map((p) => `${p.lng},${p.lat}`).join(';')
|
||||||
const cacheKey = `${profile}:${coords}`
|
// The cached result carries formatted leg distances, so the active distance unit is
|
||||||
|
// part of the key — otherwise switching km↔mi would return stale text (#1300).
|
||||||
|
const cacheKey = `${profile}:${getDistanceUnit()}:${coords}`
|
||||||
const cached = routeCache.get(cacheKey)
|
const cached = routeCache.get(cacheKey)
|
||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
|
|
||||||
|
|||||||
@@ -402,9 +402,9 @@ describe('DayDetailPanel', () => {
|
|||||||
await screen.findByText('20:15');
|
await screen.findByText('20:15');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('FE-PLANNER-DAYDETAIL-027: weather chips show imperial wind speed', async () => {
|
it('FE-PLANNER-DAYDETAIL-027: weather chips show Fahrenheit wind speed', async () => {
|
||||||
seedStore(useSettingsStore, {
|
seedStore(useSettingsStore, {
|
||||||
settings: { time_format: '24h', temperature_unit: 'celsius', distance_unit: 'imperial', blur_booking_codes: false },
|
settings: { time_format: '24h', temperature_unit: 'fahrenheit', blur_booking_codes: false },
|
||||||
});
|
});
|
||||||
server.use(
|
server.use(
|
||||||
http.get('/api/weather/detailed', () =>
|
http.get('/api/weather/detailed', () =>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { getLocaleForLanguage, useTranslation } from '../../i18n'
|
|||||||
import type { Day, Place, Category, Reservation, AssignmentsMap } from '../../types'
|
import type { Day, Place, Category, Reservation, AssignmentsMap } from '../../types'
|
||||||
import { isDayInAccommodationRange } from '../../utils/dayOrder'
|
import { isDayInAccommodationRange } from '../../utils/dayOrder'
|
||||||
import { splitReservationDateTime } from '../../utils/formatters'
|
import { splitReservationDateTime } from '../../utils/formatters'
|
||||||
import { convertDistance } from '../../utils/units'
|
|
||||||
import { useDayDetail } from './useDayDetail'
|
import { useDayDetail } from './useDayDetail'
|
||||||
|
|
||||||
const WEATHER_ICON_MAP = {
|
const WEATHER_ICON_MAP = {
|
||||||
@@ -69,7 +68,6 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
|||||||
const tripObj = useTripStore((s) => s.trip)
|
const tripObj = useTripStore((s) => s.trip)
|
||||||
const canEditDays = can('day_edit', tripObj)
|
const canEditDays = can('day_edit', tripObj)
|
||||||
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
|
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
|
||||||
const distanceUnit = useSettingsStore(s => s.settings.distance_unit) || 'metric'
|
|
||||||
const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
|
const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
|
||||||
const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes)
|
const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes)
|
||||||
const fmtTime = (v) => {
|
const fmtTime = (v) => {
|
||||||
@@ -78,8 +76,6 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
|||||||
return formatTime12(v, is12h)
|
return formatTime12(v, is12h)
|
||||||
}
|
}
|
||||||
const unit = isFahrenheit ? '°F' : '°C'
|
const unit = isFahrenheit ? '°F' : '°C'
|
||||||
const formatWindSpeed = (kmh: number) =>
|
|
||||||
distanceUnit === 'imperial' ? `${Math.round(convertDistance(kmh, distanceUnit))} mph` : `${Math.round(kmh)} km/h`
|
|
||||||
const collapsed = collapsedProp
|
const collapsed = collapsedProp
|
||||||
const toggleCollapse = () => onToggleCollapse?.()
|
const toggleCollapse = () => onToggleCollapse?.()
|
||||||
const {
|
const {
|
||||||
@@ -176,7 +172,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
|||||||
<Chip icon={CloudRain} value={`${weather.precipitation_sum.toFixed(1)} mm`} />
|
<Chip icon={CloudRain} value={`${weather.precipitation_sum.toFixed(1)} mm`} />
|
||||||
)}
|
)}
|
||||||
{weather.wind_max != null && (
|
{weather.wind_max != null && (
|
||||||
<Chip icon={Wind} value={formatWindSpeed(weather.wind_max)} />
|
<Chip icon={Wind} value={isFahrenheit ? `${Math.round(weather.wind_max * 0.621371)} mph` : `${Math.round(weather.wind_max)} km/h`} />
|
||||||
)}
|
)}
|
||||||
{weather.sunrise && <Chip icon={Sunrise} value={weather.sunrise} />}
|
{weather.sunrise && <Chip icon={Sunrise} value={weather.sunrise} />}
|
||||||
{weather.sunset && <Chip icon={Sunset} value={weather.sunset} />}
|
{weather.sunset && <Chip icon={Sunset} value={weather.sunset} />}
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
const [routeLegs, setRouteLegs] = useState<Record<number, RouteSegment>>({})
|
const [routeLegs, setRouteLegs] = useState<Record<number, RouteSegment>>({})
|
||||||
const [hotelLegs, setHotelLegs] = useState<{ top?: { seg: RouteSegment; name: string }; bottom?: { seg: RouteSegment; name: string } }>({})
|
const [hotelLegs, setHotelLegs] = useState<{ top?: { seg: RouteSegment; name: string }; bottom?: { seg: RouteSegment; name: string } }>({})
|
||||||
const optimizeFromAccommodation = useSettingsStore(s => s.settings.optimize_from_accommodation)
|
const optimizeFromAccommodation = useSettingsStore(s => s.settings.optimize_from_accommodation)
|
||||||
|
// Recompute the hotel/route legs when the user flips km↔mi so the connector
|
||||||
|
// distances refresh instead of showing stale cached text (#1300).
|
||||||
|
const distanceUnit = useSettingsStore(s => s.settings.distance_unit)
|
||||||
const legsAbortRef = useRef<AbortController | null>(null)
|
const legsAbortRef = useRef<AbortController | null>(null)
|
||||||
const [draggingId, setDraggingId] = useState(null)
|
const [draggingId, setDraggingId] = useState(null)
|
||||||
const [lockedIds, setLockedIds] = useState(new Set())
|
const [lockedIds, setLockedIds] = useState(new Set())
|
||||||
@@ -470,7 +473,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
|
|
||||||
if (!controller.signal.aborted) { setRouteLegs(map); setHotelLegs(hotel) }
|
if (!controller.signal.aborted) { setRouteLegs(map); setHotelLegs(hotel) }
|
||||||
})()
|
})()
|
||||||
}, [selectedDayId, routeShown, routeProfile, mergedItemsMap, accommodations, days, optimizeFromAccommodation])
|
}, [selectedDayId, routeShown, routeProfile, mergedItemsMap, accommodations, days, optimizeFromAccommodation, distanceUnit])
|
||||||
|
|
||||||
const openAddNote = (dayId, e) => {
|
const openAddNote = (dayId, e) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useToast } from '../shared/Toast'
|
|||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
import type { Place, Category, Day, Assignment, Reservation, TripFile, AssignmentsMap } from '../../types'
|
import type { Place, Category, Day, Assignment, Reservation, TripFile, AssignmentsMap } from '../../types'
|
||||||
import { splitReservationDateTime, formatTime } from '../../utils/formatters'
|
import { splitReservationDateTime, formatTime } from '../../utils/formatters'
|
||||||
import { formatDistance } from '../../utils/units'
|
import { formatDistance, formatElevation } from '../../utils/units'
|
||||||
|
|
||||||
const detailsCache = new Map()
|
const detailsCache = new Map()
|
||||||
|
|
||||||
@@ -784,14 +784,14 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
|
|||||||
<>
|
<>
|
||||||
<div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
|
<div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
|
||||||
<Mountain size={12} color="#22c55e" />
|
<Mountain size={12} color="#22c55e" />
|
||||||
{Math.round(maxEle)} m
|
{formatElevation(maxEle, distanceUnit)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
|
<div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
|
||||||
<Mountain size={12} color="#ef4444" />
|
<Mountain size={12} color="#ef4444" />
|
||||||
{Math.round(minEle)} m
|
{formatElevation(minEle, distanceUnit)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-content-muted" style={{ fontSize: 12 }}>
|
<div className="text-content-muted" style={{ fontSize: 12 }}>
|
||||||
↑{Math.round(totalUp)} m ↓{Math.round(totalDown)} m
|
↑{formatElevation(totalUp, distanceUnit)} ↓{formatElevation(totalDown, distanceUnit)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
|||||||
|
|
||||||
{/* Distance */}
|
{/* Distance */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2 text-content-secondary">Distance</label>
|
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.distance')}</label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{([
|
{([
|
||||||
{ value: 'metric', label: 'km Metric' },
|
{ value: 'metric', label: 'km Metric' },
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
// Draw the day's accommodation bookend legs (hotel → first stop, last stop →
|
// Draw the day's accommodation bookend legs (hotel → first stop, last stop →
|
||||||
// hotel) unless the user turned the setting off — same gate as the sidebar.
|
// hotel) unless the user turned the setting off — same gate as the sidebar.
|
||||||
const optimizeFromAccommodation = useSettingsStore((s) => s.settings.optimize_from_accommodation)
|
const optimizeFromAccommodation = useSettingsStore((s) => s.settings.optimize_from_accommodation)
|
||||||
|
// Recompute when the user flips km↔mi so leg distances (formatted at compute time)
|
||||||
|
// refresh instead of showing stale cached text (#1300).
|
||||||
|
const distanceUnit = useSettingsStore((s) => s.settings.distance_unit)
|
||||||
|
|
||||||
const updateRouteForDay = useCallback(async (dayId: number | null) => {
|
const updateRouteForDay = useCallback(async (dayId: number | null) => {
|
||||||
if (routeAbortRef.current) routeAbortRef.current.abort()
|
if (routeAbortRef.current) routeAbortRef.current.abort()
|
||||||
@@ -164,7 +167,7 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
// Aborted (day changed) — newer call owns the state. Anything else: keep straight lines.
|
// Aborted (day changed) — newer call owns the state. Anything else: keep straight lines.
|
||||||
if (!(err instanceof Error) || err.name !== 'AbortError') setRouteSegments([])
|
if (!(err instanceof Error) || err.name !== 'AbortError') setRouteSegments([])
|
||||||
}
|
}
|
||||||
}, [enabled, profile, accommodations, optimizeFromAccommodation])
|
}, [enabled, profile, accommodations, optimizeFromAccommodation, distanceUnit])
|
||||||
|
|
||||||
// Stable signature for transport reservations on the selected day — changes when a transport
|
// Stable signature for transport reservations on the selected day — changes when a transport
|
||||||
// is added, removed, or repositioned, ensuring route recalc fires even on transport-only reorders.
|
// is added, removed, or repositioned, ensuring route recalc fires even on transport-only reorders.
|
||||||
@@ -188,7 +191,7 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
if (!selectedDayId) { setRoute(null); setRouteSegments([]); return }
|
if (!selectedDayId) { setRoute(null); setRouteSegments([]); return }
|
||||||
updateRouteForDay(selectedDayId)
|
updateRouteForDay(selectedDayId)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedDayId, selectedDayAssignments, transportSignature, enabled, profile, accommodations, optimizeFromAccommodation])
|
}, [selectedDayId, selectedDayAssignments, transportSignature, enabled, profile, accommodations, optimizeFromAccommodation, distanceUnit])
|
||||||
|
|
||||||
return { route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay }
|
return { route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,12 +361,13 @@ function BoardingPassHero({ trip, bundle, locale, onOpen, onEdit, onCopy, onArch
|
|||||||
// ── Atlas / stats row ────────────────────────────────────────────────────────
|
// ── Atlas / stats row ────────────────────────────────────────────────────────
|
||||||
function formatCompactDistance(value: number): string {
|
function formatCompactDistance(value: number): string {
|
||||||
const safeValue = Number.isFinite(value) ? Math.max(0, value) : 0
|
const safeValue = Number.isFinite(value) ? Math.max(0, value) : 0
|
||||||
|
// String() keeps a '.' decimal regardless of locale (no "1,5k" in non-English UIs).
|
||||||
if (safeValue >= 1000) {
|
if (safeValue >= 1000) {
|
||||||
return `${(safeValue / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}k`
|
return `${String(Math.round(safeValue / 100) / 10)}k`
|
||||||
}
|
}
|
||||||
const rounded = Math.round(safeValue * 10) / 10
|
const rounded = Math.round(safeValue * 10) / 10
|
||||||
if (safeValue > 0 && rounded === 0) return '<0.1'
|
if (safeValue > 0 && rounded === 0) return '<0.1'
|
||||||
return rounded.toLocaleString(undefined, { maximumFractionDigits: 1 })
|
return String(rounded)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElement {
|
function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElement {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { convertDistance, formatDistance, getDistanceUnitLabel } from './units'
|
||||||
|
|
||||||
|
describe('units', () => {
|
||||||
|
describe('getDistanceUnitLabel', () => {
|
||||||
|
it('returns km for metric and mi for imperial', () => {
|
||||||
|
expect(getDistanceUnitLabel('metric')).toBe('km')
|
||||||
|
expect(getDistanceUnitLabel('imperial')).toBe('mi')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('convertDistance', () => {
|
||||||
|
it('keeps kilometres for metric', () => {
|
||||||
|
expect(convertDistance(10, 'metric')).toBe(10)
|
||||||
|
})
|
||||||
|
it('converts kilometres to miles for imperial', () => {
|
||||||
|
expect(convertDistance(10, 'imperial')).toBeCloseTo(6.21371, 4)
|
||||||
|
})
|
||||||
|
it('clamps negative and non-finite input to 0', () => {
|
||||||
|
expect(convertDistance(-5, 'imperial')).toBe(0)
|
||||||
|
expect(convertDistance(NaN, 'metric')).toBe(0)
|
||||||
|
expect(convertDistance(Infinity, 'metric')).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatDistance', () => {
|
||||||
|
it('shows metres below 1 km for metric', () => {
|
||||||
|
expect(formatDistance(0.3, 'metric')).toBe('300 m')
|
||||||
|
expect(formatDistance(0.05, 'metric')).toBe('50 m')
|
||||||
|
})
|
||||||
|
it('shows kilometres at or above 1 km for metric', () => {
|
||||||
|
expect(formatDistance(1.5, 'metric')).toBe('1.5 km')
|
||||||
|
expect(formatDistance(10, 'metric')).toBe('10 km')
|
||||||
|
})
|
||||||
|
it('shows miles for imperial', () => {
|
||||||
|
expect(formatDistance(10, 'imperial')).toBe('6.2 mi')
|
||||||
|
})
|
||||||
|
it('shows <0.1 for a tiny imperial distance', () => {
|
||||||
|
expect(formatDistance(0.05, 'imperial')).toBe('<0.1 mi')
|
||||||
|
})
|
||||||
|
it('clamps negative and non-finite input to 0', () => {
|
||||||
|
expect(formatDistance(-1, 'metric')).toBe('0 m')
|
||||||
|
expect(formatDistance(NaN, 'imperial')).toBe('0 mi')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,22 +1,35 @@
|
|||||||
import type { DistanceUnit } from '../types'
|
import type { DistanceUnit } from '../types'
|
||||||
|
|
||||||
const KM_TO_MI = 0.621371
|
const KM_TO_MI = 0.621371
|
||||||
|
const M_TO_FT = 3.28084
|
||||||
|
|
||||||
export function getDistanceUnitLabel(unit: DistanceUnit): 'km' | 'mi' {
|
export function getDistanceUnitLabel(unit: DistanceUnit): 'km' | 'mi' {
|
||||||
return unit === 'imperial' ? 'mi' : 'km'
|
return unit === 'imperial' ? 'mi' : 'km'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Formats an elevation in metres as feet for imperial, so it doesn't mix with mi distances. */
|
||||||
|
export function formatElevation(meters: number, unit: DistanceUnit): string {
|
||||||
|
const safe = Number.isFinite(meters) ? meters : 0
|
||||||
|
return unit === 'imperial' ? `${Math.round(safe * M_TO_FT)} ft` : `${Math.round(safe)} m`
|
||||||
|
}
|
||||||
|
|
||||||
export function convertDistance(km: number, unit: DistanceUnit): number {
|
export function convertDistance(km: number, unit: DistanceUnit): number {
|
||||||
const safeKm = Number.isFinite(km) ? Math.max(0, km) : 0
|
const safeKm = Number.isFinite(km) ? Math.max(0, km) : 0
|
||||||
return unit === 'imperial' ? safeKm * KM_TO_MI : safeKm
|
return unit === 'imperial' ? safeKm * KM_TO_MI : safeKm
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDistance(km: number, unit: DistanceUnit): string {
|
export function formatDistance(km: number, unit: DistanceUnit): string {
|
||||||
const value = convertDistance(km, unit)
|
const safeKm = Number.isFinite(km) ? Math.max(0, km) : 0
|
||||||
|
// Metric keeps a metres reading below 1 km (e.g. "300 m"), matching the route
|
||||||
|
// connectors; imperial has no sub-mile unit, so short hops just show "0.x mi".
|
||||||
|
if (unit === 'metric' && safeKm < 1) {
|
||||||
|
return `${Math.round(safeKm * 1000)} m`
|
||||||
|
}
|
||||||
|
const value = convertDistance(safeKm, unit)
|
||||||
const label = getDistanceUnitLabel(unit)
|
const label = getDistanceUnitLabel(unit)
|
||||||
const rounded = Math.round(value * 10) / 10
|
const rounded = Math.round(value * 10) / 10
|
||||||
const text = value > 0 && rounded === 0
|
// String() keeps a '.' decimal regardless of locale, matching the rest of the app
|
||||||
? '<0.1'
|
// (toFixed elsewhere) and avoiding "1,5 km" in non-English environments.
|
||||||
: rounded.toLocaleString(undefined, { maximumFractionDigits: 1 })
|
const text = value > 0 && rounded === 0 ? '<0.1' : String(rounded)
|
||||||
return `${text} ${label}`
|
return `${text} ${label}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const MASKED_SETTING_KEYS = new Set(['webhook_url', 'ntfy_token']);
|
|||||||
|
|
||||||
export const DEFAULTABLE_USER_SETTING_KEYS = [
|
export const DEFAULTABLE_USER_SETTING_KEYS = [
|
||||||
'temperature_unit',
|
'temperature_unit',
|
||||||
|
'distance_unit',
|
||||||
'dark_mode',
|
'dark_mode',
|
||||||
'time_format',
|
'time_format',
|
||||||
// Instance-wide default currency for Costs (new users inherit it until they
|
// Instance-wide default currency for Costs (new users inherit it until they
|
||||||
@@ -28,6 +29,7 @@ type DefaultableKey = typeof DEFAULTABLE_USER_SETTING_KEYS[number];
|
|||||||
|
|
||||||
const VALID_VALUES: Partial<Record<DefaultableKey, unknown[]>> = {
|
const VALID_VALUES: Partial<Record<DefaultableKey, unknown[]>> = {
|
||||||
temperature_unit: ['fahrenheit', 'celsius'],
|
temperature_unit: ['fahrenheit', 'celsius'],
|
||||||
|
distance_unit: ['metric', 'imperial'],
|
||||||
time_format: ['12h', '24h'],
|
time_format: ['12h', '24h'],
|
||||||
dark_mode: [true, false, 'light', 'dark', 'auto'],
|
dark_mode: [true, false, 'light', 'dark', 'auto'],
|
||||||
map_provider: ['leaflet', 'mapbox-gl'],
|
map_provider: ['leaflet', 'mapbox-gl'],
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'تلقائي',
|
'settings.auto': 'تلقائي',
|
||||||
'settings.language': 'اللغة',
|
'settings.language': 'اللغة',
|
||||||
'settings.temperature': 'وحدة الحرارة',
|
'settings.temperature': 'وحدة الحرارة',
|
||||||
|
'settings.distance': 'وحدة المسافة',
|
||||||
'settings.timeFormat': 'تنسيق الوقت',
|
'settings.timeFormat': 'تنسيق الوقت',
|
||||||
'settings.bookingLabels': 'تسميات مسارات الحجوزات',
|
'settings.bookingLabels': 'تسميات مسارات الحجوزات',
|
||||||
'settings.bookingLabelsHint': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
|
'settings.bookingLabelsHint': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automático',
|
'settings.auto': 'Automático',
|
||||||
'settings.language': 'Idioma',
|
'settings.language': 'Idioma',
|
||||||
'settings.temperature': 'Unidade de temperatura',
|
'settings.temperature': 'Unidade de temperatura',
|
||||||
|
'settings.distance': 'Unidade de distância',
|
||||||
'settings.timeFormat': 'Formato de hora',
|
'settings.timeFormat': 'Formato de hora',
|
||||||
'settings.blurBookingCodes': 'Ocultar códigos de reserva',
|
'settings.blurBookingCodes': 'Ocultar códigos de reserva',
|
||||||
'settings.optimizeFromAccommodation': 'Otimizar rota a partir da hospedagem',
|
'settings.optimizeFromAccommodation': 'Otimizar rota a partir da hospedagem',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automatické',
|
'settings.auto': 'Automatické',
|
||||||
'settings.language': 'Jazyk',
|
'settings.language': 'Jazyk',
|
||||||
'settings.temperature': 'Jednotky teploty',
|
'settings.temperature': 'Jednotky teploty',
|
||||||
|
'settings.distance': 'Jednotky vzdálenosti',
|
||||||
'settings.timeFormat': 'Formát času',
|
'settings.timeFormat': 'Formát času',
|
||||||
'settings.blurBookingCodes': 'Skrýt rezervační kódy',
|
'settings.blurBookingCodes': 'Skrýt rezervační kódy',
|
||||||
'settings.optimizeFromAccommodation': 'Optimalizovat trasu od ubytování',
|
'settings.optimizeFromAccommodation': 'Optimalizovat trasu od ubytování',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automatisch',
|
'settings.auto': 'Automatisch',
|
||||||
'settings.language': 'Sprache',
|
'settings.language': 'Sprache',
|
||||||
'settings.temperature': 'Temperatureinheit',
|
'settings.temperature': 'Temperatureinheit',
|
||||||
|
'settings.distance': 'Entfernungseinheit',
|
||||||
'settings.timeFormat': 'Zeitformat',
|
'settings.timeFormat': 'Zeitformat',
|
||||||
'settings.bookingLabels': 'Orts-Labels auf Buchungsrouten',
|
'settings.bookingLabels': 'Orts-Labels auf Buchungsrouten',
|
||||||
'settings.bookingLabelsHint': 'Zeigt Bahnhofs-/Flughafennamen auf der Karte. Wenn aus, wird nur das Icon angezeigt.',
|
'settings.bookingLabelsHint': 'Zeigt Bahnhofs-/Flughafennamen auf der Karte. Wenn aus, wird nur das Icon angezeigt.',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Auto',
|
'settings.auto': 'Auto',
|
||||||
'settings.language': 'Language',
|
'settings.language': 'Language',
|
||||||
'settings.temperature': 'Temperature Unit',
|
'settings.temperature': 'Temperature Unit',
|
||||||
|
'settings.distance': 'Distance Unit',
|
||||||
'settings.timeFormat': 'Time Format',
|
'settings.timeFormat': 'Time Format',
|
||||||
'settings.bookingLabels': 'Booking route labels',
|
'settings.bookingLabels': 'Booking route labels',
|
||||||
'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.',
|
'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.',
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automático',
|
'settings.auto': 'Automático',
|
||||||
'settings.language': 'Idioma',
|
'settings.language': 'Idioma',
|
||||||
'settings.temperature': 'Unidad de temperatura',
|
'settings.temperature': 'Unidad de temperatura',
|
||||||
|
'settings.distance': 'Unidad de distancia',
|
||||||
'settings.timeFormat': 'Formato de hora',
|
'settings.timeFormat': 'Formato de hora',
|
||||||
'settings.blurBookingCodes': 'Difuminar códigos de reserva',
|
'settings.blurBookingCodes': 'Difuminar códigos de reserva',
|
||||||
'settings.optimizeFromAccommodation': 'Optimizar la ruta desde el alojamiento',
|
'settings.optimizeFromAccommodation': 'Optimizar la ruta desde el alojamiento',
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Auto',
|
'settings.auto': 'Auto',
|
||||||
'settings.language': 'Langue',
|
'settings.language': 'Langue',
|
||||||
'settings.temperature': 'Unité de température',
|
'settings.temperature': 'Unité de température',
|
||||||
|
'settings.distance': 'Unité de distance',
|
||||||
'settings.timeFormat': "Format de l'heure",
|
'settings.timeFormat': "Format de l'heure",
|
||||||
'settings.blurBookingCodes': 'Masquer les codes de réservation',
|
'settings.blurBookingCodes': 'Masquer les codes de réservation',
|
||||||
'settings.optimizeFromAccommodation': "Optimiser l'itinéraire depuis l'hébergement",
|
'settings.optimizeFromAccommodation': "Optimiser l'itinéraire depuis l'hébergement",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Αυτόματο',
|
'settings.auto': 'Αυτόματο',
|
||||||
'settings.language': 'Γλώσσα',
|
'settings.language': 'Γλώσσα',
|
||||||
'settings.temperature': 'Μονάδα Θερμοκρασίας',
|
'settings.temperature': 'Μονάδα Θερμοκρασίας',
|
||||||
|
'settings.distance': 'Μονάδα Απόστασης',
|
||||||
'settings.timeFormat': 'Μορφή Ώρας',
|
'settings.timeFormat': 'Μορφή Ώρας',
|
||||||
'settings.bookingLabels': 'Ετικέτες διαδρομής κρατήσεων',
|
'settings.bookingLabels': 'Ετικέτες διαδρομής κρατήσεων',
|
||||||
'settings.bookingLabelsHint':
|
'settings.bookingLabelsHint':
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automatikus',
|
'settings.auto': 'Automatikus',
|
||||||
'settings.language': 'Nyelv',
|
'settings.language': 'Nyelv',
|
||||||
'settings.temperature': 'Hőmérséklet egység',
|
'settings.temperature': 'Hőmérséklet egység',
|
||||||
|
'settings.distance': 'Távolság egység',
|
||||||
'settings.timeFormat': 'Időformátum',
|
'settings.timeFormat': 'Időformátum',
|
||||||
'settings.blurBookingCodes': 'Foglalási kódok elrejtése',
|
'settings.blurBookingCodes': 'Foglalási kódok elrejtése',
|
||||||
'settings.optimizeFromAccommodation': 'Útvonal optimalizálása a szállástól',
|
'settings.optimizeFromAccommodation': 'Útvonal optimalizálása a szállástól',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Otomatis',
|
'settings.auto': 'Otomatis',
|
||||||
'settings.language': 'Bahasa',
|
'settings.language': 'Bahasa',
|
||||||
'settings.temperature': 'Satuan Suhu',
|
'settings.temperature': 'Satuan Suhu',
|
||||||
|
'settings.distance': 'Satuan Jarak',
|
||||||
'settings.timeFormat': 'Format Waktu',
|
'settings.timeFormat': 'Format Waktu',
|
||||||
'settings.blurBookingCodes': 'Sembunyikan Kode Pemesanan',
|
'settings.blurBookingCodes': 'Sembunyikan Kode Pemesanan',
|
||||||
'settings.optimizeFromAccommodation': 'Optimalkan rute dari akomodasi',
|
'settings.optimizeFromAccommodation': 'Optimalkan rute dari akomodasi',
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automatica',
|
'settings.auto': 'Automatica',
|
||||||
'settings.language': 'Lingua',
|
'settings.language': 'Lingua',
|
||||||
'settings.temperature': 'Unità di Temperatura',
|
'settings.temperature': 'Unità di Temperatura',
|
||||||
|
'settings.distance': 'Unità di Distanza',
|
||||||
'settings.timeFormat': 'Formato Ora',
|
'settings.timeFormat': 'Formato Ora',
|
||||||
'settings.blurBookingCodes': 'Nascondi codici di prenotazione',
|
'settings.blurBookingCodes': 'Nascondi codici di prenotazione',
|
||||||
'settings.optimizeFromAccommodation': "Ottimizza il percorso dall'alloggio",
|
'settings.optimizeFromAccommodation': "Ottimizza il percorso dall'alloggio",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': '自動',
|
'settings.auto': '自動',
|
||||||
'settings.language': '言語',
|
'settings.language': '言語',
|
||||||
'settings.temperature': '温度単位',
|
'settings.temperature': '温度単位',
|
||||||
|
'settings.distance': '距離単位',
|
||||||
'settings.timeFormat': '時刻形式',
|
'settings.timeFormat': '時刻形式',
|
||||||
'settings.bookingLabels': '予約ルートのラベル',
|
'settings.bookingLabels': '予約ルートのラベル',
|
||||||
'settings.bookingLabelsHint': '地図に駅・空港名を表示。オフ時はアイコンのみ。',
|
'settings.bookingLabelsHint': '地図に駅・空港名を表示。オフ時はアイコンのみ。',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': '자동',
|
'settings.auto': '자동',
|
||||||
'settings.language': '언어',
|
'settings.language': '언어',
|
||||||
'settings.temperature': '온도 단위',
|
'settings.temperature': '온도 단위',
|
||||||
|
'settings.distance': '거리 단위',
|
||||||
'settings.timeFormat': '시간 형식',
|
'settings.timeFormat': '시간 형식',
|
||||||
'settings.bookingLabels': '예약 경로 레이블',
|
'settings.bookingLabels': '예약 경로 레이블',
|
||||||
'settings.bookingLabelsHint': '지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.',
|
'settings.bookingLabelsHint': '지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automatisch',
|
'settings.auto': 'Automatisch',
|
||||||
'settings.language': 'Taal',
|
'settings.language': 'Taal',
|
||||||
'settings.temperature': 'Temperatuureenheid',
|
'settings.temperature': 'Temperatuureenheid',
|
||||||
|
'settings.distance': 'Afstandseenheid',
|
||||||
'settings.timeFormat': 'Tijdnotatie',
|
'settings.timeFormat': 'Tijdnotatie',
|
||||||
'settings.blurBookingCodes': 'Boekingscodes vervagen',
|
'settings.blurBookingCodes': 'Boekingscodes vervagen',
|
||||||
'settings.optimizeFromAccommodation': 'Route optimaliseren vanaf accommodatie',
|
'settings.optimizeFromAccommodation': 'Route optimaliseren vanaf accommodatie',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Automatyczny',
|
'settings.auto': 'Automatyczny',
|
||||||
'settings.language': 'Język',
|
'settings.language': 'Język',
|
||||||
'settings.temperature': 'Jednostka temperatury',
|
'settings.temperature': 'Jednostka temperatury',
|
||||||
|
'settings.distance': 'Jednostka odległości',
|
||||||
'settings.timeFormat': 'Format czasu',
|
'settings.timeFormat': 'Format czasu',
|
||||||
'settings.blurBookingCodes': 'Rozmyj kody rezerwacji',
|
'settings.blurBookingCodes': 'Rozmyj kody rezerwacji',
|
||||||
'settings.optimizeFromAccommodation': 'Optymalizuj trasę od zakwaterowania',
|
'settings.optimizeFromAccommodation': 'Optymalizuj trasę od zakwaterowania',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Авто',
|
'settings.auto': 'Авто',
|
||||||
'settings.language': 'Язык',
|
'settings.language': 'Язык',
|
||||||
'settings.temperature': 'Единица температуры',
|
'settings.temperature': 'Единица температуры',
|
||||||
|
'settings.distance': 'Единица расстояния',
|
||||||
'settings.timeFormat': 'Формат времени',
|
'settings.timeFormat': 'Формат времени',
|
||||||
'settings.blurBookingCodes': 'Скрыть коды бронирования',
|
'settings.blurBookingCodes': 'Скрыть коды бронирования',
|
||||||
'settings.optimizeFromAccommodation': 'Оптимизировать маршрут от места проживания',
|
'settings.optimizeFromAccommodation': 'Оптимизировать маршрут от места проживания',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Otomatik',
|
'settings.auto': 'Otomatik',
|
||||||
'settings.language': 'Dil',
|
'settings.language': 'Dil',
|
||||||
'settings.temperature': 'Sıcaklık Birimi',
|
'settings.temperature': 'Sıcaklık Birimi',
|
||||||
|
'settings.distance': 'Mesafe Birimi',
|
||||||
'settings.timeFormat': 'Saat Biçimi',
|
'settings.timeFormat': 'Saat Biçimi',
|
||||||
'settings.bookingLabels': 'Rezervasyon rota etiketleri',
|
'settings.bookingLabels': 'Rezervasyon rota etiketleri',
|
||||||
'settings.bookingLabelsHint': 'Haritada istasyon / havalimanı adlarını göster. Kapalıyken yalnızca simge görünür.',
|
'settings.bookingLabelsHint': 'Haritada istasyon / havalimanı adlarını göster. Kapalıyken yalnızca simge görünür.',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': 'Авто',
|
'settings.auto': 'Авто',
|
||||||
'settings.language': 'Мова',
|
'settings.language': 'Мова',
|
||||||
'settings.temperature': 'Одиниця температури',
|
'settings.temperature': 'Одиниця температури',
|
||||||
|
'settings.distance': 'Одиниця відстані',
|
||||||
'settings.timeFormat': 'Формат часу',
|
'settings.timeFormat': 'Формат часу',
|
||||||
'settings.blurBookingCodes': 'Приховати коди бронювання',
|
'settings.blurBookingCodes': 'Приховати коди бронювання',
|
||||||
'settings.optimizeFromAccommodation': 'Оптимізувати маршрут від житла',
|
'settings.optimizeFromAccommodation': 'Оптимізувати маршрут від житла',
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': '自動',
|
'settings.auto': '自動',
|
||||||
'settings.language': '語言',
|
'settings.language': '語言',
|
||||||
'settings.temperature': '溫度單位',
|
'settings.temperature': '溫度單位',
|
||||||
|
'settings.distance': '距離單位',
|
||||||
'settings.timeFormat': '時間格式',
|
'settings.timeFormat': '時間格式',
|
||||||
'settings.blurBookingCodes': '模糊預訂程式碼',
|
'settings.blurBookingCodes': '模糊預訂程式碼',
|
||||||
'settings.optimizeFromAccommodation': '從住宿地點最佳化路線',
|
'settings.optimizeFromAccommodation': '從住宿地點最佳化路線',
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const settings: TranslationStrings = {
|
|||||||
'settings.auto': '自动',
|
'settings.auto': '自动',
|
||||||
'settings.language': '语言',
|
'settings.language': '语言',
|
||||||
'settings.temperature': '温度单位',
|
'settings.temperature': '温度单位',
|
||||||
|
'settings.distance': '距离单位',
|
||||||
'settings.timeFormat': '时间格式',
|
'settings.timeFormat': '时间格式',
|
||||||
'settings.blurBookingCodes': '模糊预订代码',
|
'settings.blurBookingCodes': '模糊预订代码',
|
||||||
'settings.optimizeFromAccommodation': '从住宿地优化路线',
|
'settings.optimizeFromAccommodation': '从住宿地优化路线',
|
||||||
|
|||||||
Reference in New Issue
Block a user