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:
Maurice
2026-06-26 17:45:56 +02:00
committed by Maurice
parent 94dca8cad7
commit 3d65bb0c12
32 changed files with 109 additions and 23 deletions
@@ -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' },
+3 -1
View File
@@ -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 &nbsp;{Math.round(totalDown)} m {formatElevation(totalUp, distanceUnit)} &nbsp;{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' },
+5 -2
View File
@@ -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 }
} }
+3 -2
View File
@@ -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 {
+46
View File
@@ -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')
})
})
})
+17 -4
View File
@@ -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}`
} }
+2
View File
@@ -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'],
+1
View File
@@ -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': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
+1
View File
@@ -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',
+1
View File
@@ -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í',
+1
View File
@@ -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.',
+1
View File
@@ -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.',
+1
View File
@@ -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',
+1
View File
@@ -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",
+1
View File
@@ -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':
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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",
+1
View File
@@ -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': '地図に駅・空港名を表示。オフ時はアイコンのみ。',
+1
View File
@@ -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': '지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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': 'Оптимизировать маршрут от места проживания',
+1
View File
@@ -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.',
+1
View File
@@ -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': 'Оптимізувати маршрут від житла',
+1
View File
@@ -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': '從住宿地點最佳化路線',
+1
View File
@@ -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': '从住宿地优化路线',