diff --git a/client/src/components/Map/ReservationOverlay.tsx b/client/src/components/Map/ReservationOverlay.tsx index 8db225f8..295dd7c6 100644 --- a/client/src/components/Map/ReservationOverlay.tsx +++ b/client/src/components/Map/ReservationOverlay.tsx @@ -158,6 +158,7 @@ interface TransportItem { res: Reservation from: ReservationEndpoint to: ReservationEndpoint + waypoints: ReservationEndpoint[] type: TransportType arcs: [number, number][][] primaryArc: [number, number][] @@ -353,15 +354,29 @@ export default function ReservationOverlay({ reservations, showConnections, show const out: TransportItem[] = [] for (const r of reservations) { if (!TRANSPORT_TYPES.includes(r.type as TransportType)) continue - const eps = r.endpoints || [] - const from = eps.find(e => e.role === 'from') - const to = eps.find(e => e.role === 'to') - if (!from || !to) continue + // Ordered waypoints (from · stops · to). A single-leg booking has exactly two, + // so the arc + markers below are byte-identical to before for it. + const waypoints = (r.endpoints || []) + .filter(e => e.role === 'from' || e.role === 'to' || e.role === 'stop') + .slice() + .sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)) + if (waypoints.length < 2) continue + const from = waypoints[0] + const to = waypoints[waypoints.length - 1] const type = r.type as TransportType const isGeo = TYPE_META[type].geodesic - const arcs = isGeo - ? splitAntimeridian(greatCircle([from.lat, from.lng], [to.lat, to.lng])) - : [[[from.lat, from.lng], [to.lat, to.lng]] as [number, number][]] + // One arc per leg (between consecutive waypoints), concatenated. + const arcs: [number, number][][] = [] + let distanceKm = 0 + for (let i = 0; i < waypoints.length - 1; i++) { + const a = waypoints[i] + const b = waypoints[i + 1] + const segArcs = isGeo + ? splitAntimeridian(greatCircle([a.lat, a.lng], [b.lat, b.lng])) + : [[[a.lat, a.lng], [b.lat, b.lng]] as [number, number][]] + arcs.push(...segArcs) + distanceKm += haversineKm([a.lat, a.lng], [b.lat, b.lng]) + } const primaryIdx = arcs.reduce((best, seg, idx, all) => seg.length > all[best].length ? idx : best, 0) const primaryArc = arcs[primaryIdx] ?? [] const fallback: [number, number] = primaryArc.length > 0 @@ -369,12 +384,15 @@ export default function ReservationOverlay({ reservations, showConnections, show : [(from.lat + to.lat) / 2, (from.lng + to.lng) / 2] const duration = computeDuration(from, to, r.reservation_time || null, r.reservation_end_time || null) - const distance = `${Math.round(haversineKm([from.lat, from.lng], [to.lat, to.lng]))} km` - const mainLabel = from.code && to.code ? `${from.code} → ${to.code}` : null + const distance = `${Math.round(distanceKm)} km` + // Show the full route (FRA → BER → HND) when every waypoint has a code. + const mainLabel = waypoints.every(w => w.code) + ? waypoints.map(w => w.code).join(' → ') + : (from.code && to.code ? `${from.code} → ${to.code}` : null) const subParts = [duration, distance].filter(Boolean) as string[] const subLabel = subParts.length > 0 ? subParts.join(' · ') : null - out.push({ res: r, from, to, type, arcs, primaryArc, fallback, mainLabel, subLabel }) + out.push({ res: r, from, to, waypoints, type, arcs, primaryArc, fallback, mainLabel, subLabel }) } return out }, [reservations]) @@ -416,38 +434,21 @@ export default function ReservationOverlay({ reservations, showConnections, show /> )))} - {visibleItems.flatMap(item => [ + {visibleItems.flatMap(item => item.waypoints.map((wp, wi) => ( onEndpointClick?.(item.res.id) }} > -
{item.from.name}
+
{wp.name}
{item.res.title &&
{item.res.title}
}
-
, - onEndpointClick?.(item.res.id) }} - > - -
{item.to.name}
- {item.res.title &&
{item.res.title}
} -
-
, - ])} - - {showStats && visibleItems.map(item => item.type === 'flight' && (item.mainLabel || item.subLabel) && labelVisibleIds.has(item.res.id) && ( - - ))} + + )))} ) } diff --git a/client/src/components/Map/reservationsMapbox.ts b/client/src/components/Map/reservationsMapbox.ts index 2c912b91..1722690c 100644 --- a/client/src/components/Map/reservationsMapbox.ts +++ b/client/src/components/Map/reservationsMapbox.ts @@ -126,6 +126,7 @@ interface TransportItem { res: Reservation from: ReservationEndpoint to: ReservationEndpoint + waypoints: ReservationEndpoint[] type: TransportType arcs: [number, number][][] primaryArc: [number, number][] @@ -137,23 +138,38 @@ function buildItems(reservations: Reservation[]): TransportItem[] { const out: TransportItem[] = [] for (const r of reservations) { if (!TRANSPORT_TYPES.includes(r.type as TransportType)) continue - const eps = r.endpoints || [] - const from = eps.find(e => e.role === 'from') - const to = eps.find(e => e.role === 'to') - if (!from || !to) continue + // Ordered waypoints (from · stops · to); a single-leg booking has exactly two. + const waypoints = (r.endpoints || []) + .filter(e => e.role === 'from' || e.role === 'to' || e.role === 'stop') + .slice() + .sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)) + if (waypoints.length < 2) continue + const from = waypoints[0] + const to = waypoints[waypoints.length - 1] const type = r.type as TransportType const isGeo = TYPE_META[type].geodesic - const arcs = isGeo - ? splitAntimeridian(greatCircle([from.lat, from.lng], [to.lat, to.lng])) - : [[[from.lat, from.lng], [to.lat, to.lng]] as [number, number][]] + // One arc per leg (between consecutive waypoints), concatenated. + const arcs: [number, number][][] = [] + let distanceKm = 0 + for (let i = 0; i < waypoints.length - 1; i++) { + const a = waypoints[i] + const b = waypoints[i + 1] + const segArcs = isGeo + ? splitAntimeridian(greatCircle([a.lat, a.lng], [b.lat, b.lng])) + : [[[a.lat, a.lng], [b.lat, b.lng]] as [number, number][]] + arcs.push(...segArcs) + distanceKm += haversineKm([a.lat, a.lng], [b.lat, b.lng]) + } const primaryIdx = arcs.reduce((best, seg, idx, all) => seg.length > all[best].length ? idx : best, 0) const primaryArc = arcs[primaryIdx] ?? [] const duration = computeDuration(from, to, r.reservation_time || null, r.reservation_end_time || null) - const distance = `${Math.round(haversineKm([from.lat, from.lng], [to.lat, to.lng]))} km` - const mainLabel = from.code && to.code ? `${from.code} → ${to.code}` : null + const distance = `${Math.round(distanceKm)} km` + const mainLabel = waypoints.every(w => w.code) + ? waypoints.map(w => w.code).join(' → ') + : (from.code && to.code ? `${from.code} → ${to.code}` : null) const subParts = [duration, distance].filter(Boolean) as string[] const subLabel = subParts.length > 0 ? subParts.join(' · ') : null - out.push({ res: r, from, to, type, arcs, primaryArc, mainLabel, subLabel }) + out.push({ res: r, from, to, waypoints, type, arcs, primaryArc, mainLabel, subLabel }) } return out } @@ -321,7 +337,7 @@ export class ReservationMapboxOverlay { if (show) { for (const item of visibleItems) { const showLabel = this.opts.showEndpointLabels && labelVisibleIds.has(item.res.id) - for (const ep of [item.from, item.to]) { + for (const ep of item.waypoints) { const label = showLabel ? (ep.code || cleanName(ep.name)) : null const el = document.createElement('div') el.innerHTML = endpointMarkerHtml(item.type, label) @@ -342,29 +358,10 @@ export class ReservationMapboxOverlay { } } - // ── stats label (flights only) ────────────────────────────────── + // Stats badge removed — the floating route/duration label on the arc is no + // longer drawn; only the connection line and the airport markers remain. this.statsMarkers.forEach(s => s.marker.remove()) this.statsMarkers = [] - if (show && this.opts.showStats) { - for (const item of visibleItems) { - if (item.type !== 'flight') continue - if (!labelVisibleIds.has(item.res.id)) continue - if (!item.mainLabel && !item.subLabel) continue - const arc = item.primaryArc - if (arc.length < 2) continue - const mid = arc[Math.floor(arc.length / 2)]! - const { html, width, height } = buildStatsHtml(item.mainLabel, item.subLabel) - const el = document.createElement('div') - el.style.cssText = `width:${width}px;height:${height}px;pointer-events:none;` - el.innerHTML = html - const marker = new mapboxgl.Marker({ element: el, anchor: 'center' }) - .setLngLat([mid[1], mid[0]]) - .addTo(map) - this.statsMarkers.push({ marker, arc }) - } - } - // Prime rotation once so labels don't flash horizontal on first paint. - this.updateStatsRotation() } // Match the Leaflet overlay's "rotate the label along the arc" look. diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 4e493011..ccc95d9a 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -215,7 +215,13 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const icon = reservationIconSvg(r.type) const color = RESERVATION_COLOR_MAP[r.type] || '#3b82f6' let subtitle = '' - if (r.type === 'flight') subtitle = [meta.airline, meta.flight_number, meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : ''].filter(Boolean).join(' · ') + if (r.type === 'flight') { + // Full route over all waypoints (FRA → BER → HND), falling back to the + // flat metadata pair for legacy single-leg flights without endpoints. + const stops = (r.endpoints || []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)).map(e => e.code || e.name) + const route = stops.length >= 2 ? stops.join(' → ') : (meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : '') + subtitle = [meta.airline, meta.flight_number, route].filter(Boolean).join(' · ') + } else if (r.type === 'train') subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Seat ${meta.seat}` : ''].filter(Boolean).join(' · ') else if (r.type === 'restaurant') subtitle = [meta.party_size ? `${meta.party_size} guests` : ''].filter(Boolean).join(' · ') else if (r.type === 'event') subtitle = [meta.venue].filter(Boolean).join(' · ') diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 342ffdba..8f04e638 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -171,7 +171,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { const [timeConfirm, setTimeConfirm] = useState<{ dayId: number; fromId: number; time: string; // For drag & drop reorder - fromType?: string; toType?: string; toId?: number; insertAfter?: boolean; + fromType?: string; toType?: string; toId?: number; insertAfter?: boolean; toLegIndex?: number | null; // For arrow reorder reorderIds?: number[]; } | null>(null) @@ -471,6 +471,9 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { const assignmentIds: number[] = [] const noteUpdates: { id: number; sort_order: number }[] = [] const transportUpdates: { id: number; day_plan_position: number }[] = [] + // Multi-leg flight legs share a reservation id, so their positions can't live in + // the single per-booking slot — collect them per leg, keyed reservationId → legIndex → pos. + const legPosUpdates: Record> = {} let placeCount = 0 let i = 0 @@ -491,7 +494,10 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { group.forEach((g, idx) => { const pos = base + (idx + 1) / (group.length + 1) if (g.type === 'note') noteUpdates.push({ id: g.data.id, sort_order: pos }) - else if (g.type === 'transport') transportUpdates.push({ id: g.data.id, day_plan_position: pos }) + else if (g.type === 'transport') { + if (g.data.__leg) ((legPosUpdates[g.data.id] ??= {})[g.data.__leg.index] = pos) + else transportUpdates.push({ id: g.data.id, day_plan_position: pos }) + } }) } } @@ -510,6 +516,30 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { })) setTransportPosVersion(v => v + 1) } + // Per-leg positions of multi-leg flights live in metadata.legs[i].day_positions + // (the single per-booking slot can't hold one position per leg). + const legResIds = Object.keys(legPosUpdates) + if (legResIds.length) { + for (const ridStr of legResIds) { + const rid = Number(ridStr) + const r = useTripStore.getState().reservations.find(x => x.id === rid) + if (!r) continue + let parsed: any = {} + try { parsed = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) } catch { parsed = {} } + if (!Array.isArray(parsed.legs)) continue + const legs = parsed.legs.map((leg: any, i: number) => { + const pos = legPosUpdates[rid][i] + return pos == null ? leg : { ...leg, day_positions: { ...(leg.day_positions || {}), [dayId]: pos } } + }) + // Send metadata as an OBJECT (like the form does) — passing a JSON string + // here double-encodes it on the server, which wipes metadata.legs on read + // and collapses the flight back to a single span. + const newMeta = { ...parsed, legs } + useTripStore.setState(state => ({ reservations: state.reservations.map(x => (x.id === rid ? { ...x, metadata: newMeta } : x)) })) + await tripActions.updateReservation(tripId, rid, { metadata: newMeta }) + } + setTransportPosVersion(v => v + 1) + } if (assignmentIds.length) await onReorder(dayId, assignmentIds) if (transportUpdates.length) { onRouteRefresh?.() @@ -528,8 +558,11 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) } } - const handleMergedDrop = async (dayId, fromType, fromId, toType, toId, insertAfter = false) => { + const handleMergedDrop = async (dayId, fromType, fromId, toType, toId, insertAfter = false, toLegIndex = null) => { const m = getMergedItems(dayId) + // Multi-leg flights expose one item per leg sharing the same reservation id; + // disambiguate the drop target by leg index so you can drop BETWEEN legs. + const matchTo = (i: any) => i.type === toType && i.data.id === toId && (toLegIndex == null || i.data?.__leg?.index === toLegIndex) // Check if a timed place is being moved → would it break chronological order? if (fromType === 'place') { @@ -537,11 +570,11 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { const fromMinutes = parseTimeToMinutes(fromItem?.data?.place?.place_time) if (fromItem && fromMinutes !== null) { const fromIdx = m.findIndex(i => i.type === fromType && i.data.id === fromId) - const toIdx = m.findIndex(i => i.type === toType && i.data.id === toId) + const toIdx = m.findIndex(matchTo) if (fromIdx !== -1 && toIdx !== -1) { const simulated = [...m] const [moved] = simulated.splice(fromIdx, 1) - let insertIdx = simulated.findIndex(i => i.type === toType && i.data.id === toId) + let insertIdx = simulated.findIndex(matchTo) if (insertIdx === -1) insertIdx = simulated.length if (insertAfter) insertIdx += 1 simulated.splice(insertIdx, 0, moved) @@ -558,7 +591,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { if (!isChronological) { const placeTime = fromItem.data.place.place_time const timeStr = placeTime.includes(':') ? placeTime.substring(0, 5) : placeTime - setTimeConfirm({ dayId, fromType, fromId, toType, toId, insertAfter, time: timeStr }) + setTimeConfirm({ dayId, fromType, fromId, toType, toId, insertAfter, toLegIndex, time: timeStr }) setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null return } @@ -568,7 +601,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { // Build new order: remove the dragged item, insert at target position const fromIdx = m.findIndex(i => i.type === fromType && i.data.id === fromId) - const toIdx = m.findIndex(i => i.type === toType && i.data.id === toId) + const toIdx = m.findIndex(matchTo) if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) { setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null return @@ -576,7 +609,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { const newOrder = [...m] const [moved] = newOrder.splice(fromIdx, 1) - let adjustedTo = newOrder.findIndex(i => i.type === toType && i.data.id === toId) + let adjustedTo = newOrder.findIndex(matchTo) if (adjustedTo === -1) adjustedTo = newOrder.length if (insertAfter) adjustedTo += 1 newOrder.splice(adjustedTo, 0, moved) @@ -590,7 +623,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { const confirmTimeRemoval = async () => { if (!timeConfirm) return const saved = { ...timeConfirm } - const { dayId, fromId, reorderIds, fromType, toType, toId, insertAfter } = saved + const { dayId, fromId, reorderIds, fromType, toType, toId, insertAfter, toLegIndex } = saved setTimeConfirm(null) // Remove time from assignment @@ -633,13 +666,14 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { // Drag & drop reorder if (fromType && toType) { + const matchTo = (i: any) => i.type === toType && i.data.id === toId && (toLegIndex == null || i.data?.__leg?.index === toLegIndex) const fromIdx = m.findIndex(i => i.type === fromType && i.data.id === fromId) - const toIdx = m.findIndex(i => i.type === toType && i.data.id === toId) + const toIdx = m.findIndex(matchTo) if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return const newOrder = [...m] const [moved] = newOrder.splice(fromIdx, 1) - let adjustedTo = newOrder.findIndex(i => i.type === toType && i.data.id === toId) + let adjustedTo = newOrder.findIndex(matchTo) if (adjustedTo === -1) adjustedTo = newOrder.length if (insertAfter) adjustedTo += 1 newOrder.splice(adjustedTo, 0, moved) @@ -1311,6 +1345,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const isAfter = dropTargetRef.current.startsWith('transport-after-') const parts = dropTargetRef.current.replace('transport-after-', '').replace('transport-', '').split('-') const transportId = Number(parts[0]) + const legPart = parts.find(p => /^leg\d+$/.test(p)) + const toLegIndex = legPart ? Number(legPart.slice(3)) : null if (placeId) { onAssignToDay?.(parseInt(placeId), day.id) @@ -1318,15 +1354,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const r = reservations.find(x => x.id === Number(fromReservationId)) if (r) { const update = computeMultiDayMove(r, day.id, phase); tripActions.updateReservation(tripId, r.id, update).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } } else if (fromReservationId) { - handleMergedDrop(day.id, 'transport', Number(fromReservationId), 'transport', transportId, isAfter) + handleMergedDrop(day.id, 'transport', Number(fromReservationId), 'transport', transportId, isAfter, toLegIndex) } else if (assignmentId && fromDayId !== day.id) { tripActions.moveAssignment(tripId, Number(assignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } else if (assignmentId) { - handleMergedDrop(day.id, 'place', Number(assignmentId), 'transport', transportId, isAfter) + handleMergedDrop(day.id, 'place', Number(assignmentId), 'transport', transportId, isAfter, toLegIndex) } else if (noteId && fromDayId !== day.id) { tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } else if (noteId) { - handleMergedDrop(day.id, 'note', Number(noteId), 'transport', transportId, isAfter) + handleMergedDrop(day.id, 'note', Number(noteId), 'transport', transportId, isAfter, toLegIndex) } setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; window.__dragData = null return @@ -1372,9 +1408,10 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP ) : ( merged.map((item, idx) => { - const itemKey = item.type === 'transport' ? `transport-${item.data.id}-${day.id}` : (item.type === 'place' ? `place-${item.data.id}` : `note-${item.data.id}`) + const legSuffix = item.data?.__leg ? `-leg${item.data.__leg.index}` : '' + const itemKey = item.type === 'transport' ? `transport-${item.data.id}${legSuffix}-${day.id}` : (item.type === 'place' ? `place-${item.data.id}` : `note-${item.data.id}`) const showDropLine = (!!draggingId || !!dropTargetKey) && dropTargetKey === itemKey - const showDropLineAfter = item.type === 'transport' && (!!draggingId || !!dropTargetKey) && dropTargetKey === `transport-after-${item.data.id}-${day.id}` + const showDropLineAfter = item.type === 'transport' && (!!draggingId || !!dropTargetKey) && dropTargetKey === `transport-after-${item.data.id}${legSuffix}-${day.id}` if (item.type === 'place') { const assignment = item.data @@ -1722,7 +1759,13 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP // Subtitle aus Metadaten zusammensetzen let subtitle = '' - if (res.type === 'flight') { + if (res.__leg) { + // One leg of a multi-leg flight — show this segment's own route. + const parts = [res.__leg.airline, res.__leg.flight_number].filter(Boolean) + if (res.__leg.from || res.__leg.to) + parts.push([res.__leg.from, res.__leg.to].filter(Boolean).join(' → ')) + subtitle = parts.join(' · ') + } else if (res.type === 'flight') { const parts = [meta.airline, meta.flight_number].filter(Boolean) if (meta.departure_airport || meta.arrival_airport) parts.push([meta.departure_airport, meta.arrival_airport].filter(Boolean).join(' → ')) @@ -1731,28 +1774,32 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Sitz ${meta.seat}` : ''].filter(Boolean).join(' · ') } - // Multi-day span phase - const spanLabel = getSpanLabel(res, spanPhase) + // Multi-day span phase (single-leg / non-flight only — a + // multi-leg flight is shown as one row per leg, see below). + const spanLabel = res.__leg ? null : getSpanLabel(res, spanPhase) const displayTime = getDisplayTimeForDay(res, day.id) + const legKey = res.__leg ? `leg${res.__leg.index}` : 'x' return ( - +
{ if (!canEditDays) return - if (TRANSPORT_TYPES.has(res.type)) onEditTransport?.(res) - else onEditReservation?.(res) + const target = reservations.find(x => x.id === res.id) ?? res + if (TRANSPORT_TYPES.has(res.type)) onEditTransport?.(target) + else onEditReservation?.(target) }} onDragOver={e => { e.preventDefault(); e.stopPropagation() const rect = e.currentTarget.getBoundingClientRect() const inBottom = e.clientY > rect.top + rect.height / 2 - const key = inBottom ? `transport-after-${res.id}-${day.id}` : `transport-${res.id}-${day.id}` + const ls = res.__leg ? `-leg${res.__leg.index}` : '' + const key = inBottom ? `transport-after-${res.id}${ls}-${day.id}` : `transport-${res.id}${ls}-${day.id}` if (dropTargetRef.current !== key) setDropTargetKey(key) }} - draggable={canEditDays && spanPhase !== 'middle'} + draggable={canEditDays && spanPhase !== 'middle' && !res.__leg} onDragStart={e => { - if (!canEditDays || spanPhase === 'middle') { e.preventDefault(); return } + if (!canEditDays || spanPhase === 'middle' || res.__leg) { e.preventDefault(); return } // setData is required for the drag to start reliably (Firefox) and // matches how place/note items initiate their drag. e.dataTransfer.setData('reservationId', String(res.id)) @@ -1773,15 +1820,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const r2 = reservations.find(x => x.id === Number(fromReservationId)) if (r2) { const update = computeMultiDayMove(r2, day.id, phase); tripActions.updateReservation(tripId, r2.id, update).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } } else if (fromReservationId) { - handleMergedDrop(day.id, 'transport', Number(fromReservationId), 'transport', res.id, insertAfter) + handleMergedDrop(day.id, 'transport', Number(fromReservationId), 'transport', res.id, insertAfter, res.__leg?.index ?? null) } else if (fromAssignmentId && fromDayId !== day.id) { tripActions.moveAssignment(tripId, Number(fromAssignmentId), fromDayId, day.id).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } else if (fromAssignmentId) { - handleMergedDrop(day.id, 'place', Number(fromAssignmentId), 'transport', res.id, insertAfter) + handleMergedDrop(day.id, 'place', Number(fromAssignmentId), 'transport', res.id, insertAfter, res.__leg?.index ?? null) } else if (noteId && fromDayId !== day.id) { tripActions.moveDayNote(tripId, fromDayId, day.id, Number(noteId)).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } else if (noteId) { - handleMergedDrop(day.id, 'note', Number(noteId), 'transport', res.id, insertAfter) + handleMergedDrop(day.id, 'note', Number(noteId), 'transport', res.id, insertAfter, res.__leg?.index ?? null) } setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; window.__dragData = null }} @@ -1801,7 +1848,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP opacity: draggingId === res.id ? 0.4 : spanPhase === 'middle' ? 0.65 : 1, }} > - {canEditDays && spanPhase !== 'middle' && ( + {canEditDays && spanPhase !== 'middle' && !res.__leg && (
@@ -1846,7 +1893,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
)} - {onToggleConnection && (res.endpoints || []).length >= 2 && (() => { + {onToggleConnection && (!res.__leg || res.__leg.index === 0) && (res.endpoints || []).length >= 2 && (() => { const active = visibleConnectionIds.includes(res.id) return ( + )} + + {!isFirst && ( +
+
+ + updateWp({ arrDayId: v })} placeholder={t('dayplan.dayN', { n: '?' })} options={dayOptions} size="sm" /> +
+
+ + updateWp({ arrTime: v })} /> +
+ {wp.airport && ( +
+ +
{wp.airport.tz}
+
+ )} +
+ )} + {!isLast && ( + <> +
+
+ + updateWp({ depDayId: v })} placeholder={t('dayplan.dayN', { n: '?' })} options={dayOptions} size="sm" /> +
+
+ + updateWp({ depTime: v })} /> +
+ {wp.airport && ( +
+ +
{wp.airport.tz}
+
+ )} +
+
+
+ + updateWp({ airline: e.target.value })} placeholder="Lufthansa" className={inputClass} /> +
+
+ + updateWp({ flight_number: e.target.value })} placeholder="LH 123" className={inputClass} /> +
+
+ + )} + + {!isLast && ( + + )} + + ) + })} -
- - {form.type === 'flight' ? ( - setToPick({ airport: a || undefined })} /> - ) : ( - setToPick({ location: l || undefined })} /> - )} -
- - - {/* Departure row */} -
-
- - set('start_day_id', value)} - placeholder={t('dayplan.dayN', { n: '?' })} - options={dayOptions} - size="sm" - /> -
-
- - set('departure_time', v)} /> -
- {form.type === 'flight' && fromPick.airport && ( -
- -
- {fromPick.airport.tz} + ) : ( + <> + {/* From / To endpoints (non-flight) */} +
+
+ + setFromPick({ location: l || undefined })} /> +
+
+ + setToPick({ location: l || undefined })} />
- )} -
- {/* Arrival row */} -
-
- - set('end_day_id', value)} - placeholder={t('dayplan.dayN', { n: '?' })} - options={dayOptions} - size="sm" - /> -
-
- - set('arrival_time', v)} /> -
- {form.type === 'flight' && toPick.airport && ( -
- -
- {toPick.airport.tz} + {/* Departure row */} +
+
+ + set('start_day_id', value)} placeholder={t('dayplan.dayN', { n: '?' })} options={dayOptions} size="sm" /> +
+
+ + set('departure_time', v)} />
- )} -
- {/* Flight-specific fields */} - {form.type === 'flight' && ( -
-
- - set('meta_airline', e.target.value)} - placeholder="Lufthansa" className={inputClass} /> + {/* Arrival row */} +
+
+ + set('end_day_id', value)} placeholder={t('dayplan.dayN', { n: '?' })} options={dayOptions} size="sm" /> +
+
+ + set('arrival_time', v)} /> +
-
- - set('meta_flight_number', e.target.value)} - placeholder="LH 123" className={inputClass} /> -
-
+ )} {/* Train-specific fields */} diff --git a/client/src/utils/dayMerge.test.ts b/client/src/utils/dayMerge.test.ts index bc1b5d74..3e53f9c0 100644 --- a/client/src/utils/dayMerge.test.ts +++ b/client/src/utils/dayMerge.test.ts @@ -126,18 +126,18 @@ describe('getMergedItems', () => { expect(types).toEqual(['place', 'transport', 'place']) }) - it('per-day position overrides time-based insertion', () => { + it('orders a timed transport chronologically regardless of a stale per-day position', () => { const dayAssignments = [ { id: 1, order_index: 0, place: { place_time: '08:00' } }, { id: 2, order_index: 1, place: { place_time: '13:00' } }, ] - // Transport at 10:30 would normally go between the two places - // but per-day position 1.5 puts it after the second place + // The train is at 10:30, so it sorts between the 08:00 and 13:00 places by time — + // timed items are arranged chronologically even if an old manual position exists. const dayTransports = [ { id: 20, type: 'train', day_id: 5, end_day_id: 5, reservation_time: '10:30', day_positions: { 5: 1.5 } }, ] const result = getMergedItems({ dayAssignments, dayNotes: [], dayTransports, dayId: 5 }) const types = result.map(i => i.type) - expect(types).toEqual(['place', 'place', 'transport']) + expect(types).toEqual(['place', 'transport', 'place']) }) }) diff --git a/client/src/utils/dayMerge.ts b/client/src/utils/dayMerge.ts index e664772f..cabd9bbd 100644 --- a/client/src/utils/dayMerge.ts +++ b/client/src/utils/dayMerge.ts @@ -39,12 +39,66 @@ export function getDisplayTimeForDay( return r.reservation_time || null } +/** Per-leg detail of a multi-leg flight, or null for single-leg / non-flight. */ +function parseFlightLegs(r: any): any[] | null { + if (r?.type !== 'flight') return null + let meta = r.metadata + if (typeof meta === 'string') { try { meta = JSON.parse(meta || '{}') } catch { meta = {} } } + // Defensive: recover metadata that was accidentally double-encoded by an earlier + // bug (a JSON string of a JSON string) so already-saved flights heal on read. + if (typeof meta === 'string') { try { meta = JSON.parse(meta || '{}') } catch { meta = {} } } + if (meta && Array.isArray(meta.legs) && meta.legs.length > 1) return meta.legs + return null +} + +/** + * Expand a multi-leg flight into one synthetic reservation per leg that touches + * `dayId`, each with its own day span + departure/arrival time so it slots into + * the timeline independently. A single-leg flight (or any other reservation) is + * returned untouched, so existing behaviour is unchanged. + */ +export function expandFlightLegsForDay( + r: any, + dayId: number, + getDayOrder: (id: number) => number, + days: Array<{ id: number; date?: string | null }> +): any[] { + const legs = parseFlightLegs(r) + if (!legs) return [r] + const dateOf = (id: number | null): string | null => (id == null ? null : (days.find(d => d.id === id)?.date ?? null)) + const thisOrder = getDayOrder(dayId) + const out: any[] = [] + legs.forEach((leg, i) => { + const dep = leg.dep_day_id ?? r.day_id ?? null + const arr = leg.arr_day_id ?? dep + if (dep == null) return + const depOrder = getDayOrder(dep) + const arrOrder = getDayOrder(arr ?? dep) + if (!(thisOrder >= depOrder && thisOrder <= arrOrder)) return + const depDate = dateOf(dep) + const arrDate = dateOf(arr ?? dep) + out.push({ + ...r, + day_id: dep, + end_day_id: arr ?? dep, + reservation_time: leg.dep_time ? (depDate ? `${depDate}T${leg.dep_time}` : leg.dep_time) : null, + reservation_end_time: leg.arr_time ? (arrDate ? `${arrDate}T${leg.arr_time}` : leg.arr_time) : null, + // Each leg carries its OWN saved position (not the booking's) so items can be + // dropped between legs and persist; absent → falls back to time ordering. + day_positions: leg.day_positions || undefined, + day_plan_position: undefined, + __leg: { index: i, total: legs.length, from: leg.from ?? null, to: leg.to ?? null, airline: leg.airline ?? null, flight_number: leg.flight_number ?? null }, + }) + }) + return out +} + /** Filter reservations that are active transports for the given day, excluding assignment-linked ones. */ export function getTransportForDay(opts: { reservations: any[] dayId: number dayAssignmentIds: number[] - days: Array<{ id: number; day_number?: number }> + days: Array<{ id: number; day_number?: number; date?: string | null }> }): any[] { const { reservations, dayId, dayAssignmentIds, days } = opts @@ -69,7 +123,34 @@ export function getTransportForDay(opts: { return thisDayOrder >= startOrder && thisDayOrder <= endOrder } return startDayId === dayId - }) + }).flatMap(r => expandFlightLegsForDay(r, dayId, getDayOrder, days)) +} + +/** + * Order items chronologically: anything with a time (a place's place_time, a + * transport/leg display time, a timed note) sorts by that time. An item WITHOUT a + * time inherits the time of the timed item before it, so untimed items stay where + * they were manually placed. Stable on the incoming order for ties. + */ +function applyChronoOrder( + items: MergedItem[], + dayId: number, + getDisplayTime: (r: any, dayId: number) => string | null +): MergedItem[] { + const timeOf = (it: MergedItem): number | null => { + if (it.type === 'place') return parseTimeToMinutes(it.data?.place?.place_time) + if (it.type === 'note') return parseTimeToMinutes(it.data?.time) + return parseTimeToMinutes(getDisplayTime(it.data, dayId)) + } + let last = -Infinity + return items + .map((it, i) => { + const t = timeOf(it) + if (t != null) last = t + return { it, i, eff: t != null ? t : last } + }) + .sort((a, b) => a.eff - b.eff || a.i - b.i) + .map(k => k.it) } /** Merge places, notes, and transports into a single ordered day timeline. */ @@ -94,9 +175,9 @@ export function getMergedItems(opts: { minutes: parseTimeToMinutes(getDisplayTime(r, dayId)) ?? 0, })).sort((a, b) => a.minutes - b.minutes) - if (timedTransports.length === 0) return baseItems + if (timedTransports.length === 0) return applyChronoOrder(baseItems, dayId, getDisplayTime) if (baseItems.length === 0) { - return timedTransports.map((item, i) => ({ type: item.type, sortKey: i, data: item.data })) + return applyChronoOrder(timedTransports.map((item, i) => ({ type: item.type, sortKey: i, data: item.data })), dayId, getDisplayTime) } // Insert transports among base items based on per-day position or time @@ -132,5 +213,5 @@ export function getMergedItems(opts: { result.push({ type: timed.type, sortKey, data: timed.data }) } - return result.sort((a, b) => a.sortKey - b.sortKey) + return applyChronoOrder(result.sort((a, b) => a.sortKey - b.sortKey), dayId, getDisplayTime) } diff --git a/client/src/utils/flightLegs.ts b/client/src/utils/flightLegs.ts new file mode 100644 index 00000000..c9504eb5 --- /dev/null +++ b/client/src/utils/flightLegs.ts @@ -0,0 +1,105 @@ +// Multi-leg (layover) flight support. +// +// A flight booking is ONE reservation whose route is an ordered chain of airports +// (e.g. FRA -> BER -> HND). The geometry + order are the source of truth in +// `reservation.endpoints` (role 'from' for the first airport, 'stop' for each +// intermediate one, 'to' for the last, ordered by `sequence`). The per-leg detail +// — airline, flight number, and each segment's own day/time — lives in +// `metadata.legs`. The top-level metadata (`departure_airport`/`arrival_airport`/ +// `airline`/`flight_number`) and `day_id`/`end_day_id` mirror the FIRST and LAST +// leg so legacy readers keep working. +// +// A legacy single-leg flight (two endpoints, flat metadata, no `metadata.legs`) +// is normalised here into a one-leg chain, so every renderer can use one path. + +import type { Reservation, ReservationEndpoint } from '../types' + +export interface FlightLeg { + from: string | null // IATA code (or null) + to: string | null + airline?: string + flight_number?: string + dep_day_id?: number | null + dep_time?: string | null // 'HH:mm' + arr_day_id?: number | null + arr_time?: string | null +} + +/** reservation.metadata may be a JSON string or an already-parsed object. */ +export function parseReservationMetadata(r: Pick): Record { + const m = r.metadata + if (!m) return {} + if (typeof m === 'string') { + try { + let parsed = JSON.parse(m || '{}') + // Defensive: an earlier bug could double-encode metadata (a JSON string of a + // JSON string) — unwrap it once more so saved flights heal on read. + if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed) } catch { /* keep */ } } + return (parsed && typeof parsed === 'object') ? parsed : {} + } catch { return {} } + } + return m as Record +} + +/** Endpoints ordered by `sequence` (geometry + order source of truth). */ +export function orderedEndpoints(r: Pick): ReservationEndpoint[] { + return (r.endpoints || []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)) +} + +/** + * Ordered legs of a flight. `metadata.legs` is preferred; otherwise a single leg + * is derived from the endpoints (and finally the flat metadata) so that legacy + * single-leg flights — and flights created before this feature — still work. + */ +export function getFlightLegs(r: Reservation): FlightLeg[] { + const meta = parseReservationMetadata(r) + if (Array.isArray(meta.legs) && meta.legs.length > 0) { + return meta.legs.map((l: any): FlightLeg => ({ + from: l.from ?? null, + to: l.to ?? null, + airline: l.airline || undefined, + flight_number: l.flight_number || undefined, + dep_day_id: l.dep_day_id ?? null, + dep_time: l.dep_time ?? null, + arr_day_id: l.arr_day_id ?? null, + arr_time: l.arr_time ?? null, + })) + } + // Legacy fallback: one leg from the endpoints / flat metadata. + const eps = orderedEndpoints(r) + const first = eps[0] + const last = eps[eps.length - 1] + const fromCode = first?.code ?? meta.departure_airport ?? null + const toCode = last?.code ?? meta.arrival_airport ?? null + if (!fromCode && !toCode) return [] + return [{ + from: fromCode, + to: toCode, + airline: meta.airline || undefined, + flight_number: meta.flight_number || undefined, + dep_day_id: r.day_id ?? null, + dep_time: first?.local_time ?? null, + arr_day_id: r.end_day_id ?? r.day_id ?? null, + arr_time: last?.local_time ?? null, + }] +} + +/** Number of flight segments. 1 for a simple from -> to booking. */ +export function legCount(r: Reservation): number { + return getFlightLegs(r).length +} + +export function isMultiLegFlight(r: Reservation): boolean { + return r.type === 'flight' && legCount(r) > 1 +} + +/** + * Ordered route labels (IATA codes, or names when no code) for display, e.g. + * ['FRA','BER','HND']. Uses endpoints; falls back to the flat metadata pair. + */ +export function routeStops(r: Reservation): string[] { + const eps = orderedEndpoints(r) + if (eps.length >= 2) return eps.map(e => e.code || e.name) + const meta = parseReservationMetadata(r) + return [meta.departure_airport, meta.arrival_airport].filter(Boolean) as string[] +} diff --git a/server/src/nest/booking-import/kitinerary-mapper.ts b/server/src/nest/booking-import/kitinerary-mapper.ts index a26993a6..c0ada0e4 100644 --- a/server/src/nest/booking-import/kitinerary-mapper.ts +++ b/server/src/nest/booking-import/kitinerary-mapper.ts @@ -90,6 +90,90 @@ function mapFlight(r: KiReservation, source: ParsedBookingItem['source']): Parse }; } +/** True when flight `b` is a short layover connection that continues flight `a`. */ +function sameConnection(a: KiReservation, b: KiReservation): boolean { + const fa = a.reservationFor as KiFlight | undefined; + const fb = b.reservationFor as KiFlight | undefined; + if (!fa || !fb) return false; + const arrIata = fa.arrivalAirport?.iataCode?.toUpperCase(); + const depIata = fb.departureAirport?.iataCode?.toUpperCase(); + if (!arrIata || !depIata || arrIata !== depIata) return false; // must connect at the same airport + const arrIso = toIsoString(fa.arrivalTime); + const depIso = toIsoString(fb.departureTime); + if (arrIso && depIso) { + const gapMs = new Date(depIso).getTime() - new Date(arrIso).getTime(); + // A real layover is forward in time and short — anything longer (e.g. a + // round-trip return days later) stays a separate booking. + if (gapMs < 0 || gapMs > 24 * 3600 * 1000) return false; + } + return true; +} + +/** Collapse several connecting flight legs (same PNR) into one multi-leg booking. */ +function mapFlightGroup(legs: KiReservation[], source: ParsedBookingItem['source']): ParsedBookingItem | null { + const flights = legs.map(l => l.reservationFor as KiFlight | undefined); + if (flights.some(f => !f)) return mapFlight(legs[0], source); // malformed → fall back to single + const fs = flights as KiFlight[]; + + const iataOf = (ap: KiFlight['departureAirport']) => ap?.iataCode?.toUpperCase() ?? null; + const makeEndpoint = ( + ap: KiFlight['departureAirport'], role: 'from' | 'stop' | 'to', time: string | null, date: string | null, + ): ParsedEndpoint | null => { + const iata = iataOf(ap); + const found = iata ? findByIata(iata) : null; + const label = found ? (found.city ? `${found.city} (${found.iata})` : found.name) : (ap?.name ?? iata ?? 'Unknown'); + if (found) return { role, sequence: 0, name: label, code: found.iata, lat: found.lat, lng: found.lng, timezone: found.tz, local_time: time, local_date: date }; + const c = coords(ap?.geo); + if (c) return { role, sequence: 0, name: label, code: iata, lat: c.lat, lng: c.lng, timezone: null, local_time: time, local_date: date }; + return null; + }; + + const endpoints: ParsedEndpoint[] = []; + const metaLegs: Record[] = []; + const first = fs[0]; + const firstDep = splitIso(first.departureTime); + const originEp = makeEndpoint(first.departureAirport, 'from', firstDep.time, firstDep.date); + if (originEp) endpoints.push(originEp); + + fs.forEach((f, i) => { + const isLast = i === fs.length - 1; + const arr = splitIso(f.arrivalTime); + const arrEp = makeEndpoint(f.arrivalAirport, isLast ? 'to' : 'stop', arr.time, arr.date); + if (arrEp) endpoints.push(arrEp); + const airline = f.airline?.name ?? f.airline?.iataCode ?? ''; + metaLegs.push({ + from: iataOf(f.departureAirport), + to: iataOf(f.arrivalAirport), + ...(airline ? { airline } : {}), + ...(f.flightNumber ? { flight_number: f.flightNumber } : {}), + dep_time: splitIso(f.departureTime).time, + arr_time: arr.time, + }); + }); + endpoints.forEach((e, i) => { e.sequence = i; }); + + const last = fs[fs.length - 1]; + const airline = first.airline?.name ?? first.airline?.iataCode ?? ''; + const route = [iataOf(first.departureAirport), ...fs.map(f => iataOf(f.arrivalAirport))].filter(Boolean).join(' → '); + return { + type: 'flight', + title: airline ? `${airline} ${route}` : `Flight ${route}`, + reservation_time: toIsoString(first.departureTime), + reservation_end_time: toIsoString(last.arrivalTime), + confirmation_number: legs[0].reservationNumber ?? null, + metadata: { + ...(airline ? { airline } : {}), + ...(first.flightNumber ? { flight_number: first.flightNumber } : {}), + ...(iataOf(first.departureAirport) ? { departure_airport: iataOf(first.departureAirport) } : {}), + ...(iataOf(last.arrivalAirport) ? { arrival_airport: iataOf(last.arrivalAirport) } : {}), + legs: metaLegs, + }, + endpoints, + needs_review: endpoints.length < fs.length + 1, + source, + }; +} + function mapTrain(r: KiReservation, source: ParsedBookingItem['source']): ParsedBookingItem | null { const t = r.reservationFor as KiTrainTrip | undefined; if (!t) return null; @@ -233,8 +317,25 @@ export function mapReservations(kiItems: KiReservation[], fileName: string): { i const source = { fileName, index: i }; let item: ParsedBookingItem | null = null; + // Group consecutive connecting flight legs that share a PNR into one booking. + if (r['@type'] === 'FlightReservation') { + const pnr = r.reservationNumber ?? null; + const group = [r]; + while ( + i + 1 < kiItems.length && + kiItems[i + 1]['@type'] === 'FlightReservation' && + pnr != null && + (kiItems[i + 1].reservationNumber ?? null) === pnr && + sameConnection(group[group.length - 1], kiItems[i + 1]) + ) { + group.push(kiItems[++i]); + } + item = group.length > 1 ? mapFlightGroup(group, source) : mapFlight(r, source); + if (item) items.push(item); + continue; + } + switch (r['@type']) { - case 'FlightReservation': item = mapFlight(r, source); break; case 'TrainReservation': item = mapTrain(r, source); break; case 'BusReservation': item = mapBus(r, source); break; case 'BoatReservation': item = mapBoat(r, source); break; diff --git a/server/src/services/tripService.ts b/server/src/services/tripService.ts index 413cfdb4..3893bd2e 100644 --- a/server/src/services/tripService.ts +++ b/server/src/services/tripService.ts @@ -543,8 +543,14 @@ export function exportICS(tripId: string | number): { ics: string; filename: str if (r.confirmation_number) desc += `\nConfirmation: ${r.confirmation_number}`; if (meta.airline) desc += `\nAirline: ${meta.airline}`; if (meta.flight_number) desc += `\nFlight: ${meta.flight_number}`; - if (meta.departure_airport) desc += `\nFrom: ${meta.departure_airport}`; - if (meta.arrival_airport) desc += `\nTo: ${meta.arrival_airport}`; + if (Array.isArray(meta.legs) && meta.legs.length > 1) { + // Multi-leg flight: show the whole route (FRA → BER → HND) on one event. + const stops = [meta.legs[0]?.from, ...meta.legs.map((l: { to?: string }) => l.to)].filter(Boolean); + if (stops.length) desc += `\nRoute: ${stops.join(' → ')}`; + } else { + if (meta.departure_airport) desc += `\nFrom: ${meta.departure_airport}`; + if (meta.arrival_airport) desc += `\nTo: ${meta.arrival_airport}`; + } if (meta.train_number) desc += `\nTrain: ${meta.train_number}`; if (r.notes) desc += `\n${r.notes}`; if (desc) ics += `DESCRIPTION:${esc(desc)}\r\n`; diff --git a/server/tests/unit/services/kitineraryMapper.test.ts b/server/tests/unit/services/kitineraryMapper.test.ts new file mode 100644 index 00000000..35b422f2 --- /dev/null +++ b/server/tests/unit/services/kitineraryMapper.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { mapReservations } from '../../../src/nest/booking-import/kitinerary-mapper'; + +const airport = (iata: string, lat: number, lng: number) => ({ + iataCode: iata, + name: iata, + geo: { latitude: lat, longitude: lng }, +}); + +const flight = (pnr: string, dep: any, arr: any, depTime: string, arrTime: string, flightNumber: string) => ({ + '@type': 'FlightReservation', + reservationNumber: pnr, + reservationFor: { + departureAirport: dep, + arrivalAirport: arr, + departureTime: depTime, + arrivalTime: arrTime, + airline: { name: 'Lufthansa', iataCode: 'LH' }, + flightNumber, + }, +}); + +const FRA = airport('FRA', 50.04, 8.57); +const BER = airport('BER', 52.36, 13.50); +const HND = airport('HND', 35.55, 139.78); + +describe('kitinerary mapper — multi-leg flight grouping', () => { + it('groups two connecting same-PNR legs into one multi-leg booking', () => { + const { items } = mapReservations([ + flight('ABC123', FRA, BER, '2026-06-11T10:00:00', '2026-06-11T12:00:00', 'LH 100'), + flight('ABC123', BER, HND, '2026-06-11T14:30:00', '2026-06-11T23:30:00', 'LH 200'), + ] as any, 'test.json'); + + expect(items).toHaveLength(1); + const booking = items[0]; + expect(booking.type).toBe('flight'); + expect(booking.endpoints).toHaveLength(3); + expect(booking.endpoints!.map(e => e.role)).toEqual(['from', 'stop', 'to']); + expect(booking.endpoints!.map(e => e.sequence)).toEqual([0, 1, 2]); + const meta = booking.metadata as any; + expect(meta.legs).toHaveLength(2); + expect(meta.legs[0]).toMatchObject({ from: 'FRA', to: 'BER', flight_number: 'LH 100' }); + expect(meta.legs[1]).toMatchObject({ from: 'BER', to: 'HND', flight_number: 'LH 200' }); + expect(meta.departure_airport).toBe('FRA'); + expect(meta.arrival_airport).toBe('HND'); + expect(booking.reservation_time).toContain('10:00'); + expect(booking.reservation_end_time).toContain('23:30'); + }); + + it('keeps a round trip (same PNR, multi-day gap) as two separate bookings', () => { + const { items } = mapReservations([ + flight('RT999', FRA, HND, '2026-06-11T10:00:00', '2026-06-11T20:00:00', 'LH 700'), + flight('RT999', HND, FRA, '2026-06-20T10:00:00', '2026-06-20T18:00:00', 'LH 701'), + ] as any, 'test.json'); + + expect(items).toHaveLength(2); + expect((items[0].metadata as any).legs).toBeUndefined(); + expect((items[1].metadata as any).legs).toBeUndefined(); + }); + + it('leaves a single flight unchanged (two endpoints, no legs array)', () => { + const { items } = mapReservations([ + flight('S1', FRA, BER, '2026-06-11T10:00:00', '2026-06-11T12:00:00', 'LH 1'), + ] as any, 'test.json'); + + expect(items).toHaveLength(1); + expect(items[0].endpoints).toHaveLength(2); + expect((items[0].metadata as any).legs).toBeUndefined(); + }); +}); diff --git a/shared/src/i18n/ar/budget.ts b/shared/src/i18n/ar/budget.ts index 0e9a8989..4ed35eb5 100644 --- a/shared/src/i18n/ar/budget.ts +++ b/shared/src/i18n/ar/budget.ts @@ -38,78 +38,78 @@ const budget: TranslationStrings = { 'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.', 'budget.netBalances': 'الأرصدة الصافية', 'budget.categoriesLabel': 'فئات', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "أنت", + "costs.youShort": "أنت", + "costs.youLower": "أنت", + "costs.youOwe": "عليك", + "costs.youOweSub": "عليك أن تدفع للآخرين", + "costs.youreOwed": "لك", + "costs.youreOwedSub": "على الآخرين أن يدفعوا لك", + "costs.totalSpend": "إجمالي إنفاق الرحلة", + "costs.totalSpendSub": "عبر جميع المسافرين", + "costs.to": "إلى", + "costs.from": "من", + "costs.allSettled": "لقد سوّيت كل حساباتك", + "costs.nothingOwed": "لا شيء مستحق لك", + "costs.yourShare": "حصتك", + "costs.youPaid": "أنت دفعت", + "costs.expenses": "المصروفات", + "costs.entries": "{count} إدخالات", + "costs.searchPlaceholder": "ابحث في المصروفات…", + "costs.filter.all": "الكل", + "costs.filter.mine": "دفعتها أنا", + "costs.filter.owed": "مستحق لي", + "costs.addExpense": "إضافة مصروف", + "costs.editExpense": "تعديل المصروف", + "costs.noMatch": "لا توجد مصروفات تطابق بحثك.", + "costs.emptyText": "لا توجد مصروفات بعد. أضف أول مصروف لك.", + "costs.spent": "تم إنفاق {amount}", + "costs.noDate": "بدون تاريخ", + "costs.noOnePaid": "لم يدفع أحد بعد", + "costs.youLent": "أقرضت {amount}", + "costs.youBorrowed": "اقترضت {amount}", + "costs.settleUp": "تسوية الحساب", + "costs.history": "السجل", + "costs.everyoneSquare": "الجميع متعادلون", + "costs.nothingOutstanding": "لا توجد مدفوعات معلّقة الآن.", + "costs.pay": "ادفع", + "costs.pays": "يدفع", + "costs.settle": "تسوية", + "costs.balances": "الأرصدة", + "costs.byCategory": "حسب الفئة", + "costs.noCategories": "لا توجد مصروفات بعد.", + "costs.settleHistory": "سجل التسويات", + "costs.noSettlements": "لا توجد مدفوعات مسوّاة بعد.", + "costs.paymentsSettled": "تمت تسوية {count} مدفوعات", + "costs.paid": "مدفوع", + "costs.undo": "تراجع", + "costs.whatFor": "لأجل ماذا كان؟", + "costs.namePlaceholder": "مثل: عشاء، هدايا تذكارية، وقود…", + "costs.totalAmount": "المبلغ الإجمالي", + "costs.currency": "العملة", + "costs.day": "اليوم", + "costs.rateLabel": "1 {from} بـ {to}", + "costs.category": "الفئة", + "costs.whoPaid": "من دفع؟", + "costs.splitBetween": "تقسيم بالتساوي بين", + "costs.pickSomeone": "اختر شخصًا واحدًا على الأقل للتقسيم معه.", + "costs.splitSummary": "تقسيم على {count} · {amount} لكل واحد", + "costs.cat.accommodation": "الإقامة", + "costs.cat.food": "الطعام والشراب", + "costs.cat.groceries": "البقالة", + "costs.cat.transport": "النقل", + "costs.cat.flights": "الرحلات الجوية", + "costs.cat.activities": "الأنشطة", + "costs.cat.sightseeing": "معالم سياحية", + "costs.cat.shopping": "التسوق", + "costs.cat.fees": "الرسوم والتذاكر", + "costs.cat.health": "الصحة", + "costs.cat.tips": "البقشيش", + "costs.cat.other": "أخرى", + "costs.daysCount": "{count} أيام", + "costs.travelers": "{count} مسافرين", + "costs.liveRate": "سعر مباشر", + "costs.settleAll": "تسوية الكل", }; export default budget; diff --git a/shared/src/i18n/ar/reservations.ts b/shared/src/i18n/ar/reservations.ts index 484085ec..04e9f3db 100644 --- a/shared/src/i18n/ar/reservations.ts +++ b/shared/src/i18n/ar/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'رقم الرحلة', 'reservations.meta.from': 'من', 'reservations.meta.to': 'إلى', + 'reservations.layover.route': 'المسار', + 'reservations.layover.stop': 'محطة توقف', + 'reservations.layover.addStop': 'إضافة محطة توقف', + 'reservations.layover.connection': 'رحلة متّصلة', + 'reservations.layover.layover': 'توقف بيني', 'reservations.needsReview': 'مراجعة', 'reservations.needsReviewHint': 'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.', diff --git a/shared/src/i18n/br/budget.ts b/shared/src/i18n/br/budget.ts index c280e442..644ff885 100644 --- a/shared/src/i18n/br/budget.ts +++ b/shared/src/i18n/br/budget.ts @@ -39,78 +39,78 @@ const budget: TranslationStrings = { 'Clique no avatar de um membro em um item do orçamento para marcá-lo em verde — significa que ele pagou. O acerto mostra quem deve quanto a quem.', 'budget.netBalances': 'Saldos líquidos', 'budget.categoriesLabel': 'categorias', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Você", + "costs.youShort": "V", + "costs.youLower": "você", + "costs.youOwe": "Você deve", + "costs.youOweSub": "Você deve pagar os outros", + "costs.youreOwed": "Devem a você", + "costs.youreOwedSub": "Os outros devem pagar você", + "costs.totalSpend": "Gasto total da viagem", + "costs.totalSpendSub": "Entre todos os viajantes", + "costs.to": "Para", + "costs.from": "De", + "costs.allSettled": "Suas contas estão acertadas", + "costs.nothingOwed": "Ninguém deve nada a você", + "costs.yourShare": "Sua parte", + "costs.youPaid": "Você pagou", + "costs.expenses": "Despesas", + "costs.entries": "{count} lançamentos", + "costs.searchPlaceholder": "Buscar despesas…", + "costs.filter.all": "Todas", + "costs.filter.mine": "Pagas por mim", + "costs.filter.owed": "Devem a mim", + "costs.addExpense": "Adicionar despesa", + "costs.editExpense": "Editar despesa", + "costs.noMatch": "Nenhuma despesa corresponde à busca.", + "costs.emptyText": "Nenhuma despesa ainda. Adicione a primeira.", + "costs.spent": "{amount} gastos", + "costs.noDate": "Sem data", + "costs.noOnePaid": "Ninguém pagou ainda", + "costs.youLent": "você emprestou {amount}", + "costs.youBorrowed": "você pegou emprestado {amount}", + "costs.settleUp": "Acertar contas", + "costs.history": "Histórico", + "costs.everyoneSquare": "Todos quitados", + "costs.nothingOutstanding": "Nenhum pagamento pendente no momento.", + "costs.pay": "paga", + "costs.pays": "paga", + "costs.settle": "Acertar", + "costs.balances": "Saldos", + "costs.byCategory": "Por categoria", + "costs.noCategories": "Nenhuma despesa ainda.", + "costs.settleHistory": "Histórico de acertos", + "costs.noSettlements": "Nenhum pagamento acertado ainda.", + "costs.paymentsSettled": "{count} pagamentos acertados", + "costs.paid": "pago", + "costs.undo": "Desfazer", + "costs.whatFor": "Para que foi?", + "costs.namePlaceholder": "ex.: jantar, lembranças, gasolina…", + "costs.totalAmount": "Valor total", + "costs.currency": "Moeda", + "costs.day": "Dia", + "costs.rateLabel": "1 {from} em {to}", + "costs.category": "Categoria", + "costs.whoPaid": "Quem pagou?", + "costs.splitBetween": "Dividir igualmente entre", + "costs.pickSomeone": "Escolha pelo menos uma pessoa para dividir.", + "costs.splitSummary": "Dividido entre {count} · {amount} cada", + "costs.cat.accommodation": "Hospedagem", + "costs.cat.food": "Comida e bebida", + "costs.cat.groceries": "Mercado", + "costs.cat.transport": "Transporte", + "costs.cat.flights": "Voos", + "costs.cat.activities": "Atividades", + "costs.cat.sightseeing": "Passeios turísticos", + "costs.cat.shopping": "Compras", + "costs.cat.fees": "Taxas e ingressos", + "costs.cat.health": "Saúde", + "costs.cat.tips": "Gorjetas", + "costs.cat.other": "Outros", + "costs.daysCount": "{count} dias", + "costs.travelers": "{count} viajantes", + "costs.liveRate": "taxa ao vivo", + "costs.settleAll": "Acertar tudo", }; export default budget; diff --git a/shared/src/i18n/br/reservations.ts b/shared/src/i18n/br/reservations.ts index d6de7ba0..f26c3108 100644 --- a/shared/src/i18n/br/reservations.ts +++ b/shared/src/i18n/br/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Nº do voo', 'reservations.meta.from': 'De', 'reservations.meta.to': 'Para', + 'reservations.layover.route': 'Rota', + 'reservations.layover.stop': 'Parada', + 'reservations.layover.addStop': 'Adicionar parada', + 'reservations.layover.connection': 'Conexão', + 'reservations.layover.layover': 'Escala', 'reservations.needsReview': 'Verificar', 'reservations.needsReviewHint': 'Aeroporto não pôde ser identificado automaticamente — confirme o local.', diff --git a/shared/src/i18n/cs/budget.ts b/shared/src/i18n/cs/budget.ts index a7e292ea..c0a8c008 100644 --- a/shared/src/i18n/cs/budget.ts +++ b/shared/src/i18n/cs/budget.ts @@ -39,78 +39,78 @@ const budget: TranslationStrings = { 'Klikněte na avatar člena u rozpočtové položky pro zelené označení – to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.', 'budget.netBalances': 'Čisté zůstatky', 'budget.categoriesLabel': 'kategorie', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Vy", + "costs.youShort": "Vy", + "costs.youLower": "vy", + "costs.youOwe": "Dlužíte", + "costs.youOweSub": "Měli byste zaplatit ostatním", + "costs.youreOwed": "Dluží vám", + "costs.youreOwedSub": "Ostatní by měli zaplatit vám", + "costs.totalSpend": "Celkové výdaje na cestu", + "costs.totalSpendSub": "Za všechny cestovatele", + "costs.to": "Komu", + "costs.from": "Od", + "costs.allSettled": "Máte vše vyrovnáno", + "costs.nothingOwed": "Nikdo vám nic nedluží", + "costs.yourShare": "Váš podíl", + "costs.youPaid": "Zaplatili jste", + "costs.expenses": "Výdaje", + "costs.entries": "{count} položek", + "costs.searchPlaceholder": "Hledat výdaje…", + "costs.filter.all": "Vše", + "costs.filter.mine": "Zaplaceno mnou", + "costs.filter.owed": "Dluží mi", + "costs.addExpense": "Přidat výdaj", + "costs.editExpense": "Upravit výdaj", + "costs.noMatch": "Žádné výdaje neodpovídají vašemu hledání.", + "costs.emptyText": "Zatím žádné výdaje. Přidejte první.", + "costs.spent": "Utraceno {amount}", + "costs.noDate": "Bez data", + "costs.noOnePaid": "Zatím nikdo nezaplatil", + "costs.youLent": "půjčili jste {amount}", + "costs.youBorrowed": "vypůjčili jste si {amount}", + "costs.settleUp": "Vyrovnat", + "costs.history": "Historie", + "costs.everyoneSquare": "Všichni jsou vyrovnáni", + "costs.nothingOutstanding": "Momentálně žádné nevyrovnané platby.", + "costs.pay": "zaplatí", + "costs.pays": "zaplatí", + "costs.settle": "Vyrovnat", + "costs.balances": "Zůstatky", + "costs.byCategory": "Podle kategorie", + "costs.noCategories": "Zatím žádné výdaje.", + "costs.settleHistory": "Historie vyrovnání", + "costs.noSettlements": "Zatím žádné vyrovnané platby.", + "costs.paymentsSettled": "{count} plateb vyrovnáno", + "costs.paid": "zaplaceno", + "costs.undo": "Vrátit zpět", + "costs.whatFor": "Za co to bylo?", + "costs.namePlaceholder": "např. večeře, suvenýry, benzín…", + "costs.totalAmount": "Celková částka", + "costs.currency": "Měna", + "costs.day": "Den", + "costs.rateLabel": "1 {from} v {to}", + "costs.category": "Kategorie", + "costs.whoPaid": "Kdo zaplatil?", + "costs.splitBetween": "Rozdělit rovným dílem mezi", + "costs.pickSomeone": "Vyberte alespoň jednu osobu pro rozdělení.", + "costs.splitSummary": "Rozděleno na {count} dílů · {amount} každý", + "costs.cat.accommodation": "Ubytování", + "costs.cat.food": "Jídlo a pití", + "costs.cat.groceries": "Potraviny", + "costs.cat.transport": "Doprava", + "costs.cat.flights": "Lety", + "costs.cat.activities": "Aktivity", + "costs.cat.sightseeing": "Prohlídka památek", + "costs.cat.shopping": "Nákupy", + "costs.cat.fees": "Poplatky a vstupenky", + "costs.cat.health": "Zdraví", + "costs.cat.tips": "Spropitné", + "costs.cat.other": "Ostatní", + "costs.daysCount": "{count} dní", + "costs.travelers": "{count} cestovatelů", + "costs.liveRate": "aktuální kurz", + "costs.settleAll": "Vyrovnat vše", }; export default budget; diff --git a/shared/src/i18n/cs/reservations.ts b/shared/src/i18n/cs/reservations.ts index 4fb68a23..0b43d17b 100644 --- a/shared/src/i18n/cs/reservations.ts +++ b/shared/src/i18n/cs/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Číslo letu', 'reservations.meta.from': 'Z', 'reservations.meta.to': 'Do', + 'reservations.layover.route': 'Trasa', + 'reservations.layover.stop': 'Zastávka', + 'reservations.layover.addStop': 'Přidat zastávku', + 'reservations.layover.connection': 'Přípoj', + 'reservations.layover.layover': 'Mezipřistání', 'reservations.needsReview': 'Zkontrolovat', 'reservations.needsReviewHint': 'Letiště nebylo možné automaticky rozpoznat — potvrďte prosím místo.', diff --git a/shared/src/i18n/de/reservations.ts b/shared/src/i18n/de/reservations.ts index 4930fc09..13bb6483 100644 --- a/shared/src/i18n/de/reservations.ts +++ b/shared/src/i18n/de/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Flugnr.', 'reservations.meta.from': 'Von', 'reservations.meta.to': 'Nach', + 'reservations.layover.route': 'Route', + 'reservations.layover.stop': 'Zwischenstopp', + 'reservations.layover.addStop': 'Zwischenstopp hinzufügen', + 'reservations.layover.connection': 'Anschlussflug', + 'reservations.layover.layover': 'Zwischenstopp', 'reservations.needsReview': 'Prüfen', 'reservations.needsReviewHint': 'Flughafen konnte nicht automatisch erkannt werden — bitte Ort bestätigen.', diff --git a/shared/src/i18n/en/reservations.ts b/shared/src/i18n/en/reservations.ts index 702ddcbe..87ae0ee7 100644 --- a/shared/src/i18n/en/reservations.ts +++ b/shared/src/i18n/en/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Flight No.', 'reservations.meta.from': 'From', 'reservations.meta.to': 'To', + 'reservations.layover.route': 'Route', + 'reservations.layover.stop': 'Stop', + 'reservations.layover.addStop': 'Add stop', + 'reservations.layover.connection': 'Connection', + 'reservations.layover.layover': 'Layover', 'reservations.needsReview': 'Review', 'reservations.needsReviewHint': 'Airport could not be matched automatically — please confirm the location.', diff --git a/shared/src/i18n/es/budget.ts b/shared/src/i18n/es/budget.ts index b865f36b..b1e5291a 100644 --- a/shared/src/i18n/es/budget.ts +++ b/shared/src/i18n/es/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { 'Haz clic en el avatar de un miembro en una partida del presupuesto para marcarlo en verde — esto significa que ha pagado. La liquidación muestra quién debe cuánto a quién.', 'budget.netBalances': 'Saldos netos', 'budget.categoriesLabel': 'categorías', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Tú", + "costs.youShort": "Tú", + "costs.youLower": "tú", + "costs.youOwe": "Debes", + "costs.youOweSub": "Deberías pagar a otros", + "costs.youreOwed": "Te deben", + "costs.youreOwedSub": "Otros deberían pagarte", + "costs.totalSpend": "Gasto total del viaje", + "costs.totalSpendSub": "Entre todos los viajeros", + "costs.to": "Para", + "costs.from": "De", + "costs.allSettled": "Estás al día con todo", + "costs.nothingOwed": "Nadie te debe nada", + "costs.yourShare": "Tu parte", + "costs.youPaid": "Pagaste", + "costs.expenses": "Gastos", + "costs.entries": "{count} entradas", + "costs.searchPlaceholder": "Buscar gastos…", + "costs.filter.all": "Todos", + "costs.filter.mine": "Pagados por mí", + "costs.filter.owed": "Me deben", + "costs.addExpense": "Añadir gasto", + "costs.editExpense": "Editar gasto", + "costs.noMatch": "Ningún gasto coincide con tu búsqueda.", + "costs.emptyText": "Aún no hay gastos. Añade el primero.", + "costs.spent": "{amount} gastados", + "costs.noDate": "Sin fecha", + "costs.noOnePaid": "Nadie ha pagado aún", + "costs.youLent": "prestaste {amount}", + "costs.youBorrowed": "tomaste prestado {amount}", + "costs.settleUp": "Saldar cuentas", + "costs.history": "Historial", + "costs.everyoneSquare": "Todos están en paz", + "costs.nothingOutstanding": "No hay pagos pendientes ahora mismo.", + "costs.pay": "paga", + "costs.pays": "paga", + "costs.settle": "Saldar", + "costs.balances": "Saldos", + "costs.byCategory": "Por categoría", + "costs.noCategories": "Aún no hay gastos.", + "costs.settleHistory": "Historial de pagos", + "costs.noSettlements": "Aún no hay pagos saldados.", + "costs.paymentsSettled": "{count} pagos saldados", + "costs.paid": "pagado", + "costs.undo": "Deshacer", + "costs.whatFor": "¿Para qué fue?", + "costs.namePlaceholder": "p. ej. Cena, souvenirs, gasolina…", + "costs.totalAmount": "Importe total", + "costs.currency": "Moneda", + "costs.day": "Día", + "costs.rateLabel": "1 {from} en {to}", + "costs.category": "Categoría", + "costs.whoPaid": "¿Quién pagó?", + "costs.splitBetween": "Dividir a partes iguales entre", + "costs.pickSomeone": "Elige al menos una persona con quien dividir.", + "costs.splitSummary": "Dividido entre {count} · {amount} cada uno", + "costs.cat.accommodation": "Alojamiento", + "costs.cat.food": "Comida y bebida", + "costs.cat.groceries": "Compras de comida", + "costs.cat.transport": "Transporte", + "costs.cat.flights": "Vuelos", + "costs.cat.activities": "Actividades", + "costs.cat.sightseeing": "Turismo", + "costs.cat.shopping": "Compras", + "costs.cat.fees": "Tasas y entradas", + "costs.cat.health": "Salud", + "costs.cat.tips": "Propinas", + "costs.cat.other": "Otros", + "costs.daysCount": "{count} días", + "costs.travelers": "{count} viajeros", + "costs.liveRate": "tasa en vivo", + "costs.settleAll": "Saldar todo", }; export default budget; diff --git a/shared/src/i18n/es/reservations.ts b/shared/src/i18n/es/reservations.ts index 33818bd8..26723284 100644 --- a/shared/src/i18n/es/reservations.ts +++ b/shared/src/i18n/es/reservations.ts @@ -101,6 +101,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'N° de vuelo', 'reservations.meta.from': 'Desde', 'reservations.meta.to': 'Hasta', + 'reservations.layover.route': 'Ruta', + 'reservations.layover.stop': 'Escala', + 'reservations.layover.addStop': 'Añadir escala', + 'reservations.layover.connection': 'Conexión', + 'reservations.layover.layover': 'Escala', 'reservations.needsReview': 'Revisar', 'reservations.needsReviewHint': 'No se pudo identificar el aeropuerto automáticamente — por favor confirma la ubicación.', diff --git a/shared/src/i18n/fr/budget.ts b/shared/src/i18n/fr/budget.ts index 8e7949c7..90b1c996 100644 --- a/shared/src/i18n/fr/budget.ts +++ b/shared/src/i18n/fr/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { "Cliquez sur l'avatar d'un membre sur un poste budgétaire pour le marquer en vert — cela signifie qu'il a payé. Le règlement indique ensuite qui doit combien à qui.", 'budget.netBalances': 'Soldes nets', 'budget.categoriesLabel': 'catégories', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", + "costs.you": "Vous", + "costs.youShort": "V", + "costs.youLower": "vous", + "costs.youOwe": "Vous devez", + "costs.youOweSub": "Vous devez payer les autres", + "costs.youreOwed": "On vous doit", + "costs.youreOwedSub": "Les autres doivent vous payer", + "costs.totalSpend": "Dépenses totales du voyage", + "costs.totalSpendSub": "Tous voyageurs confondus", + "costs.to": "À", + "costs.from": "De", + "costs.allSettled": "Tout est réglé pour vous", + "costs.nothingOwed": "On ne vous doit rien", + "costs.yourShare": "Votre part", + "costs.youPaid": "Vous avez payé", + "costs.expenses": "Dépenses", + "costs.entries": "{count} entrées", + "costs.searchPlaceholder": "Rechercher des dépenses…", + "costs.filter.all": "Toutes", + "costs.filter.mine": "Payées par moi", + "costs.filter.owed": "On me doit", + "costs.addExpense": "Ajouter une dépense", + "costs.editExpense": "Modifier la dépense", + "costs.noMatch": "Aucune dépense ne correspond à votre recherche.", + "costs.emptyText": "Aucune dépense pour le moment. Ajoutez la première.", + "costs.spent": "{amount} dépensés", + "costs.noDate": "Aucune date", + "costs.noOnePaid": "Personne n'a encore payé", + "costs.youLent": "vous avez prêté {amount}", + "costs.youBorrowed": "vous avez emprunté {amount}", + "costs.settleUp": "Régler", + "costs.history": "Historique", + "costs.everyoneSquare": "Tout le monde est quitte", + "costs.nothingOutstanding": "Aucun paiement en attente pour le moment.", + "costs.pay": "payer", + "costs.pays": "paie", + "costs.settle": "Régler", + "costs.balances": "Soldes", + "costs.byCategory": "Par catégorie", + "costs.noCategories": "Aucune dépense pour le moment.", + "costs.settleHistory": "Historique des règlements", + "costs.noSettlements": "Aucun paiement réglé pour le moment.", + "costs.paymentsSettled": "{count} paiements réglés", + "costs.paid": "payé", + "costs.undo": "Annuler", + "costs.whatFor": "C'était pour quoi ?", + "costs.namePlaceholder": "ex. dîner, souvenirs, essence…", + "costs.totalAmount": "Montant total", + "costs.currency": "Devise", + "costs.day": "Jour", + "costs.rateLabel": "1 {from} en {to}", + "costs.category": "Catégorie", + "costs.whoPaid": "Qui a payé ?", + "costs.splitBetween": "Partager équitablement entre", + "costs.pickSomeone": "Choisissez au moins une personne avec qui partager.", + "costs.splitSummary": "Partagé en {count} · {amount} chacun", + "costs.cat.accommodation": "Hébergement", + "costs.cat.food": "Nourriture et boissons", + "costs.cat.groceries": "Courses", "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", + "costs.cat.flights": "Vols", + "costs.cat.activities": "Activités", + "costs.cat.sightseeing": "Visites", "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.cat.fees": "Frais et billets", + "costs.cat.health": "Santé", + "costs.cat.tips": "Pourboires", + "costs.cat.other": "Autre", + "costs.daysCount": "{count} jours", + "costs.travelers": "{count} voyageurs", + "costs.liveRate": "taux en direct", + "costs.settleAll": "Tout régler", }; export default budget; diff --git a/shared/src/i18n/fr/reservations.ts b/shared/src/i18n/fr/reservations.ts index 7e5fd025..a76892a9 100644 --- a/shared/src/i18n/fr/reservations.ts +++ b/shared/src/i18n/fr/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'N° de vol', 'reservations.meta.from': 'De', 'reservations.meta.to': 'À', + 'reservations.layover.route': 'Itinéraire', + 'reservations.layover.stop': 'Escale', + 'reservations.layover.addStop': 'Ajouter une escale', + 'reservations.layover.connection': 'Correspondance', + 'reservations.layover.layover': 'Escale', 'reservations.needsReview': 'Vérifier', 'reservations.needsReviewHint': "L'aéroport n'a pas pu être identifié automatiquement — veuillez confirmer l'emplacement.", diff --git a/shared/src/i18n/gr/budget.ts b/shared/src/i18n/gr/budget.ts index e9f41c10..501313c3 100644 --- a/shared/src/i18n/gr/budget.ts +++ b/shared/src/i18n/gr/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { 'Κάντε κλικ στο avatar ενός μέλους σε μια εγγραφή προϋπολογισμού για να το επισημάνετε πράσινο — αυτό σημαίνει ότι πλήρωσε. Η εκκαθάριση δείχνει στη συνέχεια ποιος χρωστάει σε ποιον και πόσα.', 'budget.netBalances': 'Καθαρά Υπόλοιπα', 'budget.categoriesLabel': 'κατηγορίες', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Εσείς", + "costs.youShort": "Ε", + "costs.youLower": "εσείς", + "costs.youOwe": "Χρωστάτε", + "costs.youOweSub": "Πρέπει να πληρώσετε άλλους", + "costs.youreOwed": "Σας χρωστούν", + "costs.youreOwedSub": "Άλλοι πρέπει να σας πληρώσουν", + "costs.totalSpend": "Συνολικές δαπάνες ταξιδιού", + "costs.totalSpendSub": "Όλων των ταξιδιωτών", + "costs.to": "Προς", + "costs.from": "Από", + "costs.allSettled": "Έχετε εξοφλήσει τα πάντα", + "costs.nothingOwed": "Δεν σας χρωστάει κανείς", + "costs.yourShare": "Το μερίδιό σας", + "costs.youPaid": "Πληρώσατε", + "costs.expenses": "Έξοδα", + "costs.entries": "{count} εγγραφές", + "costs.searchPlaceholder": "Αναζήτηση εξόδων…", + "costs.filter.all": "Όλα", + "costs.filter.mine": "Πληρωμένα από εμένα", + "costs.filter.owed": "Μου χρωστούν", + "costs.addExpense": "Προσθήκη εξόδου", + "costs.editExpense": "Επεξεργασία εξόδου", + "costs.noMatch": "Κανένα έξοδο δεν ταιριάζει με την αναζήτησή σας.", + "costs.emptyText": "Δεν υπάρχουν έξοδα ακόμη. Προσθέστε το πρώτο σας.", + "costs.spent": "{amount} δαπάνη", + "costs.noDate": "Χωρίς ημερομηνία", + "costs.noOnePaid": "Δεν πλήρωσε κανείς ακόμη", + "costs.youLent": "δανείσατε {amount}", + "costs.youBorrowed": "δανειστήκατε {amount}", + "costs.settleUp": "Εξόφληση", + "costs.history": "Ιστορικό", + "costs.everyoneSquare": "Όλοι είναι ξεκάθαροι", + "costs.nothingOutstanding": "Δεν υπάρχουν εκκρεμείς πληρωμές αυτή τη στιγμή.", + "costs.pay": "πληρώνει", + "costs.pays": "πληρώνει", + "costs.settle": "Εξόφληση", + "costs.balances": "Υπόλοιπα", + "costs.byCategory": "Ανά κατηγορία", + "costs.noCategories": "Δεν υπάρχουν έξοδα ακόμη.", + "costs.settleHistory": "Ιστορικό εξοφλήσεων", + "costs.noSettlements": "Δεν υπάρχουν εξοφλημένες πληρωμές ακόμη.", + "costs.paymentsSettled": "{count} πληρωμές εξοφλήθηκαν", + "costs.paid": "πλήρωσε", + "costs.undo": "Αναίρεση", + "costs.whatFor": "Για τι ήταν;", + "costs.namePlaceholder": "π.χ. Δείπνο, σουβενίρ, βενζίνη…", + "costs.totalAmount": "Συνολικό ποσό", + "costs.currency": "Νόμισμα", + "costs.day": "Ημέρα", + "costs.rateLabel": "1 {from} σε {to}", + "costs.category": "Κατηγορία", + "costs.whoPaid": "Ποιος πλήρωσε;", + "costs.splitBetween": "Ισόποση κατανομή μεταξύ", + "costs.pickSomeone": "Επιλέξτε τουλάχιστον ένα άτομο για τον διαμοιρασμό.", + "costs.splitSummary": "Κατανομή σε {count} μέρη · {amount} το καθένα", + "costs.cat.accommodation": "Διαμονή", + "costs.cat.food": "Φαγητό & ποτό", + "costs.cat.groceries": "Ψώνια σούπερ μάρκετ", + "costs.cat.transport": "Μεταφορά", + "costs.cat.flights": "Πτήσεις", + "costs.cat.activities": "Δραστηριότητες", + "costs.cat.sightseeing": "Αξιοθέατα", + "costs.cat.shopping": "Ψώνια", + "costs.cat.fees": "Τέλη & εισιτήρια", + "costs.cat.health": "Υγεία", + "costs.cat.tips": "Φιλοδωρήματα", + "costs.cat.other": "Άλλα", + "costs.daysCount": "{count} ημέρες", + "costs.travelers": "{count} ταξιδιώτες", + "costs.liveRate": "ζωντανή ισοτιμία", + "costs.settleAll": "Εξόφληση όλων", }; export default budget; diff --git a/shared/src/i18n/gr/reservations.ts b/shared/src/i18n/gr/reservations.ts index 6dcdf2e9..1a4ce8ed 100644 --- a/shared/src/i18n/gr/reservations.ts +++ b/shared/src/i18n/gr/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Αρ. Πτήσης', 'reservations.meta.from': 'Από', 'reservations.meta.to': 'Προς', + 'reservations.layover.route': 'Διαδρομή', + 'reservations.layover.stop': 'Στάση', + 'reservations.layover.addStop': 'Προσθήκη στάσης', + 'reservations.layover.connection': 'Ανταπόκριση', + 'reservations.layover.layover': 'Ενδιάμεση στάση', 'reservations.needsReview': 'Έλεγχος', 'reservations.needsReviewHint': 'Δεν ήταν δυνατή η αυτόματη αντιστοίχιση του αεροδρομίου — παρακαλώ επιβεβαιώστε την τοποθεσία.', diff --git a/shared/src/i18n/hu/budget.ts b/shared/src/i18n/hu/budget.ts index 70f7ec59..305b96e2 100644 --- a/shared/src/i18n/hu/budget.ts +++ b/shared/src/i18n/hu/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { 'Kattints egy tag avatárjára egy költségvetési tételen a zöld jelöléshez — ez azt jelenti, hogy fizetett. Az elszámolás ezután mutatja, ki kinek mennyivel tartozik.', 'budget.netBalances': 'Nettó egyenlegek', 'budget.categoriesLabel': 'kategóriák', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Te", + "costs.youShort": "T", + "costs.youLower": "te", + "costs.youOwe": "Tartozol", + "costs.youOweSub": "Fizetned kell másoknak", + "costs.youreOwed": "Neked tartoznak", + "costs.youreOwedSub": "Mások fizetnek neked", + "costs.totalSpend": "Teljes utazási költség", + "costs.totalSpendSub": "Az összes utazóra vetítve", + "costs.to": "Kinek", + "costs.from": "Kitől", + "costs.allSettled": "Minden el van számolva", + "costs.nothingOwed": "Senki sem tartozik neked", + "costs.yourShare": "A te részed", + "costs.youPaid": "Te fizettél", + "costs.expenses": "Költségek", + "costs.entries": "{count} bejegyzés", + "costs.searchPlaceholder": "Költségek keresése…", + "costs.filter.all": "Mind", + "costs.filter.mine": "Én fizettem", + "costs.filter.owed": "Nekem tartoznak", + "costs.addExpense": "Költség hozzáadása", + "costs.editExpense": "Költség szerkesztése", + "costs.noMatch": "Nincs a keresésnek megfelelő költség.", + "costs.emptyText": "Még nincs költség. Add hozzá az elsőt.", + "costs.spent": "{amount} elköltve", + "costs.noDate": "Nincs dátum", + "costs.noOnePaid": "Még senki sem fizetett", + "costs.youLent": "{amount} kölcsönadtál", + "costs.youBorrowed": "{amount} kölcsönkértél", + "costs.settleUp": "Elszámolás", + "costs.history": "Előzmények", + "costs.everyoneSquare": "Mindenki kvittben van", + "costs.nothingOutstanding": "Jelenleg nincs kifizetendő összeg.", + "costs.pay": "fizet", + "costs.pays": "fizet", + "costs.settle": "Elszámol", + "costs.balances": "Egyenlegek", + "costs.byCategory": "Kategóriánként", + "costs.noCategories": "Még nincs költség.", + "costs.settleHistory": "Elszámolási előzmények", + "costs.noSettlements": "Még nincs elszámolt fizetés.", + "costs.paymentsSettled": "{count} fizetés elszámolva", + "costs.paid": "fizetve", + "costs.undo": "Visszavonás", + "costs.whatFor": "Mire volt?", + "costs.namePlaceholder": "pl. vacsora, ajándékok, benzin…", + "costs.totalAmount": "Teljes összeg", + "costs.currency": "Pénznem", + "costs.day": "Nap", + "costs.rateLabel": "1 {from} ennyi: {to}", + "costs.category": "Kategória", + "costs.whoPaid": "Ki fizetett?", + "costs.splitBetween": "Egyenlően elosztva köztük", + "costs.pickSomeone": "Válassz legalább egy személyt a megosztáshoz.", + "costs.splitSummary": "{count} fő közt megosztva · egyenként {amount}", + "costs.cat.accommodation": "Szállás", + "costs.cat.food": "Étel és ital", + "costs.cat.groceries": "Élelmiszer", + "costs.cat.transport": "Közlekedés", + "costs.cat.flights": "Repülőjáratok", + "costs.cat.activities": "Programok", + "costs.cat.sightseeing": "Városnézés", + "costs.cat.shopping": "Vásárlás", + "costs.cat.fees": "Díjak és jegyek", + "costs.cat.health": "Egészség", + "costs.cat.tips": "Borravaló", + "costs.cat.other": "Egyéb", + "costs.daysCount": "{count} nap", + "costs.travelers": "{count} utazó", + "costs.liveRate": "élő árfolyam", + "costs.settleAll": "Összes elszámolása", }; export default budget; diff --git a/shared/src/i18n/hu/reservations.ts b/shared/src/i18n/hu/reservations.ts index 54edc1c7..5d5f83ba 100644 --- a/shared/src/i18n/hu/reservations.ts +++ b/shared/src/i18n/hu/reservations.ts @@ -29,6 +29,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Járatszám', 'reservations.meta.from': 'Honnan', 'reservations.meta.to': 'Hová', + 'reservations.layover.route': 'Útvonal', + 'reservations.layover.stop': 'Megálló', + 'reservations.layover.addStop': 'Megálló hozzáadása', + 'reservations.layover.connection': 'Csatlakozás', + 'reservations.layover.layover': 'Átszállás', 'reservations.needsReview': 'Ellenőrzés', 'reservations.needsReviewHint': 'A repülőteret nem sikerült automatikusan azonosítani — erősítsd meg a helyet.', diff --git a/shared/src/i18n/id/budget.ts b/shared/src/i18n/id/budget.ts index ed35f589..abbf8784 100644 --- a/shared/src/i18n/id/budget.ts +++ b/shared/src/i18n/id/budget.ts @@ -39,78 +39,78 @@ const budget: TranslationStrings = { 'Klik foto anggota di item anggaran untuk menandainya hijau — artinya mereka sudah bayar. Penyelesaian lalu menunjukkan siapa berhutang ke siapa dan berapa.', 'budget.netBalances': 'Saldo Bersih', 'budget.categoriesLabel': 'kategori', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Kamu", + "costs.youShort": "K", + "costs.youLower": "kamu", + "costs.youOwe": "Kamu berhutang", + "costs.youOweSub": "Kamu harus membayar yang lain", + "costs.youreOwed": "Kamu dipinjami", + "costs.youreOwedSub": "Yang lain harus membayarmu", + "costs.totalSpend": "Total pengeluaran perjalanan", + "costs.totalSpendSub": "Untuk semua pelancong", + "costs.to": "Ke", + "costs.from": "Dari", + "costs.allSettled": "Semua sudah lunas", + "costs.nothingOwed": "Tidak ada yang berhutang padamu", + "costs.yourShare": "Bagianmu", + "costs.youPaid": "Kamu membayar", + "costs.expenses": "Pengeluaran", + "costs.entries": "{count} entri", + "costs.searchPlaceholder": "Cari pengeluaran…", + "costs.filter.all": "Semua", + "costs.filter.mine": "Dibayar olehku", + "costs.filter.owed": "Dipinjami padaku", + "costs.addExpense": "Tambah pengeluaran", + "costs.editExpense": "Edit pengeluaran", + "costs.noMatch": "Tidak ada pengeluaran yang cocok dengan pencarianmu.", + "costs.emptyText": "Belum ada pengeluaran. Tambahkan yang pertama.", + "costs.spent": "{amount} dibelanjakan", + "costs.noDate": "Tanpa tanggal", + "costs.noOnePaid": "Belum ada yang membayar", + "costs.youLent": "kamu meminjamkan {amount}", + "costs.youBorrowed": "kamu meminjam {amount}", + "costs.settleUp": "Lunasi", + "costs.history": "Riwayat", + "costs.everyoneSquare": "Semua sudah impas", + "costs.nothingOutstanding": "Tidak ada pembayaran tertunggak saat ini.", + "costs.pay": "bayar", + "costs.pays": "membayar", + "costs.settle": "Lunasi", + "costs.balances": "Saldo", + "costs.byCategory": "Per kategori", + "costs.noCategories": "Belum ada pengeluaran.", + "costs.settleHistory": "Riwayat pelunasan", + "costs.noSettlements": "Belum ada pembayaran yang dilunasi.", + "costs.paymentsSettled": "{count} pembayaran dilunasi", + "costs.paid": "dibayar", + "costs.undo": "Urungkan", + "costs.whatFor": "Untuk apa?", + "costs.namePlaceholder": "mis. Makan malam, oleh-oleh, bensin…", + "costs.totalAmount": "Jumlah total", + "costs.currency": "Mata uang", + "costs.day": "Hari", + "costs.rateLabel": "1 {from} dalam {to}", + "costs.category": "Kategori", + "costs.whoPaid": "Siapa yang membayar?", + "costs.splitBetween": "Bagi rata antara", + "costs.pickSomeone": "Pilih setidaknya satu orang untuk berbagi.", + "costs.splitSummary": "Dibagi {count} cara · {amount} masing-masing", + "costs.cat.accommodation": "Akomodasi", + "costs.cat.food": "Makanan & minuman", + "costs.cat.groceries": "Belanja kebutuhan", + "costs.cat.transport": "Transportasi", + "costs.cat.flights": "Penerbangan", + "costs.cat.activities": "Aktivitas", + "costs.cat.sightseeing": "Wisata", + "costs.cat.shopping": "Belanja", + "costs.cat.fees": "Biaya & tiket", + "costs.cat.health": "Kesehatan", + "costs.cat.tips": "Tip", + "costs.cat.other": "Lainnya", + "costs.daysCount": "{count} hari", + "costs.travelers": "{count} pelancong", + "costs.liveRate": "kurs langsung", + "costs.settleAll": "Lunasi semua", }; export default budget; diff --git a/shared/src/i18n/id/reservations.ts b/shared/src/i18n/id/reservations.ts index 5779b617..d8ac5be1 100644 --- a/shared/src/i18n/id/reservations.ts +++ b/shared/src/i18n/id/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'No. Penerbangan', 'reservations.meta.from': 'Dari', 'reservations.meta.to': 'Ke', + 'reservations.layover.route': 'Rute', + 'reservations.layover.stop': 'Persinggahan', + 'reservations.layover.addStop': 'Tambah persinggahan', + 'reservations.layover.connection': 'Sambungan', + 'reservations.layover.layover': 'Transit', 'reservations.needsReview': 'Tinjau', 'reservations.needsReviewHint': 'Bandara tidak dapat dicocokkan otomatis — konfirmasi lokasi.', diff --git a/shared/src/i18n/it/budget.ts b/shared/src/i18n/it/budget.ts index bb65a324..af357871 100644 --- a/shared/src/i18n/it/budget.ts +++ b/shared/src/i18n/it/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { "Clicca sull'avatar di un membro su una voce di budget per contrassegnarlo in verde — significa che ha pagato. Il regolamento mostra poi chi deve quanto a chi.", 'budget.netBalances': 'Saldi netti', 'budget.categoriesLabel': 'categorie', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", + "costs.you": "Tu", + "costs.youShort": "T", + "costs.youLower": "tu", + "costs.youOwe": "Devi", + "costs.youOweSub": "Dovresti pagare gli altri", + "costs.youreOwed": "Ti devono", + "costs.youreOwedSub": "Gli altri dovrebbero pagarti", + "costs.totalSpend": "Spesa totale del viaggio", + "costs.totalSpendSub": "Tra tutti i viaggiatori", + "costs.to": "A", + "costs.from": "Da", + "costs.allSettled": "Hai saldato tutto", + "costs.nothingOwed": "Nessuno ti deve nulla", + "costs.yourShare": "La tua quota", + "costs.youPaid": "Hai pagato", + "costs.expenses": "Spese", + "costs.entries": "{count} voci", + "costs.searchPlaceholder": "Cerca spese…", + "costs.filter.all": "Tutte", + "costs.filter.mine": "Pagate da me", + "costs.filter.owed": "Mi devono", + "costs.addExpense": "Aggiungi spesa", + "costs.editExpense": "Modifica spesa", + "costs.noMatch": "Nessuna spesa corrisponde alla ricerca.", + "costs.emptyText": "Ancora nessuna spesa. Aggiungi la prima.", + "costs.spent": "{amount} spesi", + "costs.noDate": "Nessuna data", + "costs.noOnePaid": "Nessuno ha ancora pagato", + "costs.youLent": "hai prestato {amount}", + "costs.youBorrowed": "hai preso in prestito {amount}", + "costs.settleUp": "Salda", + "costs.history": "Cronologia", + "costs.everyoneSquare": "Sono tutti in pari", + "costs.nothingOutstanding": "Nessun pagamento in sospeso al momento.", + "costs.pay": "paga", + "costs.pays": "paga", + "costs.settle": "Salda", + "costs.balances": "Saldi", + "costs.byCategory": "Per categoria", + "costs.noCategories": "Ancora nessuna spesa.", + "costs.settleHistory": "Cronologia saldi", + "costs.noSettlements": "Ancora nessun pagamento saldato.", + "costs.paymentsSettled": "{count} pagamenti saldati", + "costs.paid": "pagato", + "costs.undo": "Annulla", + "costs.whatFor": "Per cosa era?", + "costs.namePlaceholder": "es. Cena, souvenir, benzina…", + "costs.totalAmount": "Importo totale", + "costs.currency": "Valuta", + "costs.day": "Giorno", "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", + "costs.category": "Categoria", + "costs.whoPaid": "Chi ha pagato?", + "costs.splitBetween": "Dividi equamente tra", + "costs.pickSomeone": "Scegli almeno una persona con cui dividere.", + "costs.splitSummary": "Diviso in {count} · {amount} ciascuno", + "costs.cat.accommodation": "Alloggio", + "costs.cat.food": "Cibo e bevande", + "costs.cat.groceries": "Spesa alimentare", + "costs.cat.transport": "Trasporti", + "costs.cat.flights": "Voli", + "costs.cat.activities": "Attività", + "costs.cat.sightseeing": "Visite turistiche", "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.cat.fees": "Tariffe e biglietti", + "costs.cat.health": "Salute", + "costs.cat.tips": "Mance", + "costs.cat.other": "Altro", + "costs.daysCount": "{count} giorni", + "costs.travelers": "{count} viaggiatori", + "costs.liveRate": "tasso in tempo reale", + "costs.settleAll": "Salda tutto", }; export default budget; diff --git a/shared/src/i18n/it/reservations.ts b/shared/src/i18n/it/reservations.ts index a3918195..374880fd 100644 --- a/shared/src/i18n/it/reservations.ts +++ b/shared/src/i18n/it/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'N. volo', 'reservations.meta.from': 'Da', 'reservations.meta.to': 'A', + 'reservations.layover.route': 'Itinerario', + 'reservations.layover.stop': 'Scalo', + 'reservations.layover.addStop': 'Aggiungi scalo', + 'reservations.layover.connection': 'Coincidenza', + 'reservations.layover.layover': 'Sosta', 'reservations.needsReview': 'Verifica', 'reservations.needsReviewHint': "L'aeroporto non è stato riconosciuto automaticamente — conferma la posizione.", diff --git a/shared/src/i18n/ja/budget.ts b/shared/src/i18n/ja/budget.ts index 1c676d0c..77e39b49 100644 --- a/shared/src/i18n/ja/budget.ts +++ b/shared/src/i18n/ja/budget.ts @@ -38,78 +38,78 @@ const budget: TranslationStrings = { '予算項目のメンバーアイコンをクリックして緑にすると、支払い済みを示します。精算では、誰が誰にいくら支払うべきかを表示します。', 'budget.netBalances': '差引残高', 'budget.categoriesLabel': 'カテゴリ', - "costs.you": "You", + "costs.you": "自分", "costs.youShort": "Y", "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.youOwe": "支払う額", + "costs.youOweSub": "他の人に支払うべき額", + "costs.youreOwed": "受け取る額", + "costs.youreOwedSub": "他の人があなたに支払うべき額", + "costs.totalSpend": "旅行の合計支出", + "costs.totalSpendSub": "全員の合計", + "costs.to": "支払先", + "costs.from": "支払元", + "costs.allSettled": "すべて精算済みです", + "costs.nothingOwed": "受け取る額はありません", + "costs.yourShare": "あなたの負担分", + "costs.youPaid": "あなたが支払った額", + "costs.expenses": "支出", + "costs.entries": "{count}件", + "costs.searchPlaceholder": "支出を検索…", + "costs.filter.all": "すべて", + "costs.filter.mine": "自分が支払った分", + "costs.filter.owed": "受け取る分", + "costs.addExpense": "支出を追加", + "costs.editExpense": "支出を編集", + "costs.noMatch": "検索条件に一致する支出はありません。", + "costs.emptyText": "支出はまだありません。最初の支出を追加しましょう。", + "costs.spent": "{amount}を支出", + "costs.noDate": "日付なし", + "costs.noOnePaid": "まだ誰も支払っていません", + "costs.youLent": "{amount}を立て替えました", + "costs.youBorrowed": "{amount}を借りています", + "costs.settleUp": "精算する", + "costs.history": "履歴", + "costs.everyoneSquare": "全員が清算済みです", + "costs.nothingOutstanding": "現在、未払いの支払いはありません。", + "costs.pay": "支払う", + "costs.pays": "支払う", + "costs.settle": "精算", + "costs.balances": "残高", + "costs.byCategory": "カテゴリ別", + "costs.noCategories": "支出はまだありません。", + "costs.settleHistory": "精算履歴", + "costs.noSettlements": "精算済みの支払いはまだありません。", + "costs.paymentsSettled": "{count}件の支払いを精算済み", + "costs.paid": "支払済み", + "costs.undo": "元に戻す", + "costs.whatFor": "何の支出ですか?", + "costs.namePlaceholder": "例:夕食、お土産、ガソリン…", + "costs.totalAmount": "合計金額", + "costs.currency": "通貨", + "costs.day": "日", + "costs.rateLabel": "1 {from} = {to}", + "costs.category": "カテゴリ", + "costs.whoPaid": "誰が支払いましたか?", + "costs.splitBetween": "均等に分割する相手", + "costs.pickSomeone": "分割する相手を少なくとも1人選んでください。", + "costs.splitSummary": "{count}人で分割 · 各{amount}", + "costs.cat.accommodation": "宿泊", + "costs.cat.food": "飲食", + "costs.cat.groceries": "食料品", + "costs.cat.transport": "交通", + "costs.cat.flights": "航空券", + "costs.cat.activities": "アクティビティ", + "costs.cat.sightseeing": "観光", + "costs.cat.shopping": "買い物", + "costs.cat.fees": "手数料・チケット", + "costs.cat.health": "健康", + "costs.cat.tips": "チップ", + "costs.cat.other": "その他", + "costs.daysCount": "{count}日間", + "costs.travelers": "{count}人の旅行者", + "costs.liveRate": "リアルタイムレート", + "costs.settleAll": "すべて精算", }; export default budget; diff --git a/shared/src/i18n/ja/reservations.ts b/shared/src/i18n/ja/reservations.ts index a11859d6..4acd2180 100644 --- a/shared/src/i18n/ja/reservations.ts +++ b/shared/src/i18n/ja/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': '便名', 'reservations.meta.from': '出発地', 'reservations.meta.to': '到着地', + 'reservations.layover.route': '経路', + 'reservations.layover.stop': '経由地', + 'reservations.layover.addStop': '経由地を追加', + 'reservations.layover.connection': '乗り継ぎ便', + 'reservations.layover.layover': '乗り継ぎ', 'reservations.needsReview': '要確認', 'reservations.needsReviewHint': '空港を自動で特定できませんでした。場所を確認してください。', diff --git a/shared/src/i18n/ko/budget.ts b/shared/src/i18n/ko/budget.ts index 1bdd2e55..ef5cd497 100644 --- a/shared/src/i18n/ko/budget.ts +++ b/shared/src/i18n/ko/budget.ts @@ -38,78 +38,78 @@ const budget: TranslationStrings = { '예산 항목의 멤버 아바타를 클릭하면 녹색으로 표시됩니다 — 해당 멤버가 지불했음을 의미합니다. 그러면 정산에서 누가 누구에게 얼마를 지불해야 하는지 보여줍니다.', 'budget.netBalances': '순 잔액', 'budget.categoriesLabel': '카테고리', - "costs.you": "You", + "costs.you": "나", "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.youLower": "나", + "costs.youOwe": "내가 줄 돈", + "costs.youOweSub": "다른 사람에게 지불해야 합니다", + "costs.youreOwed": "받을 돈", + "costs.youreOwedSub": "다른 사람이 나에게 지불해야 합니다", + "costs.totalSpend": "총 여행 지출", + "costs.totalSpendSub": "모든 여행자 합계", + "costs.to": "받는 사람", + "costs.from": "보낸 사람", + "costs.allSettled": "모두 정산되었습니다", + "costs.nothingOwed": "받을 돈이 없습니다", + "costs.yourShare": "내 몫", + "costs.youPaid": "내가 지불함", + "costs.expenses": "지출", + "costs.entries": "{count}개 항목", + "costs.searchPlaceholder": "지출 검색…", + "costs.filter.all": "전체", + "costs.filter.mine": "내가 지불", + "costs.filter.owed": "받을 돈", + "costs.addExpense": "지출 추가", + "costs.editExpense": "지출 편집", + "costs.noMatch": "검색과 일치하는 지출이 없습니다.", + "costs.emptyText": "아직 지출이 없습니다. 첫 항목을 추가하세요.", + "costs.spent": "{amount} 지출", + "costs.noDate": "날짜 없음", + "costs.noOnePaid": "아직 아무도 지불하지 않음", + "costs.youLent": "{amount} 빌려줌", + "costs.youBorrowed": "{amount} 빌림", + "costs.settleUp": "정산하기", + "costs.history": "내역", + "costs.everyoneSquare": "모두 정산 완료", + "costs.nothingOutstanding": "현재 미결제 금액이 없습니다.", + "costs.pay": "지불", + "costs.pays": "지불", + "costs.settle": "정산", + "costs.balances": "잔액", + "costs.byCategory": "카테고리별", + "costs.noCategories": "아직 지출이 없습니다.", + "costs.settleHistory": "정산 내역", + "costs.noSettlements": "아직 정산된 결제가 없습니다.", + "costs.paymentsSettled": "{count}건 정산됨", + "costs.paid": "지불됨", + "costs.undo": "실행 취소", + "costs.whatFor": "무엇을 위한 것인가요?", + "costs.namePlaceholder": "예: 저녁 식사, 기념품, 주유…", + "costs.totalAmount": "총 금액", + "costs.currency": "통화", + "costs.day": "날짜", + "costs.rateLabel": "1 {from} = {to}", + "costs.category": "카테고리", + "costs.whoPaid": "누가 지불했나요?", + "costs.splitBetween": "균등 분할 대상", + "costs.pickSomeone": "분할할 사람을 한 명 이상 선택하세요.", + "costs.splitSummary": "{count}명 분할 · 각 {amount}", + "costs.cat.accommodation": "숙박", + "costs.cat.food": "식음료", + "costs.cat.groceries": "식료품", + "costs.cat.transport": "교통", + "costs.cat.flights": "항공편", + "costs.cat.activities": "액티비티", + "costs.cat.sightseeing": "관광", + "costs.cat.shopping": "쇼핑", + "costs.cat.fees": "요금 및 입장권", + "costs.cat.health": "건강", + "costs.cat.tips": "팁", + "costs.cat.other": "기타", + "costs.daysCount": "{count}일", + "costs.travelers": "여행자 {count}명", + "costs.liveRate": "실시간 환율", + "costs.settleAll": "전체 정산", }; export default budget; diff --git a/shared/src/i18n/ko/reservations.ts b/shared/src/i18n/ko/reservations.ts index 38918c4b..5072f916 100644 --- a/shared/src/i18n/ko/reservations.ts +++ b/shared/src/i18n/ko/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': '항공편 번호', 'reservations.meta.from': '출발', 'reservations.meta.to': '도착', + 'reservations.layover.route': '경로', + 'reservations.layover.stop': '경유', + 'reservations.layover.addStop': '경유지 추가', + 'reservations.layover.connection': '연결편', + 'reservations.layover.layover': '경유 대기', 'reservations.needsReview': '검토 필요', 'reservations.needsReviewHint': '공항이 자동으로 매칭되지 않았습니다 — 위치를 확인해 주세요.', diff --git a/shared/src/i18n/nl/budget.ts b/shared/src/i18n/nl/budget.ts index fb02fc33..37532e3c 100644 --- a/shared/src/i18n/nl/budget.ts +++ b/shared/src/i18n/nl/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { 'Klik op de avatar van een lid bij een budgetpost om deze groen te markeren — dit betekent dat diegene heeft betaald. De afrekening toont vervolgens wie wie hoeveel verschuldigd is.', 'budget.netBalances': 'Nettosaldi', 'budget.categoriesLabel': 'categorieën', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", + "costs.you": "Jij", + "costs.youShort": "J", + "costs.youLower": "jij", + "costs.youOwe": "Jij bent verschuldigd", + "costs.youOweSub": "Jij moet anderen betalen", + "costs.youreOwed": "Jij krijgt nog", + "costs.youreOwedSub": "Anderen moeten jou betalen", + "costs.totalSpend": "Totale reisuitgaven", + "costs.totalSpendSub": "Voor alle reizigers", + "costs.to": "Aan", + "costs.from": "Van", + "costs.allSettled": "Je bent helemaal afgerekend", + "costs.nothingOwed": "Niemand is jou iets verschuldigd", + "costs.yourShare": "Jouw aandeel", + "costs.youPaid": "Jij hebt betaald", + "costs.expenses": "Uitgaven", + "costs.entries": "{count} invoeren", + "costs.searchPlaceholder": "Uitgaven zoeken…", + "costs.filter.all": "Alles", + "costs.filter.mine": "Door mij betaald", + "costs.filter.owed": "Mij verschuldigd", + "costs.addExpense": "Uitgave toevoegen", + "costs.editExpense": "Uitgave bewerken", + "costs.noMatch": "Geen uitgaven komen overeen met je zoekopdracht.", + "costs.emptyText": "Nog geen uitgaven. Voeg je eerste toe.", + "costs.spent": "{amount} uitgegeven", + "costs.noDate": "Geen datum", + "costs.noOnePaid": "Nog niemand heeft betaald", + "costs.youLent": "je hebt {amount} voorgeschoten", + "costs.youBorrowed": "je hebt {amount} geleend", + "costs.settleUp": "Afrekenen", + "costs.history": "Geschiedenis", + "costs.everyoneSquare": "Iedereen is quitte", + "costs.nothingOutstanding": "Er staan op dit moment geen betalingen open.", + "costs.pay": "betaalt", + "costs.pays": "betaalt", + "costs.settle": "Afrekenen", + "costs.balances": "Saldi", + "costs.byCategory": "Per categorie", + "costs.noCategories": "Nog geen uitgaven.", + "costs.settleHistory": "Afrekengeschiedenis", + "costs.noSettlements": "Nog geen afgerekende betalingen.", + "costs.paymentsSettled": "{count} betalingen afgerekend", + "costs.paid": "betaald", + "costs.undo": "Ongedaan maken", + "costs.whatFor": "Waar was het voor?", + "costs.namePlaceholder": "bijv. Diner, souvenirs, benzine…", + "costs.totalAmount": "Totaalbedrag", + "costs.currency": "Valuta", + "costs.day": "Dag", "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.category": "Categorie", + "costs.whoPaid": "Wie heeft betaald?", + "costs.splitBetween": "Gelijk verdelen over", + "costs.pickSomeone": "Kies minstens één persoon om mee te delen.", + "costs.splitSummary": "Verdeeld over {count} · {amount} elk", + "costs.cat.accommodation": "Accommodatie", + "costs.cat.food": "Eten & drinken", + "costs.cat.groceries": "Boodschappen", + "costs.cat.transport": "Vervoer", + "costs.cat.flights": "Vluchten", + "costs.cat.activities": "Activiteiten", + "costs.cat.sightseeing": "Bezienswaardigheden", + "costs.cat.shopping": "Winkelen", + "costs.cat.fees": "Kosten & tickets", + "costs.cat.health": "Gezondheid", + "costs.cat.tips": "Fooien", + "costs.cat.other": "Overig", + "costs.daysCount": "{count} dagen", + "costs.travelers": "{count} reizigers", + "costs.liveRate": "live koers", + "costs.settleAll": "Alles afrekenen", }; export default budget; diff --git a/shared/src/i18n/nl/reservations.ts b/shared/src/i18n/nl/reservations.ts index a61a15b8..06b6fdbb 100644 --- a/shared/src/i18n/nl/reservations.ts +++ b/shared/src/i18n/nl/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Vluchtnr.', 'reservations.meta.from': 'Van', 'reservations.meta.to': 'Naar', + 'reservations.layover.route': 'Route', + 'reservations.layover.stop': 'Tussenstop', + 'reservations.layover.addStop': 'Tussenstop toevoegen', + 'reservations.layover.connection': 'Aansluiting', + 'reservations.layover.layover': 'Overstap', 'reservations.needsReview': 'Controleren', 'reservations.needsReviewHint': 'Luchthaven kon niet automatisch worden herkend — bevestig de locatie.', diff --git a/shared/src/i18n/pl/budget.ts b/shared/src/i18n/pl/budget.ts index 752acbf1..8f39ea49 100644 --- a/shared/src/i18n/pl/budget.ts +++ b/shared/src/i18n/pl/budget.ts @@ -38,78 +38,78 @@ const budget: TranslationStrings = { 'budget.exportCsv': 'Eksportuj CSV', 'budget.table.date': 'Data', 'budget.categoriesLabel': 'kategorie', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", + "costs.you": "Ty", + "costs.youShort": "T", + "costs.youLower": "ty", + "costs.youOwe": "Jesteś winien", + "costs.youOweSub": "Powinieneś zapłacić innym", + "costs.youreOwed": "Należy ci się", + "costs.youreOwedSub": "Inni powinni zapłacić tobie", + "costs.totalSpend": "Łączne wydatki na podróż", + "costs.totalSpendSub": "Wszystkich podróżnych", + "costs.to": "Do", + "costs.from": "Od", + "costs.allSettled": "Wszystko rozliczone", + "costs.nothingOwed": "Nikt nie jest ci nic winien", + "costs.yourShare": "Twój udział", + "costs.youPaid": "Zapłaciłeś", + "costs.expenses": "Wydatki", + "costs.entries": "Wpisów: {count}", + "costs.searchPlaceholder": "Szukaj wydatków…", + "costs.filter.all": "Wszystkie", + "costs.filter.mine": "Opłacone przeze mnie", + "costs.filter.owed": "Należy mi się", + "costs.addExpense": "Dodaj wydatek", + "costs.editExpense": "Edytuj wydatek", + "costs.noMatch": "Brak wydatków pasujących do wyszukiwania.", + "costs.emptyText": "Brak wydatków. Dodaj pierwszy.", + "costs.spent": "wydano {amount}", + "costs.noDate": "Brak daty", + "costs.noOnePaid": "Nikt jeszcze nie zapłacił", + "costs.youLent": "pożyczyłeś {amount}", + "costs.youBorrowed": "pożyczyłeś od innych {amount}", + "costs.settleUp": "Rozlicz", + "costs.history": "Historia", + "costs.everyoneSquare": "Wszyscy rozliczeni", + "costs.nothingOutstanding": "Brak nierozliczonych płatności.", + "costs.pay": "zapłać", + "costs.pays": "płaci", + "costs.settle": "Rozlicz", + "costs.balances": "Salda", + "costs.byCategory": "Według kategorii", + "costs.noCategories": "Brak wydatków.", + "costs.settleHistory": "Historia rozliczeń", + "costs.noSettlements": "Brak rozliczonych płatności.", + "costs.paymentsSettled": "Rozliczonych płatności: {count}", + "costs.paid": "zapłacono", + "costs.undo": "Cofnij", + "costs.whatFor": "Na co to było?", + "costs.namePlaceholder": "np. kolacja, pamiątki, paliwo…", + "costs.totalAmount": "Łączna kwota", + "costs.currency": "Waluta", + "costs.day": "Dzień", + "costs.rateLabel": "1 {from} w {to}", + "costs.category": "Kategoria", + "costs.whoPaid": "Kto zapłacił?", + "costs.splitBetween": "Podziel równo między", + "costs.pickSomeone": "Wybierz co najmniej jedną osobę do podziału.", + "costs.splitSummary": "Podział na {count} · {amount} na osobę", + "costs.cat.accommodation": "Nocleg", + "costs.cat.food": "Jedzenie i napoje", + "costs.cat.groceries": "Zakupy spożywcze", "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.cat.flights": "Loty", + "costs.cat.activities": "Atrakcje", + "costs.cat.sightseeing": "Zwiedzanie", + "costs.cat.shopping": "Zakupy", + "costs.cat.fees": "Opłaty i bilety", + "costs.cat.health": "Zdrowie", + "costs.cat.tips": "Napiwki", + "costs.cat.other": "Inne", + "costs.daysCount": "Dni: {count}", + "costs.travelers": "Podróżnych: {count}", + "costs.liveRate": "kurs na żywo", + "costs.settleAll": "Rozlicz wszystko", }; export default budget; diff --git a/shared/src/i18n/pl/reservations.ts b/shared/src/i18n/pl/reservations.ts index 3f8328a3..01444301 100644 --- a/shared/src/i18n/pl/reservations.ts +++ b/shared/src/i18n/pl/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Numer lotu', 'reservations.meta.from': 'Skąd', 'reservations.meta.to': 'Dokąd', + 'reservations.layover.route': 'Trasa', + 'reservations.layover.stop': 'Przystanek', + 'reservations.layover.addStop': 'Dodaj przystanek', + 'reservations.layover.connection': 'Połączenie', + 'reservations.layover.layover': 'Przesiadka', 'reservations.meta.trainNumber': 'Numer pociągu', 'reservations.meta.platform': 'Peron', 'reservations.meta.seat': 'Miejsce', diff --git a/shared/src/i18n/ru/budget.ts b/shared/src/i18n/ru/budget.ts index 03c559c5..df75787d 100644 --- a/shared/src/i18n/ru/budget.ts +++ b/shared/src/i18n/ru/budget.ts @@ -40,78 +40,78 @@ const budget: TranslationStrings = { 'Нажмите на аватар участника в строке бюджета, чтобы отметить его зелёным — это значит, что он заплатил. Взаиморасчёт покажет, кто кому и сколько должен.', 'budget.netBalances': 'Чистые балансы', 'budget.categoriesLabel': 'категорий', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Вы", + "costs.youShort": "В", + "costs.youLower": "вы", + "costs.youOwe": "Вы должны", + "costs.youOweSub": "Вы должны заплатить другим", + "costs.youreOwed": "Вам должны", + "costs.youreOwedSub": "Другие должны заплатить вам", + "costs.totalSpend": "Общие расходы поездки", + "costs.totalSpendSub": "По всем участникам", + "costs.to": "Кому", + "costs.from": "От", + "costs.allSettled": "У вас всё рассчитано", + "costs.nothingOwed": "Вам ничего не должны", + "costs.yourShare": "Ваша доля", + "costs.youPaid": "Вы заплатили", + "costs.expenses": "Расходы", + "costs.entries": "{count} записей", + "costs.searchPlaceholder": "Поиск расходов…", + "costs.filter.all": "Все", + "costs.filter.mine": "Оплачено мной", + "costs.filter.owed": "Мне должны", + "costs.addExpense": "Добавить расход", + "costs.editExpense": "Изменить расход", + "costs.noMatch": "Нет расходов по вашему запросу.", + "costs.emptyText": "Расходов пока нет. Добавьте первый.", + "costs.spent": "потрачено {amount}", + "costs.noDate": "Без даты", + "costs.noOnePaid": "Пока никто не заплатил", + "costs.youLent": "вы одолжили {amount}", + "costs.youBorrowed": "вы заняли {amount}", + "costs.settleUp": "Рассчитаться", + "costs.history": "История", + "costs.everyoneSquare": "Все в расчёте", + "costs.nothingOutstanding": "Сейчас нет неоплаченных платежей.", + "costs.pay": "платит", + "costs.pays": "платит", + "costs.settle": "Рассчитать", + "costs.balances": "Балансы", + "costs.byCategory": "По категориям", + "costs.noCategories": "Расходов пока нет.", + "costs.settleHistory": "История расчётов", + "costs.noSettlements": "Расчётов пока нет.", + "costs.paymentsSettled": "Рассчитано платежей: {count}", + "costs.paid": "оплачено", + "costs.undo": "Отменить", + "costs.whatFor": "За что это было?", + "costs.namePlaceholder": "напр. ужин, сувениры, бензин…", + "costs.totalAmount": "Общая сумма", + "costs.currency": "Валюта", + "costs.day": "День", + "costs.rateLabel": "1 {from} в {to}", + "costs.category": "Категория", + "costs.whoPaid": "Кто заплатил?", + "costs.splitBetween": "Поделить поровну между", + "costs.pickSomeone": "Выберите хотя бы одного человека для разделения.", + "costs.splitSummary": "Разделено на {count} · по {amount}", + "costs.cat.accommodation": "Проживание", + "costs.cat.food": "Еда и напитки", + "costs.cat.groceries": "Продукты", + "costs.cat.transport": "Транспорт", + "costs.cat.flights": "Авиаперелёты", + "costs.cat.activities": "Развлечения", + "costs.cat.sightseeing": "Достопримечательности", + "costs.cat.shopping": "Покупки", + "costs.cat.fees": "Сборы и билеты", + "costs.cat.health": "Здоровье", + "costs.cat.tips": "Чаевые", + "costs.cat.other": "Прочее", + "costs.daysCount": "{count} дней", + "costs.travelers": "{count} путешественников", + "costs.liveRate": "актуальный курс", + "costs.settleAll": "Рассчитать всё", }; export default budget; diff --git a/shared/src/i18n/ru/reservations.ts b/shared/src/i18n/ru/reservations.ts index 6ebc1620..d4ae5d08 100644 --- a/shared/src/i18n/ru/reservations.ts +++ b/shared/src/i18n/ru/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Номер рейса', 'reservations.meta.from': 'Откуда', 'reservations.meta.to': 'Куда', + 'reservations.layover.route': 'Маршрут', + 'reservations.layover.stop': 'Остановка', + 'reservations.layover.addStop': 'Добавить остановку', + 'reservations.layover.connection': 'Стыковка', + 'reservations.layover.layover': 'Пересадка', 'reservations.needsReview': 'Проверить', 'reservations.needsReviewHint': 'Аэропорт не удалось определить автоматически — подтвердите местоположение.', diff --git a/shared/src/i18n/tr/budget.ts b/shared/src/i18n/tr/budget.ts index ab913e59..6b6e1815 100644 --- a/shared/src/i18n/tr/budget.ts +++ b/shared/src/i18n/tr/budget.ts @@ -39,78 +39,78 @@ const budget: TranslationStrings = { 'Bir bütçe kalemindeki üye avatarına tıklayarak yeşil işaretleyin — bu ödedikleri anlamına gelir. Hesaplaşma kimin kime ne kadar borçlu olduğunu gösterir.', 'budget.netBalances': 'Net Bakiyeler', 'budget.categoriesLabel': 'kategoriler', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Siz", + "costs.youShort": "S", + "costs.youLower": "siz", + "costs.youOwe": "Borcunuz", + "costs.youOweSub": "Başkalarına ödemeniz gerekiyor", + "costs.youreOwed": "Size borçlu", + "costs.youreOwedSub": "Başkaları size ödemeli", + "costs.totalSpend": "Toplam seyahat harcaması", + "costs.totalSpendSub": "Tüm yolcular genelinde", + "costs.to": "Alıcı", + "costs.from": "Gönderen", + "costs.allSettled": "Tüm hesaplar kapandı", + "costs.nothingOwed": "Size borç yok", + "costs.yourShare": "Sizin payınız", + "costs.youPaid": "Siz ödediniz", + "costs.expenses": "Harcamalar", + "costs.entries": "{count} kayıt", + "costs.searchPlaceholder": "Harcamalarda ara…", + "costs.filter.all": "Tümü", + "costs.filter.mine": "Benim ödediklerim", + "costs.filter.owed": "Bana borçlu", + "costs.addExpense": "Harcama ekle", + "costs.editExpense": "Harcamayı düzenle", + "costs.noMatch": "Aramanızla eşleşen harcama yok.", + "costs.emptyText": "Henüz harcama yok. İlkini ekleyin.", + "costs.spent": "{amount} harcandı", + "costs.noDate": "Tarih yok", + "costs.noOnePaid": "Henüz kimse ödemedi", + "costs.youLent": "{amount} verdiniz", + "costs.youBorrowed": "{amount} aldınız", + "costs.settleUp": "Hesaplaş", + "costs.history": "Geçmiş", + "costs.everyoneSquare": "Herkesin hesabı kapalı", + "costs.nothingOutstanding": "Şu anda bekleyen ödeme yok.", + "costs.pay": "öde", + "costs.pays": "ödüyor", + "costs.settle": "Hesaplaş", + "costs.balances": "Bakiyeler", + "costs.byCategory": "Kategoriye göre", + "costs.noCategories": "Henüz harcama yok.", + "costs.settleHistory": "Hesaplaşma geçmişi", + "costs.noSettlements": "Henüz kapatılmış ödeme yok.", + "costs.paymentsSettled": "{count} ödeme kapatıldı", + "costs.paid": "ödendi", + "costs.undo": "Geri al", + "costs.whatFor": "Ne içindi?", + "costs.namePlaceholder": "örn. Akşam yemeği, hediyelik, benzin…", + "costs.totalAmount": "Toplam tutar", + "costs.currency": "Para birimi", + "costs.day": "Gün", + "costs.rateLabel": "{to} cinsinden 1 {from}", + "costs.category": "Kategori", + "costs.whoPaid": "Kim ödedi?", + "costs.splitBetween": "Eşit olarak böl", + "costs.pickSomeone": "Paylaşmak için en az bir kişi seçin.", + "costs.splitSummary": "{count} kişiye bölündü · her biri {amount}", + "costs.cat.accommodation": "Konaklama", + "costs.cat.food": "Yiyecek & içecek", + "costs.cat.groceries": "Market alışverişi", + "costs.cat.transport": "Ulaşım", + "costs.cat.flights": "Uçuşlar", + "costs.cat.activities": "Etkinlikler", + "costs.cat.sightseeing": "Gezi & turlar", + "costs.cat.shopping": "Alışveriş", + "costs.cat.fees": "Ücretler & biletler", + "costs.cat.health": "Sağlık", + "costs.cat.tips": "Bahşişler", + "costs.cat.other": "Diğer", + "costs.daysCount": "{count} gün", + "costs.travelers": "{count} yolcu", + "costs.liveRate": "anlık kur", + "costs.settleAll": "Tümünü hesaplaş", }; export default budget; diff --git a/shared/src/i18n/tr/reservations.ts b/shared/src/i18n/tr/reservations.ts index 5a3f647d..d4dd9165 100644 --- a/shared/src/i18n/tr/reservations.ts +++ b/shared/src/i18n/tr/reservations.ts @@ -28,6 +28,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Uçuş No.', 'reservations.meta.from': 'İtibaren', 'reservations.meta.to': 'İle', + 'reservations.layover.route': 'Rota', + 'reservations.layover.stop': 'Durak', + 'reservations.layover.addStop': 'Durak ekle', + 'reservations.layover.connection': 'Aktarma', + 'reservations.layover.layover': 'Aktarma bekleme süresi', 'reservations.needsReview': 'Gözden geçirmek', 'reservations.needsReviewHint': 'Havaalanı otomatik olarak eşleştirilemedi; lütfen konumu onaylayın.', diff --git a/shared/src/i18n/uk/budget.ts b/shared/src/i18n/uk/budget.ts index 15e50057..889940b5 100644 --- a/shared/src/i18n/uk/budget.ts +++ b/shared/src/i18n/uk/budget.ts @@ -39,78 +39,78 @@ const budget: TranslationStrings = { 'Натисніть на аватар учасника в рядку бюджету, щоб відзначити його зеленим — це означає, що він заплатив. Взаєморозрахунок покаже, хто кому і скільки винен.', 'budget.netBalances': 'Чисті баланси', 'budget.categoriesLabel': 'категорії', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "Ви", + "costs.youShort": "Ви", + "costs.youLower": "ви", + "costs.youOwe": "Ви винні", + "costs.youOweSub": "Ви маєте заплатити іншим", + "costs.youreOwed": "Вам винні", + "costs.youreOwedSub": "Інші мають заплатити вам", + "costs.totalSpend": "Загальні витрати поїздки", + "costs.totalSpendSub": "Серед усіх мандрівників", + "costs.to": "Кому", + "costs.from": "Від", + "costs.allSettled": "Усі розрахунки завершено", + "costs.nothingOwed": "Вам нічого не винні", + "costs.yourShare": "Ваша частка", + "costs.youPaid": "Ви заплатили", + "costs.expenses": "Витрати", + "costs.entries": "{count} записів", + "costs.searchPlaceholder": "Пошук витрат…", + "costs.filter.all": "Усі", + "costs.filter.mine": "Сплачено мною", + "costs.filter.owed": "Мені винні", + "costs.addExpense": "Додати витрату", + "costs.editExpense": "Редагувати витрату", + "costs.noMatch": "Жодна витрата не відповідає пошуку.", + "costs.emptyText": "Витрат ще немає. Додайте першу.", + "costs.spent": "Витрачено {amount}", + "costs.noDate": "Без дати", + "costs.noOnePaid": "Ще ніхто не заплатив", + "costs.youLent": "ви позичили {amount}", + "costs.youBorrowed": "ви заборгували {amount}", + "costs.settleUp": "Розрахуватися", + "costs.history": "Історія", + "costs.everyoneSquare": "Усі розрахувалися", + "costs.nothingOutstanding": "Зараз немає непогашених платежів.", + "costs.pay": "заплатити", + "costs.pays": "платить", + "costs.settle": "Розрахувати", + "costs.balances": "Баланси", + "costs.byCategory": "За категоріями", + "costs.noCategories": "Витрат ще немає.", + "costs.settleHistory": "Історія розрахунків", + "costs.noSettlements": "Розрахованих платежів ще немає.", + "costs.paymentsSettled": "Розраховано платежів: {count}", + "costs.paid": "сплачено", + "costs.undo": "Скасувати", + "costs.whatFor": "За що це було?", + "costs.namePlaceholder": "напр. вечеря, сувеніри, пальне…", + "costs.totalAmount": "Загальна сума", + "costs.currency": "Валюта", + "costs.day": "День", + "costs.rateLabel": "1 {from} у {to}", + "costs.category": "Категорія", + "costs.whoPaid": "Хто заплатив?", + "costs.splitBetween": "Розділити порівну між", + "costs.pickSomeone": "Виберіть хоча б одну особу для розподілу.", + "costs.splitSummary": "Розділено на {count} · по {amount}", + "costs.cat.accommodation": "Проживання", + "costs.cat.food": "Їжа та напої", + "costs.cat.groceries": "Продукти", + "costs.cat.transport": "Транспорт", + "costs.cat.flights": "Авіапереліт", + "costs.cat.activities": "Активності", + "costs.cat.sightseeing": "Огляд пам’яток", + "costs.cat.shopping": "Покупки", + "costs.cat.fees": "Збори та квитки", + "costs.cat.health": "Здоров’я", + "costs.cat.tips": "Чайові", + "costs.cat.other": "Інше", + "costs.daysCount": "{count} днів", + "costs.travelers": "{count} мандрівників", + "costs.liveRate": "поточний курс", + "costs.settleAll": "Розрахувати все", }; export default budget; diff --git a/shared/src/i18n/uk/reservations.ts b/shared/src/i18n/uk/reservations.ts index 984b5b47..454ca7b8 100644 --- a/shared/src/i18n/uk/reservations.ts +++ b/shared/src/i18n/uk/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': 'Номер рейсу', 'reservations.meta.from': 'Звідки', 'reservations.meta.to': 'Куди', + 'reservations.layover.route': 'Маршрут', + 'reservations.layover.stop': 'Зупинка', + 'reservations.layover.addStop': 'Додати зупинку', + 'reservations.layover.connection': 'Пересадка', + 'reservations.layover.layover': 'Очікування', 'reservations.needsReview': 'Перевірити', 'reservations.needsReviewHint': 'Аеропорт не вдалося визначити автоматично — підтвердіть місцезнаходження.', diff --git a/shared/src/i18n/zh-TW/budget.ts b/shared/src/i18n/zh-TW/budget.ts index a56e9a57..14aa7225 100644 --- a/shared/src/i18n/zh-TW/budget.ts +++ b/shared/src/i18n/zh-TW/budget.ts @@ -38,78 +38,78 @@ const budget: TranslationStrings = { '點選預算專案上的成員頭像將其標記為綠色——表示該成員已付款。結算會顯示誰欠誰多少。', 'budget.netBalances': '淨餘額', 'budget.categoriesLabel': '類別', - "costs.you": "You", + "costs.you": "你", "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.youLower": "你", + "costs.youOwe": "你欠款", + "costs.youOweSub": "你需付款給他人", + "costs.youreOwed": "他人欠你", + "costs.youreOwedSub": "他人需付款給你", + "costs.totalSpend": "旅程總支出", + "costs.totalSpendSub": "所有旅伴合計", + "costs.to": "給", + "costs.from": "來自", + "costs.allSettled": "你已全部結清", + "costs.nothingOwed": "沒有人欠你", + "costs.yourShare": "你的分攤", + "costs.youPaid": "你支付了", + "costs.expenses": "支出", + "costs.entries": "{count} 筆", + "costs.searchPlaceholder": "搜尋支出…", + "costs.filter.all": "全部", + "costs.filter.mine": "我支付的", + "costs.filter.owed": "他人欠我", + "costs.addExpense": "新增支出", + "costs.editExpense": "編輯支出", + "costs.noMatch": "沒有符合搜尋的支出。", + "costs.emptyText": "尚無支出,新增第一筆吧。", + "costs.spent": "支出 {amount}", + "costs.noDate": "無日期", + "costs.noOnePaid": "尚無人付款", + "costs.youLent": "你借出 {amount}", + "costs.youBorrowed": "你借入 {amount}", + "costs.settleUp": "結清", + "costs.history": "歷史紀錄", + "costs.everyoneSquare": "大家都已結清", + "costs.nothingOutstanding": "目前沒有待付款項。", + "costs.pay": "支付", + "costs.pays": "支付", + "costs.settle": "結算", + "costs.balances": "餘額", + "costs.byCategory": "按分類", + "costs.noCategories": "尚無支出。", + "costs.settleHistory": "結算紀錄", + "costs.noSettlements": "尚無已結清的款項。", + "costs.paymentsSettled": "已結清 {count} 筆款項", + "costs.paid": "已付", + "costs.undo": "復原", + "costs.whatFor": "這筆是什麼支出?", + "costs.namePlaceholder": "例如:晚餐、紀念品、油費…", + "costs.totalAmount": "總金額", + "costs.currency": "貨幣", + "costs.day": "日期", + "costs.rateLabel": "1 {from} 兌 {to}", + "costs.category": "分類", + "costs.whoPaid": "誰付的款?", + "costs.splitBetween": "平均分攤給", + "costs.pickSomeone": "至少選擇一人來分攤。", + "costs.splitSummary": "分 {count} 份 · 每份 {amount}", + "costs.cat.accommodation": "住宿", + "costs.cat.food": "餐飲", + "costs.cat.groceries": "雜貨", + "costs.cat.transport": "交通", + "costs.cat.flights": "機票", + "costs.cat.activities": "活動", + "costs.cat.sightseeing": "觀光", + "costs.cat.shopping": "購物", + "costs.cat.fees": "費用與票券", + "costs.cat.health": "健康", + "costs.cat.tips": "小費", + "costs.cat.other": "其他", + "costs.daysCount": "{count} 天", + "costs.travelers": "{count} 位旅伴", + "costs.liveRate": "即時匯率", + "costs.settleAll": "全部結清", }; export default budget; diff --git a/shared/src/i18n/zh-TW/reservations.ts b/shared/src/i18n/zh-TW/reservations.ts index 2f8a5dee..50482ba1 100644 --- a/shared/src/i18n/zh-TW/reservations.ts +++ b/shared/src/i18n/zh-TW/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': '航班號', 'reservations.meta.from': '出發', 'reservations.meta.to': '到達', + 'reservations.layover.route': '航線', + 'reservations.layover.stop': '中轉站', + 'reservations.layover.addStop': '新增中轉站', + 'reservations.layover.connection': '轉乘航班', + 'reservations.layover.layover': '轉機等候', 'reservations.needsReview': '待確認', 'reservations.needsReviewHint': '無法自動匹配機場 — 請確認位置。', 'reservations.searchLocation': '搜尋車站、港口、地址...', diff --git a/shared/src/i18n/zh/budget.ts b/shared/src/i18n/zh/budget.ts index c7a57c72..b2e93c31 100644 --- a/shared/src/i18n/zh/budget.ts +++ b/shared/src/i18n/zh/budget.ts @@ -38,78 +38,78 @@ const budget: TranslationStrings = { '点击预算项目上的成员头像将其标记为绿色——表示该成员已付款。结算会显示谁欠谁多少。', 'budget.netBalances': '净余额', 'budget.categoriesLabel': '类别', - "costs.you": "You", - "costs.youShort": "Y", - "costs.youLower": "you", - "costs.youOwe": "You owe", - "costs.youOweSub": "You should pay others", - "costs.youreOwed": "You're owed", - "costs.youreOwedSub": "Others should pay you", - "costs.totalSpend": "Total trip spend", - "costs.totalSpendSub": "Across all travelers", - "costs.to": "To", - "costs.from": "From", - "costs.allSettled": "You're all settled up", - "costs.nothingOwed": "Nothing owed to you", - "costs.yourShare": "Your share", - "costs.youPaid": "You paid", - "costs.expenses": "Expenses", - "costs.entries": "{count} entries", - "costs.searchPlaceholder": "Search expenses…", - "costs.filter.all": "All", - "costs.filter.mine": "Paid by me", - "costs.filter.owed": "I'm owed", - "costs.addExpense": "Add expense", - "costs.editExpense": "Edit expense", - "costs.noMatch": "No expenses match your search.", - "costs.emptyText": "No expenses yet. Add your first one.", - "costs.spent": "{amount} spent", - "costs.noDate": "No date", - "costs.noOnePaid": "No one paid yet", - "costs.youLent": "you lent {amount}", - "costs.youBorrowed": "you borrowed {amount}", - "costs.settleUp": "Settle up", - "costs.history": "History", - "costs.everyoneSquare": "Everyone's square", - "costs.nothingOutstanding": "No payments outstanding right now.", - "costs.pay": "pay", - "costs.pays": "pays", - "costs.settle": "Settle", - "costs.balances": "Balances", - "costs.byCategory": "By category", - "costs.noCategories": "No expenses yet.", - "costs.settleHistory": "Settle history", - "costs.noSettlements": "No settled payments yet.", - "costs.paymentsSettled": "{count} payments settled", - "costs.paid": "paid", - "costs.undo": "Undo", - "costs.whatFor": "What was it for?", - "costs.namePlaceholder": "e.g. Dinner, souvenirs, gas…", - "costs.totalAmount": "Total amount", - "costs.currency": "Currency", - "costs.day": "Day", - "costs.rateLabel": "1 {from} in {to}", - "costs.category": "Category", - "costs.whoPaid": "Who paid?", - "costs.splitBetween": "Split equally between", - "costs.pickSomeone": "Pick at least one person to split with.", - "costs.splitSummary": "Split {count} ways · {amount} each", - "costs.cat.accommodation": "Accommodation", - "costs.cat.food": "Food & drink", - "costs.cat.groceries": "Groceries", - "costs.cat.transport": "Transport", - "costs.cat.flights": "Flights", - "costs.cat.activities": "Activities", - "costs.cat.sightseeing": "Sightseeing", - "costs.cat.shopping": "Shopping", - "costs.cat.fees": "Fees & tickets", - "costs.cat.health": "Health", - "costs.cat.tips": "Tips", - "costs.cat.other": "Other", - "costs.daysCount": "{count} days", - "costs.travelers": "{count} travelers", - "costs.liveRate": "live rate", - "costs.settleAll": "Settle all", + "costs.you": "你", + "costs.youShort": "你", + "costs.youLower": "你", + "costs.youOwe": "你欠款", + "costs.youOweSub": "你需要付钱给其他人", + "costs.youreOwed": "别人欠你", + "costs.youreOwedSub": "其他人需要付钱给你", + "costs.totalSpend": "行程总支出", + "costs.totalSpendSub": "所有同行者合计", + "costs.to": "收款方", + "costs.from": "付款方", + "costs.allSettled": "你已全部结清", + "costs.nothingOwed": "没有人欠你", + "costs.yourShare": "你的份额", + "costs.youPaid": "你已支付", + "costs.expenses": "支出", + "costs.entries": "{count} 条记录", + "costs.searchPlaceholder": "搜索支出…", + "costs.filter.all": "全部", + "costs.filter.mine": "我支付的", + "costs.filter.owed": "别人欠我", + "costs.addExpense": "添加支出", + "costs.editExpense": "编辑支出", + "costs.noMatch": "没有符合搜索条件的支出。", + "costs.emptyText": "暂无支出。添加第一笔吧。", + "costs.spent": "已花费 {amount}", + "costs.noDate": "无日期", + "costs.noOnePaid": "尚无人支付", + "costs.youLent": "你垫付了 {amount}", + "costs.youBorrowed": "你欠了 {amount}", + "costs.settleUp": "结算", + "costs.history": "历史记录", + "costs.everyoneSquare": "大家已两清", + "costs.nothingOutstanding": "目前没有待结算的款项。", + "costs.pay": "支付", + "costs.pays": "支付", + "costs.settle": "结算", + "costs.balances": "余额", + "costs.byCategory": "按分类", + "costs.noCategories": "暂无支出。", + "costs.settleHistory": "结算历史", + "costs.noSettlements": "暂无已结算的款项。", + "costs.paymentsSettled": "已结算 {count} 笔付款", + "costs.paid": "已支付", + "costs.undo": "撤销", + "costs.whatFor": "这笔花在哪了?", + "costs.namePlaceholder": "例如:晚餐、纪念品、油费…", + "costs.totalAmount": "总金额", + "costs.currency": "货币", + "costs.day": "日期", + "costs.rateLabel": "1 {from} = {to}", + "costs.category": "分类", + "costs.whoPaid": "谁支付的?", + "costs.splitBetween": "平均分摊给", + "costs.pickSomeone": "至少选择一人参与分摊。", + "costs.splitSummary": "分 {count} 份 · 每份 {amount}", + "costs.cat.accommodation": "住宿", + "costs.cat.food": "餐饮", + "costs.cat.groceries": "采买", + "costs.cat.transport": "交通", + "costs.cat.flights": "机票", + "costs.cat.activities": "活动", + "costs.cat.sightseeing": "观光", + "costs.cat.shopping": "购物", + "costs.cat.fees": "费用与门票", + "costs.cat.health": "健康", + "costs.cat.tips": "小费", + "costs.cat.other": "其他", + "costs.daysCount": "{count} 天", + "costs.travelers": "{count} 位同行者", + "costs.liveRate": "实时汇率", + "costs.settleAll": "全部结算", }; export default budget; diff --git a/shared/src/i18n/zh/reservations.ts b/shared/src/i18n/zh/reservations.ts index facd1084..298bafbb 100644 --- a/shared/src/i18n/zh/reservations.ts +++ b/shared/src/i18n/zh/reservations.ts @@ -27,6 +27,11 @@ const reservations: TranslationStrings = { 'reservations.meta.flightNumber': '航班号', 'reservations.meta.from': '出发', 'reservations.meta.to': '到达', + 'reservations.layover.route': '航线', + 'reservations.layover.stop': '经停', + 'reservations.layover.addStop': '添加经停', + 'reservations.layover.connection': '转机', + 'reservations.layover.layover': '中转停留', 'reservations.needsReview': '待确认', 'reservations.needsReviewHint': '无法自动匹配机场 — 请确认位置。', 'reservations.searchLocation': '搜索车站、港口、地址...',