feat: optimize routes around accommodation, confirm note deletions (#1123)

Optimize day routes around the accommodation

When a day has an accommodation set, the route optimizer now treats it as
the day's home base: it optimizes a loop that leaves the hotel and returns
to it, so the stop nearest the hotel comes first. On a transfer day -
checking out of one hotel and into another - the route runs from the first
hotel to the second instead.

The optimizer also gained a 2-opt pass on top of the nearest-neighbor
ordering, which removes the crossings the greedy pass used to leave behind.
A new display setting ("optimize route from accommodation", on by default)
lets you turn the anchoring off.

Confirm before deleting notes

Deleting a plan note or a collab note now asks for confirmation first. On
phones and tablets the edit and delete icons sit close together and were
easy to mis-tap, which deleted notes with no way back.
This commit is contained in:
Maurice
2026-06-07 12:52:06 +02:00
committed by GitHub
parent 093e069ccc
commit 49b3af8b0d
72 changed files with 504 additions and 26 deletions
@@ -175,7 +175,7 @@ describe('CollabNotes', () => {
expect(document.body).toBeInTheDocument();
});
it('FE-COMP-NOTES-013: delete note calls DELETE API and removes it from grid', async () => {
it('FE-COMP-NOTES-013: deleting a note asks for confirmation, then calls DELETE API and removes it', async () => {
const user = userEvent.setup();
server.use(
http.get('/api/trips/1/collab/notes', () =>
@@ -193,8 +193,11 @@ describe('CollabNotes', () => {
);
render(<CollabNotes {...defaultProps} />);
await screen.findByText('Remove Me');
const deleteBtn = screen.getByTitle('Delete');
await user.click(deleteBtn);
await user.click(screen.getByTitle('Delete'));
// Deleting now asks for confirmation first — the note stays until confirmed.
expect(screen.getByText('Delete note?')).toBeInTheDocument();
expect(screen.getByText('Remove Me')).toBeInTheDocument();
await user.click(document.querySelector('button.bg-red-600') as HTMLElement);
await waitFor(() => expect(screen.queryByText('Remove Me')).not.toBeInTheDocument());
});
+15 -2
View File
@@ -10,6 +10,7 @@ import { useTripStore } from '../../store/tripStore'
import { addListener, removeListener } from '../../api/websocket'
import { useTranslation } from '../../i18n'
import { useToast } from '../shared/Toast'
import ConfirmDialog from '../shared/ConfirmDialog'
import type { User } from '../../types'
import type { CollabNote } from './CollabNotes.types'
import { FONT, NOTE_COLORS } from './CollabNotes.constants'
@@ -44,6 +45,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
const [previewFile, setPreviewFile] = useState(null)
const [showSettings, setShowSettings] = useState(false)
const [activeCategory, setActiveCategory] = useState(null)
const [pendingDeleteNoteId, setPendingDeleteNoteId] = useState<number | null>(null)
// Empty categories (no notes yet) stored in localStorage
const [emptyCategories, setEmptyCategories] = useState(() => {
@@ -231,6 +233,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) {
activeCategory, setActiveCategory, categoryColors, getCategoryColor,
handleCreateNote, handleUpdateNote, saveCategoryColors, handleEditSubmit,
handleDeleteNoteFile, handleDeleteNote, categories, sortedNotes,
pendingDeleteNoteId, setPendingDeleteNoteId,
}
}
@@ -319,7 +322,7 @@ function CollabCategoryPills({ categories, activeCategory, setActiveCategory, t
function CollabNotesGrid(S: NotesState) {
const {
sortedNotes, currentUser, canEdit, handleUpdateNote, handleDeleteNote,
sortedNotes, currentUser, canEdit, handleUpdateNote, setPendingDeleteNoteId,
setEditingNote, setViewingNote, setPreviewFile, getCategoryColor, tripId, t,
} = S
return (
@@ -352,7 +355,7 @@ function CollabNotesGrid(S: NotesState) {
currentUser={currentUser}
canEdit={canEdit}
onUpdate={handleUpdateNote}
onDelete={handleDeleteNote}
onDelete={setPendingDeleteNoteId}
onEdit={setEditingNote}
onView={setViewingNote}
onPreviewFile={setPreviewFile}
@@ -470,6 +473,7 @@ export default function CollabNotes(props: CollabNotesProps) {
viewingNote, showNewModal, editingNote, previewFile, showSettings,
setShowNewModal, setEditingNote, setPreviewFile, setShowSettings,
handleCreateNote, handleEditSubmit, handleDeleteNoteFile, saveCategoryColors, handleUpdateNote,
handleDeleteNote, pendingDeleteNoteId, setPendingDeleteNoteId,
} = S
if (loading) return <CollabNotesLoading {...S} />
@@ -527,6 +531,15 @@ export default function CollabNotes(props: CollabNotesProps) {
t={t}
/>
)}
{/* Confirm: delete a collab note — guards against accidental deletion */}
<ConfirmDialog
isOpen={pendingDeleteNoteId !== null}
onClose={() => setPendingDeleteNoteId(null)}
onConfirm={() => { if (pendingDeleteNoteId !== null) handleDeleteNote(pendingDeleteNoteId) }}
title={t('collab.notes.confirmDeleteTitle')}
message={t('collab.notes.confirmDeleteBody')}
/>
</div>
)
}
@@ -16,7 +16,7 @@ interface NoteCardProps {
currentUser: User
canEdit: boolean
onUpdate: (noteId: number, data: Partial<CollabNote>) => Promise<void>
onDelete: (noteId: number) => Promise<void>
onDelete: (noteId: number) => void
onEdit: (note: CollabNote) => void
onView: (note: CollabNote) => void
onPreviewFile: (file: NoteFile) => void
@@ -161,6 +161,62 @@ describe('optimizeRoute', () => {
expect(result[1]).toEqual(c)
expect(result[2]).toEqual(b)
})
it('FE-COMP-ROUTECALCULATOR-016: start anchor begins the chain at the anchor-nearest stop', () => {
const a = { lat: 10, lng: 1 }
const b = { lat: 2, lng: 1 }
const c = { lat: 5, lng: 1 }
// From the accommodation anchor (1,1): nearest is b(2,1), then c(5,1), then a(10,1)
const result = optimizeRoute([a, b, c], { start: { lat: 1, lng: 1 } })
expect(result).toEqual([b, c, a])
})
it('FE-COMP-ROUTECALCULATOR-017: start + end anchors reorder a shuffled day and keep the end-nearest stop last', () => {
const a = { lat: 2, lng: 1 }
const b = { lat: 5, lng: 1 }
const c = { lat: 8, lng: 1 }
// Transfer day: start at hotel A (1,1), end at hotel B (9,1). c is nearest B, so it must be last.
const result = optimizeRoute([c, a, b], { start: { lat: 1, lng: 1 }, end: { lat: 9, lng: 1 } })
expect(result).toEqual([a, b, c])
})
it('FE-COMP-ROUTECALCULATOR-018: an anchor makes even a two-stop day sortable', () => {
const a = { lat: 10, lng: 1 }
const b = { lat: 2, lng: 1 }
// Without anchors two stops are returned unchanged; the start anchor orders them by proximity.
const result = optimizeRoute([a, b], { start: { lat: 1, lng: 1 } })
expect(result).toEqual([b, a])
})
it('FE-COMP-ROUTECALCULATOR-019: 2-opt untangles a round-trip into a clean loop around the hotel', () => {
const hotel = { lat: 48.8668, lng: 2.3013 } // Rue Marbeuf
const stops = [
{ id: 1, lat: 48.8565, lng: 2.3324 },
{ id: 2, lat: 48.8813, lng: 2.3151 },
{ id: 3, lat: 48.8796, lng: 2.308 },
{ id: 4, lat: 48.8723, lng: 2.2926 },
{ id: 5, lat: 48.866, lng: 2.3102 }, // nearest the hotel
]
const d = (a: { lat: number; lng: number }, b: { lat: number; lng: number }) =>
Math.hypot(a.lat - b.lat, a.lng - b.lng)
const loop = (order: typeof stops) =>
d(hotel, order[0]) + order.slice(1).reduce((s, p, i) => s + d(order[i], p), 0) + d(order[order.length - 1], hotel)
const result = optimizeRoute(stops, { start: hotel, end: hotel })
// The optimized loop is no longer than the original order…
expect(loop(result)).toBeLessThanOrEqual(loop(stops) + 1e-9)
// …and the hotel-adjacent stop sits at one end of the loop, right next to the hotel.
expect([result[0].id, result[result.length - 1].id]).toContain(5)
})
it('FE-COMP-ROUTECALCULATOR-020: an end anchor without a start finishes at the stop nearest it', () => {
const a = { lat: 2, lng: 1 }
const b = { lat: 5, lng: 1 }
const c = { lat: 9, lng: 1 }
// a is nearest the end anchor, so the route must finish at a rather than start there.
const result = optimizeRoute([a, b, c], { end: { lat: 1, lng: 1 } })
expect(result[result.length - 1]).toEqual(a)
})
})
// ── generateGoogleMapsUrl ──────────────────────────────────────────────────────
+76 -13
View File
@@ -1,4 +1,4 @@
import type { RouteResult, RouteSegment, RouteWithLegs, Waypoint } from '../../types'
import type { RouteResult, RouteSegment, RouteWithLegs, Waypoint, RouteAnchors } from '../../types'
const OSRM_BASE = 'https://router.project-osrm.org/route/v1'
@@ -77,35 +77,98 @@ export function generateGoogleMapsUrl(places: Waypoint[]): string | null {
return `https://www.google.com/maps/dir/${stops}`
}
/** Reorders waypoints using a nearest-neighbor heuristic to minimize total Euclidean distance. */
export function optimizeRoute<T extends Waypoint>(places: T[]): T[] {
const valid = places.filter((p) => p.lat && p.lng)
if (valid.length <= 2) return places
// Squared planar distance — enough for nearest-neighbor comparisons and cheaper than a full haversine.
function sqDist(a: Waypoint, b: Waypoint): number {
return (a.lat - b.lat) ** 2 + (a.lng - b.lng) ** 2
}
// Length of visiting `order` in sequence, optionally pinned to a fixed start and/or end anchor.
// With start === end this is a closed loop back to the anchor (a day out from and back to the hotel).
function tourLength(order: Waypoint[], start?: Waypoint, end?: Waypoint): number {
if (order.length === 0) return 0
let total = 0
if (start) total += Math.sqrt(sqDist(start, order[0]))
for (let i = 0; i < order.length - 1; i++) total += Math.sqrt(sqDist(order[i], order[i + 1]))
if (end) total += Math.sqrt(sqDist(order[order.length - 1], end))
return total
}
// Greedy nearest-neighbor ordering, seeded at the start anchor when there is one.
function nearestNeighborOrder<T extends Waypoint>(valid: T[], start?: Waypoint): T[] {
const visited = new Set<number>()
const result: T[] = []
let current = valid[0]
visited.add(0)
result.push(current)
let current: Waypoint
if (start) {
current = start
} else {
current = valid[0]
visited.add(0)
result.push(valid[0])
}
while (result.length < valid.length) {
let nearestIdx = -1
let minDist = Infinity
for (let i = 0; i < valid.length; i++) {
if (visited.has(i)) continue
const d = Math.sqrt(
Math.pow(valid[i].lat - current.lat, 2) + Math.pow(valid[i].lng - current.lng, 2)
)
const d = sqDist(valid[i], current)
if (d < minDist) { minDist = d; nearestIdx = i }
}
if (nearestIdx === -1) break
visited.add(nearestIdx)
current = valid[nearestIdx]
result.push(current)
result.push(valid[nearestIdx])
}
return result
}
// 2-opt: repeatedly reverse a sub-segment whenever it shortens the tour. This removes the crossings
// a pure nearest-neighbor pass leaves behind. The start/end anchors stay fixed, so a round trip
// (start === end) is untangled into a clean loop rather than an open path.
function twoOptImprove<T extends Waypoint>(order: T[], start?: Waypoint, end?: Waypoint): T[] {
if (order.length < 3) return order
let best = order
let bestLen = tourLength(best, start, end)
let improved = true
while (improved) {
improved = false
for (let i = 0; i < best.length - 1; i++) {
for (let j = i + 1; j < best.length; j++) {
const candidate = best.slice(0, i).concat(best.slice(i, j + 1).reverse(), best.slice(j + 1))
const len = tourLength(candidate, start, end)
if (len < bestLen - 1e-12) {
best = candidate
bestLen = len
improved = true
}
}
}
}
return best
}
/**
* Reorders waypoints to minimize travel distance: a nearest-neighbor pass for a good starting order,
* then 2-opt to untangle crossings. Optional anchors (e.g. the day's accommodation) pin the route's
* ends — start === end makes it a loop out from and back to the hotel; a transfer day runs start → end.
*/
export function optimizeRoute<T extends Waypoint>(places: T[], anchors: RouteAnchors = {}): T[] {
const { start, end } = anchors
const valid = places.filter((p) => p.lat && p.lng)
if (valid.length <= 1) return places
// Two unanchored stops have no meaningful order to optimize; anchors can still flip them.
if (valid.length === 2 && !start && !end) return places
const order = twoOptImprove(nearestNeighborOrder(valid, start), start, end)
// A round trip's loop direction is arbitrary, so orient it to begin at the stop nearest the hotel —
// that reads naturally as "leave the hotel, head to the closest place, …, come back".
if (start && end && start.lat === end.lat && start.lng === end.lng && order.length > 1) {
if (sqDist(order[order.length - 1], start) < sqDist(order[0], start)) order.reverse()
}
return order
}
/** Fetches per-leg distance/duration from OSRM and returns segment metadata (midpoints, walking/driving times). */
export async function calculateSegments(
waypoints: Waypoint[],
@@ -982,7 +982,7 @@ describe('DayPlanSidebar', () => {
}
})
it('FE-PLANNER-DAYPLAN-065: note card delete button calls deleteNote', async () => {
it('FE-PLANNER-DAYPLAN-065: deleting a note asks for confirmation before calling deleteNote', async () => {
const user = userEvent.setup()
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
const note = buildDayNote({ id: 55, day_id: 10, text: 'My note' })
@@ -992,6 +992,11 @@ describe('DayPlanSidebar', () => {
const noteEditBtns = document.querySelectorAll('.note-edit-buttons button')
if (noteEditBtns.length > 1) {
await user.click(noteEditBtns[1] as HTMLElement)
// Clicking delete opens a confirmation dialog rather than deleting immediately.
expect(mockDayNotesState.deleteNote).not.toHaveBeenCalled()
expect(screen.getByText('Delete note?')).toBeInTheDocument()
// Confirming triggers the actual delete.
await user.click(screen.getByRole('button', { name: /^delete$/i }))
expect(mockDayNotesState.deleteNote).toHaveBeenCalled()
}
})
@@ -7,6 +7,7 @@ import { ChevronDown, ChevronRight, ChevronUp, Navigation, RotateCcw, ExternalLi
import { assignmentsApi, reservationsApi } from '../../api/client'
import { calculateRoute, calculateRouteWithLegs, optimizeRoute } from '../Map/RouteCalculator'
import PlaceAvatar from '../shared/PlaceAvatar'
import ConfirmDialog from '../shared/ConfirmDialog'
import { useContextMenu, ContextMenu } from '../shared/ContextMenu'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
@@ -17,7 +18,7 @@ import { useTripStore } from '../../store/tripStore'
import { useCanDo } from '../../store/permissionsStore'
import { useSettingsStore } from '../../store/settingsStore'
import { useTranslation } from '../../i18n'
import { isDayInAccommodationRange } from '../../utils/dayOrder'
import { isDayInAccommodationRange, getAccommodationAnchors } from '../../utils/dayOrder'
import {
TRANSPORT_TYPES, parseTimeToMinutes, getSpanPhase, getDisplayTimeForDay,
getTransportForDay as _getTransportForDay, getMergedItems as _getMergedItems,
@@ -451,6 +452,10 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
_openEditNote(dayId, note)
}
// Deleting a note asks for confirmation first — the edit/delete icons sit close together and are
// easy to mis-tap on touch devices, where an accidental delete was previously unrecoverable.
const [pendingDeleteNote, setPendingDeleteNote] = useState<{ dayId: number; noteId: number } | null>(null)
const deleteNote = async (dayId: number, noteId: number, e?: React.MouseEvent) => {
e?.stopPropagation()
await _deleteNote(dayId, noteId)
@@ -703,8 +708,14 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
// Optimize only unlocked assignments (work on assignments, not places)
const unlockedWithCoords = unlocked.filter(a => a.place?.lat && a.place?.lng)
const unlockedNoCoords = unlocked.filter(a => !a.place?.lat || !a.place?.lng)
// Anchor the route on the day's accommodation (when enabled): a loop out from and back to the
// hotel, or — on a transfer day — a run from the hotel you leave to the one you arrive at.
const day = days.find(d => d.id === selectedDayId)
const anchors = day && useSettingsStore.getState().settings.optimize_from_accommodation !== false
? getAccommodationAnchors(day, days, accommodations)
: {}
const optimizedAssignments = unlockedWithCoords.length >= 2
? optimizeRoute(unlockedWithCoords.map(a => ({ ...a.place, _assignmentId: a.id }))).map(p => unlockedWithCoords.find(a => a.id === p._assignmentId)).filter(Boolean)
? optimizeRoute(unlockedWithCoords.map(a => ({ ...a.place, _assignmentId: a.id })), anchors).map(p => unlockedWithCoords.find(a => a.id === p._assignmentId)).filter(Boolean)
: unlockedWithCoords
const optimizedQueue = [...optimizedAssignments, ...unlockedNoCoords]
@@ -717,7 +728,8 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
}
await onReorder(selectedDayId, result.map(a => a.id))
toast.success(t('dayplan.toast.routeOptimized'))
const usedHotel = !!(anchors.start || anchors.end)
toast.success(usedHotel ? t('dayplan.toast.routeOptimizedFromHotel') : t('dayplan.toast.routeOptimized'))
const capturedDayId = selectedDayId
pushUndo?.(t('undo.optimize'), async () => {
await tripActions.reorderAssignments(tripId, capturedDayId, prevIds)
@@ -851,6 +863,8 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
cancelNote,
saveNote,
deleteNote,
pendingDeleteNote,
setPendingDeleteNote,
moveNote,
expandedDays,
setExpandedDays,
@@ -993,6 +1007,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
cancelNote,
saveNote,
deleteNote,
pendingDeleteNote,
setPendingDeleteNote,
moveNote,
expandedDays,
setExpandedDays,
@@ -1908,7 +1924,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
onContextMenu={canEditDays ? e => ctxMenu.open(e, [
{ label: t('common.edit'), icon: Pencil, onClick: () => openEditNote(day.id, note) },
{ divider: true },
{ label: t('common.delete'), icon: Trash2, danger: true, onClick: () => deleteNote(day.id, note.id) },
{ label: t('common.delete'), icon: Trash2, danger: true, onClick: () => setPendingDeleteNote({ dayId: day.id, noteId: note.id }) },
]) : undefined}
onMouseEnter={e => {
const grip = e.currentTarget.querySelector('.dp-grip') as HTMLElement | null
@@ -1950,7 +1966,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
</div>
{canEditDays && <div className="note-edit-buttons" style={{ display: 'flex', gap: 1, flexShrink: 0, opacity: 0, transition: 'opacity 0.15s' }}>
<button onClick={e => openEditNote(day.id, note, e)} className="text-content-faint" style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }}><Pencil size={10} /></button>
<button onClick={e => deleteNote(day.id, note.id, e)} className="text-content-faint" style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }}><Trash2 size={10} /></button>
<button onClick={e => { e.stopPropagation(); setPendingDeleteNote({ dayId: day.id, noteId: note.id }) }} className="text-content-faint" style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }}><Trash2 size={10} /></button>
</div>}
{canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}>
<button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'up') }} disabled={noteIdx === 0} className={noteIdx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronUp size={12} strokeWidth={2} /></button>
@@ -2093,6 +2109,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
t={t}
/>
{/* Confirm: delete a day note — guards against accidental taps on touch devices */}
<ConfirmDialog
isOpen={!!pendingDeleteNote}
onClose={() => setPendingDeleteNote(null)}
onConfirm={() => { if (pendingDeleteNote) deleteNote(pendingDeleteNote.dayId, pendingDeleteNote.noteId) }}
title={t('dayplan.confirmDeleteNoteTitle')}
message={t('dayplan.confirmDeleteNoteBody')}
/>
{/* Transport-Detail-Modal */}
<DayPlanSidebarTransportDetailModal
transportDetail={transportDetail}
@@ -291,6 +291,37 @@ export default function DisplaySettingsTab(): React.ReactElement {
))}
</div>
</div>
{/* Optimize route from accommodation */}
<div>
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.optimizeFromAccommodation')}</label>
<div className="flex gap-3">
{[
{ value: true, label: t('settings.on') || 'On' },
{ value: false, label: t('settings.off') || 'Off' },
].map(opt => (
<button
key={String(opt.value)}
onClick={async () => {
try { await updateSetting('optimize_from_accommodation', opt.value) }
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
}}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 20px', borderRadius: 10, cursor: 'pointer',
fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
border: (settings.optimize_from_accommodation !== false) === opt.value ? '2px solid var(--text-primary)' : '2px solid var(--border-primary)',
background: (settings.optimize_from_accommodation !== false) === opt.value ? 'var(--bg-hover)' : 'var(--bg-card)',
color: 'var(--text-primary)',
transition: 'all 0.15s',
}}
>
{opt.label}
</button>
))}
</div>
<p className="text-xs mt-1 text-content-faint">{t('settings.optimizeFromAccommodationHint')}</p>
</div>
</Section>
)
}
+1
View File
@@ -32,6 +32,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
temperature_unit: 'fahrenheit',
time_format: '12h',
show_place_description: false,
optimize_from_accommodation: true,
map_provider: 'leaflet',
mapbox_access_token: '',
mapbox_style: 'mapbox://styles/mapbox/standard',
+7
View File
@@ -113,6 +113,7 @@ export interface Settings {
show_place_description: boolean
blur_booking_codes?: boolean
map_booking_labels?: boolean
optimize_from_accommodation?: boolean
map_provider?: 'leaflet' | 'mapbox-gl'
mapbox_access_token?: string
mapbox_style?: string
@@ -162,6 +163,12 @@ export interface Waypoint {
lng: number
}
// Optional fixed start/end points for route optimization (e.g. the day's accommodation).
export interface RouteAnchors {
start?: Waypoint
end?: Waypoint
}
// User with optional OIDC fields
export interface UserWithOidc extends User {
oidc_issuer?: string | null
+73
View File
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest'
import type { Day, Accommodation } from '../types'
import { getDayOrder, isDayInAccommodationRange, getAccommodationAnchors } from './dayOrder'
const days = [
{ id: 10, day_number: 1 },
{ id: 20, day_number: 2 },
{ id: 30, day_number: 3 },
] as unknown as Day[]
const hotel = (over: Partial<Accommodation>): Accommodation =>
({ place_lat: 48.1, place_lng: 11.5, start_day_id: 10, end_day_id: 30, ...over }) as Accommodation
describe('getDayOrder', () => {
it('prefers day_number when present', () => {
expect(getDayOrder(days[1], days)).toBe(2)
})
it('falls back to array index when day_number is missing', () => {
const noNumber = [{ id: 5 }, { id: 6 }] as unknown as Day[]
expect(getDayOrder(noNumber[1], noNumber)).toBe(1)
})
})
describe('isDayInAccommodationRange', () => {
it('is inclusive of both the check-in and check-out day', () => {
expect(isDayInAccommodationRange(days[0], 10, 30, days)).toBe(true) // check-in morning
expect(isDayInAccommodationRange(days[1], 10, 30, days)).toBe(true) // mid-stay
expect(isDayInAccommodationRange(days[2], 10, 30, days)).toBe(true) // check-out day
})
it('excludes days outside the stay', () => {
expect(isDayInAccommodationRange(days[0], 20, 30, days)).toBe(false)
})
})
describe('getAccommodationAnchors', () => {
it('returns no anchors when the day has no accommodation', () => {
expect(getAccommodationAnchors(days[1], days, [])).toEqual({})
})
it('anchors both ends to the same hotel on a mid-stay day (round trip)', () => {
const accs = [hotel({ start_day_id: 10, end_day_id: 30, place_lat: 48.1, place_lng: 11.5 })]
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({
start: { lat: 48.1, lng: 11.5 },
end: { lat: 48.1, lng: 11.5 },
})
})
it('loops a single hotel on its check-out day (home base for the day)', () => {
const accs = [hotel({ start_day_id: 10, end_day_id: 20, place_lat: 1, place_lng: 2 })]
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ start: { lat: 1, lng: 2 }, end: { lat: 1, lng: 2 } })
})
it('loops a single hotel on its check-in day (home base for the day)', () => {
const accs = [hotel({ start_day_id: 20, end_day_id: 30, place_lat: 3, place_lng: 4 })]
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ start: { lat: 3, lng: 4 }, end: { lat: 3, lng: 4 } })
})
it('uses the checked-out hotel as start and the checked-in hotel as end on a transfer day', () => {
const accs = [
hotel({ start_day_id: 10, end_day_id: 20, place_lat: 1, place_lng: 1 }), // checkout today
hotel({ start_day_id: 20, end_day_id: 30, place_lat: 9, place_lng: 9 }), // check-in today
]
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({
start: { lat: 1, lng: 1 },
end: { lat: 9, lng: 9 },
})
})
it('ignores accommodations that have no coordinates', () => {
const accs = [hotel({ start_day_id: 10, end_day_id: 30, place_lat: null, place_lng: null })]
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({})
})
})
+27 -1
View File
@@ -1,8 +1,34 @@
import type { Day } from '../types'
import type { Day, Accommodation, RouteAnchors } from '../types'
export const getDayOrder = (day: Day, days: Day[]): number =>
day.day_number ?? days.indexOf(day)
// Derives route anchors from the accommodation(s) active on a day. A single hotel is the day's home
// base, so the route is a loop that starts and ends there. A transfer day — checking out of one hotel
// and into another — instead runs from the morning hotel to the evening one.
export const getAccommodationAnchors = (
day: Day,
days: Day[],
accommodations: Accommodation[],
): RouteAnchors => {
const located = accommodations.filter(a =>
a.place_lat != null && a.place_lng != null &&
isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days),
)
if (located.length === 0) return {}
const toAnchor = (a: Accommodation) => ({ lat: a.place_lat as number, lng: a.place_lng as number })
const checkOut = located.find(a => a.end_day_id === day.id) // the hotel you leave this morning
const checkIn = located.find(a => a.start_day_id === day.id) // the hotel you arrive at tonight
if (checkOut && checkIn && checkOut !== checkIn) {
return { start: toAnchor(checkOut), end: toAnchor(checkIn) }
}
const hotel = toAnchor(located[0])
return { start: hotel, end: hotel }
}
export const isDayInAccommodationRange = (
day: Day,
startDayId: number,
+2
View File
@@ -39,6 +39,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'إلغاء',
'collab.notes.edit': 'تعديل',
'collab.notes.delete': 'حذف',
'collab.notes.confirmDeleteTitle': 'حذف الملاحظة؟',
'collab.notes.confirmDeleteBody': 'سيتم حذف هذه الملاحظة نهائيًا.',
'collab.notes.pin': 'تثبيت',
'collab.notes.unpin': 'إلغاء التثبيت',
'collab.notes.daysAgo': 'منذ {n} يوم',
+3
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل',
'dayplan.confirmDeleteNoteTitle': 'حذف الملاحظة؟',
'dayplan.confirmDeleteNoteBody': 'سيتم حذف هذه الملاحظة نهائيًا.',
'dayplan.cannotDropOnTimed':
'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت',
'dayplan.cannotBreakChronology':
@@ -29,6 +31,7 @@ const dayplan: TranslationStrings = {
'dayplan.routeError': 'فشل حساب المسار',
'dayplan.toast.needTwoPlaces': 'يلزم مكانان على الأقل لتحسين المسار',
'dayplan.toast.routeOptimized': 'تم تحسين المسار',
'dayplan.toast.routeOptimizedFromHotel': 'تم تحسين المسار انطلاقًا من مكان إقامتك',
'dayplan.toast.noGeoPlaces': 'لم يتم العثور على أماكن بإحداثيات لحساب المسار',
'dayplan.confirmed': 'مؤكد',
'dayplan.pendingRes': 'قيد الانتظار',
+3
View File
@@ -60,6 +60,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
'settings.blurBookingCodes': 'إخفاء رموز الحجز',
'settings.optimizeFromAccommodation': 'تحسين المسار انطلاقًا من مكان الإقامة',
'settings.optimizeFromAccommodationHint':
'عند تحسين يوم ما، يبدأ المسار من الفندق الذي تستيقظ فيه وينتهي عند الفندق الذي تسجّل الوصول إليه في تلك الليلة.',
'settings.notifications': 'الإشعارات',
'settings.notifyTripInvite': 'دعوات الرحلات',
'settings.notifyBookingChange': 'تغييرات الحجز',
+2
View File
@@ -41,6 +41,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Cancelar',
'collab.notes.edit': 'Editar',
'collab.notes.delete': 'Excluir',
'collab.notes.confirmDeleteTitle': 'Excluir nota?',
'collab.notes.confirmDeleteBody': 'Esta nota será excluída permanentemente.',
'collab.notes.pin': 'Fixar',
'collab.notes.unpin': 'Desafixar',
'collab.notes.daysAgo': 'há {n} d',
+4
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'São necessários pelo menos dois lugares para otimizar a rota',
'dayplan.toast.routeOptimized': 'Rota otimizada',
'dayplan.toast.routeOptimizedFromHotel':
'Rota otimizada a partir da sua hospedagem',
'dayplan.toast.noGeoPlaces':
'Nenhum lugar com coordenadas para calcular a rota',
'dayplan.confirmed': 'Confirmada',
@@ -33,6 +35,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Este lugar tem um horário fixo ({time}). Movê-lo removerá o horário e permitirá ordenação livre.',
'dayplan.confirmRemoveTimeAction': 'Remover horário e mover',
'dayplan.confirmDeleteNoteTitle': 'Excluir nota?',
'dayplan.confirmDeleteNoteBody': 'Esta nota será excluída permanentemente.',
'dayplan.cannotDropOnTimed':
'Itens não podem ser colocados entre entradas com horário fixo',
'dayplan.cannotBreakChronology':
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Unidade de temperatura',
'settings.timeFormat': 'Formato de hora',
'settings.blurBookingCodes': 'Ocultar códigos de reserva',
'settings.optimizeFromAccommodation': 'Otimizar rota a partir da hospedagem',
'settings.optimizeFromAccommodationHint':
'Ao otimizar um dia, comece a rota no hotel onde você acorda e termine no hotel em que você faz check-in à noite.',
'settings.notifications': 'Notificações',
'settings.notifyTripInvite': 'Convites de viagem',
'settings.notifyBookingChange': 'Alterações de reserva',
+2
View File
@@ -36,6 +36,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Zrušit',
'collab.notes.edit': 'Upravit',
'collab.notes.delete': 'Smazat',
'collab.notes.confirmDeleteTitle': 'Smazat poznámku?',
'collab.notes.confirmDeleteBody': 'Tato poznámka bude trvale smazána.',
'collab.notes.pin': 'Připnout',
'collab.notes.unpin': 'Odepnout',
'collab.notes.daysAgo': 'před {n} dny',
+4
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Pro optimalizaci trasy jsou potřeba alespoň dvě místa',
'dayplan.toast.routeOptimized': 'Trasa byla optimalizována',
'dayplan.toast.routeOptimizedFromHotel':
'Trasa byla optimalizována od vašeho ubytování',
'dayplan.toast.noGeoPlaces':
'Nebyla nalezena žádná místa se souřadnicemi pro výpočet trasy',
'dayplan.confirmed': 'Potvrzeno',
@@ -33,6 +35,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Toto místo má pevný čas ({time}). Přesunutím se čas odebere a povolí se volné řazení.',
'dayplan.confirmRemoveTimeAction': 'Odebrat čas a přesunout',
'dayplan.confirmDeleteNoteTitle': 'Smazat poznámku?',
'dayplan.confirmDeleteNoteBody': 'Tato poznámka bude trvale smazána.',
'dayplan.cannotDropOnTimed':
'Položky nelze umístit mezi záznamy s pevným časem',
'dayplan.cannotBreakChronology':
+3
View File
@@ -63,6 +63,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Jednotky teploty',
'settings.timeFormat': 'Formát času',
'settings.blurBookingCodes': 'Skrýt rezervační kódy',
'settings.optimizeFromAccommodation': 'Optimalizovat trasu od ubytování',
'settings.optimizeFromAccommodationHint':
'Při optimalizaci dne začne trasa v hotelu, ve kterém se ráno probudíte, a skončí v hotelu, do kterého se večer ubytujete.',
'settings.notifications': 'Oznámení',
'settings.notifyTripInvite': 'Pozvánky na cesty',
'settings.notifyBookingChange': 'Změny rezervací',
+2
View File
@@ -41,6 +41,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Abbrechen',
'collab.notes.edit': 'Bearbeiten',
'collab.notes.delete': 'Löschen',
'collab.notes.confirmDeleteTitle': 'Notiz löschen?',
'collab.notes.confirmDeleteBody': 'Diese Notiz wird dauerhaft gelöscht.',
'collab.notes.pin': 'Anheften',
'collab.notes.unpin': 'Loslösen',
'collab.notes.daysAgo': 'vor {n} T.',
+3
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Dieser Ort hat eine feste Uhrzeit ({time}). Durch das Verschieben wird die Uhrzeit entfernt und der Ort kann frei sortiert werden.',
'dayplan.confirmRemoveTimeAction': 'Uhrzeit entfernen & verschieben',
'dayplan.confirmDeleteNoteTitle': 'Notiz löschen?',
'dayplan.confirmDeleteNoteBody': 'Diese Notiz wird dauerhaft gelöscht.',
'dayplan.cannotDropOnTimed':
'Orte können nicht zwischen zeitgebundene Einträge geschoben werden',
'dayplan.cannotBreakChronology':
@@ -32,6 +34,7 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Mindestens zwei Orte für Routenoptimierung nötig',
'dayplan.toast.routeOptimized': 'Route optimiert',
'dayplan.toast.routeOptimizedFromHotel': 'Route ab deiner Unterkunft optimiert',
'dayplan.toast.noGeoPlaces':
'Keine Orte mit Koordinaten für Routenberechnung gefunden',
'dayplan.confirmed': 'Bestätigt',
+3
View File
@@ -65,6 +65,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'Zeigt Bahnhofs-/Flughafennamen auf der Karte. Wenn aus, wird nur das Icon angezeigt.',
'settings.blurBookingCodes': 'Buchungscodes verbergen',
'settings.optimizeFromAccommodation': 'Route ab der Unterkunft optimieren',
'settings.optimizeFromAccommodationHint':
'Beim Optimieren eines Tages startet die Route an der Unterkunft, in der du aufwachst, und endet an der, in die du am Abend eincheckst.',
'settings.notifications': 'Mitteilungen',
'settings.notifyTripInvite': 'Trip-Einladungen',
'settings.notifyBookingChange': 'Buchungsänderungen',
+2
View File
@@ -40,6 +40,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Cancel',
'collab.notes.edit': 'Edit',
'collab.notes.delete': 'Delete',
'collab.notes.confirmDeleteTitle': 'Delete note?',
'collab.notes.confirmDeleteBody': 'This note will be permanently deleted.',
'collab.notes.pin': 'Pin',
'collab.notes.unpin': 'Unpin',
'collab.notes.daysAgo': '{n}d ago',
+3
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'This place has a fixed time ({time}). Moving it will remove the time and allow free sorting.',
'dayplan.confirmRemoveTimeAction': 'Remove time & move',
'dayplan.confirmDeleteNoteTitle': 'Delete note?',
'dayplan.confirmDeleteNoteBody': 'This note will be permanently deleted.',
'dayplan.cannotDropOnTimed':
'Items cannot be placed between time-bound entries',
'dayplan.cannotBreakChronology':
@@ -32,6 +34,7 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'At least two places needed for route optimization',
'dayplan.toast.routeOptimized': 'Route optimized',
'dayplan.toast.routeOptimizedFromHotel': 'Route optimized from your accommodation',
'dayplan.toast.noGeoPlaces':
'No places with coordinates found for route calculation',
'dayplan.confirmed': 'Confirmed',
+3
View File
@@ -64,6 +64,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'Show station / airport names on the map. When off, only the icon is shown.',
'settings.blurBookingCodes': 'Blur Booking Codes',
'settings.optimizeFromAccommodation': 'Optimize route from accommodation',
'settings.optimizeFromAccommodationHint':
'When optimizing a day, start the route at the hotel you wake up in and end it at the one you check into that evening.',
'settings.notifications': 'Notifications',
'settings.notifyTripInvite': 'Trip invitations',
'settings.notifyBookingChange': 'Booking changes',
+2
View File
@@ -41,6 +41,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Cancelar',
'collab.notes.edit': 'Editar',
'collab.notes.delete': 'Eliminar',
'collab.notes.confirmDeleteTitle': '¿Eliminar nota?',
'collab.notes.confirmDeleteBody': 'Esta nota se eliminará de forma permanente.',
'collab.notes.pin': 'Fijar',
'collab.notes.unpin': 'Desfijar',
'collab.notes.daysAgo': 'hace {n} d',
+3
View File
@@ -20,6 +20,7 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Se necesitan al menos dos lugares para optimizar la ruta',
'dayplan.toast.routeOptimized': 'Ruta optimizada',
'dayplan.toast.routeOptimizedFromHotel': 'Ruta optimizada desde tu alojamiento',
'dayplan.toast.noGeoPlaces':
'No se encontraron lugares con coordenadas para calcular la ruta',
'dayplan.confirmed': 'Confirmado',
@@ -33,6 +34,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Este lugar tiene una hora fija ({time}). Al moverlo se eliminará la hora y se permitirá el orden libre.',
'dayplan.confirmRemoveTimeAction': 'Eliminar hora y mover',
'dayplan.confirmDeleteNoteTitle': '¿Eliminar nota?',
'dayplan.confirmDeleteNoteBody': 'Esta nota se eliminará de forma permanente.',
'dayplan.cannotDropOnTimed':
'No se pueden colocar elementos entre entradas con hora fija',
'dayplan.cannotBreakChronology':
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Unidad de temperatura',
'settings.timeFormat': 'Formato de hora',
'settings.blurBookingCodes': 'Difuminar códigos de reserva',
'settings.optimizeFromAccommodation': 'Optimizar la ruta desde el alojamiento',
'settings.optimizeFromAccommodationHint':
'Al optimizar un día, comienza la ruta en el hotel donde despiertas y termínala en aquel en el que te registras esa noche.',
'settings.notifications': 'Notificaciones',
'settings.notifyTripInvite': 'Invitaciones de viaje',
'settings.notifyBookingChange': 'Cambios en reservas',
+2
View File
@@ -42,6 +42,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Annuler',
'collab.notes.edit': 'Modifier',
'collab.notes.delete': 'Supprimer',
'collab.notes.confirmDeleteTitle': 'Supprimer la note ?',
'collab.notes.confirmDeleteBody': 'Cette note sera définitivement supprimée.',
'collab.notes.pin': 'Épingler',
'collab.notes.unpin': 'Désépingler',
'collab.notes.daysAgo': 'il y a {n} j',
+5
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
"Au moins deux lieux nécessaires pour optimiser l'itinéraire",
'dayplan.toast.routeOptimized': 'Itinéraire optimisé',
'dayplan.toast.routeOptimizedFromHotel':
'Itinéraire optimisé depuis votre hébergement',
'dayplan.toast.noGeoPlaces':
"Aucun lieu avec des coordonnées trouvé pour le calcul d'itinéraire",
'dayplan.confirmed': 'Confirmé',
@@ -33,6 +35,9 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
"Ce lieu a une heure fixe ({time}). Le déplacer supprimera l'heure et permettra un tri libre.",
'dayplan.confirmRemoveTimeAction': "Supprimer l'heure et déplacer",
'dayplan.confirmDeleteNoteTitle': 'Supprimer la note ?',
'dayplan.confirmDeleteNoteBody':
'Cette note sera définitivement supprimée.',
'dayplan.cannotDropOnTimed':
'Les éléments ne peuvent pas être placés entre des entrées à heure fixe',
'dayplan.cannotBreakChronology':
+4
View File
@@ -62,6 +62,10 @@ const settings: TranslationStrings = {
'settings.temperature': 'Unité de température',
'settings.timeFormat': "Format de l'heure",
'settings.blurBookingCodes': 'Masquer les codes de réservation',
'settings.optimizeFromAccommodation':
"Optimiser l'itinéraire depuis l'hébergement",
'settings.optimizeFromAccommodationHint':
"Lors de l'optimisation d'une journée, commencez l'itinéraire à l'hôtel où vous vous réveillez et terminez-le à celui où vous arrivez le soir.",
'settings.notifications': 'Notifications',
'settings.notifyTripInvite': 'Invitations de voyage',
'settings.notifyBookingChange': 'Modifications de réservation',
+2
View File
@@ -41,6 +41,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Ακύρωση',
'collab.notes.edit': 'Επεξεργασία',
'collab.notes.delete': 'Διαγραφή',
'collab.notes.confirmDeleteTitle': 'Διαγραφή σημείωσης;',
'collab.notes.confirmDeleteBody': 'Αυτή η σημείωση θα διαγραφεί οριστικά.',
'collab.notes.pin': 'Καρφίτσωμα',
'collab.notes.unpin': 'Ξεκαρφίτσωμα',
'collab.notes.daysAgo': '{n}η πριν',
+5
View File
@@ -9,6 +9,9 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Αυτό το μέρος έχει σταθερή ώρα ({time}). Η μετακίνησή του θα αφαιρέσει την ώρα και θα επιτρέψει ελεύθερη ταξινόμηση.',
'dayplan.confirmRemoveTimeAction': 'Αφαίρεση ώρας & μετακίνηση',
'dayplan.confirmDeleteNoteTitle': 'Διαγραφή σημείωσης;',
'dayplan.confirmDeleteNoteBody':
'Αυτή η σημείωση θα διαγραφεί οριστικά.',
'dayplan.cannotDropOnTimed':
'Τα στοιχεία δεν μπορούν να τοποθετηθούν μεταξύ καταχωρήσεων με ώρα',
'dayplan.cannotBreakChronology':
@@ -32,6 +35,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Χρειάζονται τουλάχιστον δύο μέρη για βελτιστοποίηση διαδρομής',
'dayplan.toast.routeOptimized': 'Η διαδρομή βελτιστοποιήθηκε',
'dayplan.toast.routeOptimizedFromHotel':
'Η διαδρομή βελτιστοποιήθηκε από το κατάλυμά σας',
'dayplan.toast.noGeoPlaces':
'Δεν βρέθηκαν μέρη με συντεταγμένες για τον υπολογισμό διαδρομής',
'dayplan.confirmed': 'Επιβεβαιωμένο',
+3
View File
@@ -66,6 +66,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'Εμφάνιση ονομάτων σταθμών / αεροδρομίων στον χάρτη. Όταν είναι απενεργοποιημένο, εμφανίζεται μόνο το εικονίδιο.',
'settings.blurBookingCodes': 'Θόλωμα Κωδικών Κρατήσεων',
'settings.optimizeFromAccommodation': 'Βελτιστοποίηση διαδρομής από το κατάλυμα',
'settings.optimizeFromAccommodationHint':
'Κατά τη βελτιστοποίηση μιας ημέρας, ξεκινήστε τη διαδρομή από το ξενοδοχείο στο οποίο ξυπνάτε και τερματίστε την σε αυτό στο οποίο κάνετε check-in το ίδιο βράδυ.',
'settings.notifications': 'Ειδοποιήσεις',
'settings.notifyTripInvite': 'Προσκλήσεις ταξιδιού',
'settings.notifyBookingChange': 'Αλλαγές κρατήσεων',
+2
View File
@@ -42,6 +42,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Mégse',
'collab.notes.edit': 'Szerkesztés',
'collab.notes.delete': 'Törlés',
'collab.notes.confirmDeleteTitle': 'Törli a jegyzetet?',
'collab.notes.confirmDeleteBody': 'Ez a jegyzet véglegesen törlődik.',
'collab.notes.pin': 'Kitűzés',
'collab.notes.unpin': 'Kitűzés eltávolítása',
'collab.notes.daysAgo': '{n} napja',
+4
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Legalább két hely szükséges az útvonal-optimalizáláshoz',
'dayplan.toast.routeOptimized': 'Útvonal optimalizálva',
'dayplan.toast.routeOptimizedFromHotel':
'Útvonal optimalizálva a szállásodtól',
'dayplan.toast.noGeoPlaces':
'Nem találhatók koordinátákkal rendelkező helyek az útvonalszámításhoz',
'dayplan.confirmed': 'Megerősítve',
@@ -33,6 +35,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Ennek a helynek rögzített időpontja van ({time}). Az áthelyezéssel az időpont eltávolítódik és szabad rendezés válik lehetővé.',
'dayplan.confirmRemoveTimeAction': 'Időpont eltávolítása és áthelyezés',
'dayplan.confirmDeleteNoteTitle': 'Törli a jegyzetet?',
'dayplan.confirmDeleteNoteBody': 'Ez a jegyzet véglegesen törlődik.',
'dayplan.cannotDropOnTimed':
'Elemek nem helyezhetők rögzített időpontú bejegyzések közé',
'dayplan.cannotBreakChronology':
+3
View File
@@ -63,6 +63,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Hőmérséklet egység',
'settings.timeFormat': 'Időformátum',
'settings.blurBookingCodes': 'Foglalási kódok elrejtése',
'settings.optimizeFromAccommodation': 'Útvonal optimalizálása a szállástól',
'settings.optimizeFromAccommodationHint':
'A nap optimalizálásakor az útvonal annál a szállásnál kezdődjön, ahol felébredsz, és annál érjen véget, ahova este bejelentkezel.',
'settings.notifications': 'Értesítések',
'settings.notifyTripInvite': 'Utazási meghívók',
'settings.notifyBookingChange': 'Foglalási változások',
+2
View File
@@ -40,6 +40,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Batal',
'collab.notes.edit': 'Edit',
'collab.notes.delete': 'Hapus',
'collab.notes.confirmDeleteTitle': 'Hapus catatan?',
'collab.notes.confirmDeleteBody': 'Catatan ini akan dihapus secara permanen.',
'collab.notes.pin': 'Sematkan',
'collab.notes.unpin': 'Lepas sematan',
'collab.notes.daysAgo': '{n}h lalu',
+4
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Tempat ini memiliki waktu tetap ({time}). Memindahkannya akan menghapus waktu dan mengizinkan pengurutan bebas.',
'dayplan.confirmRemoveTimeAction': 'Hapus waktu & pindahkan',
'dayplan.confirmDeleteNoteTitle': 'Hapus catatan?',
'dayplan.confirmDeleteNoteBody': 'Catatan ini akan dihapus secara permanen.',
'dayplan.cannotDropOnTimed':
'Item tidak dapat ditempatkan di antara entri yang terikat waktu',
'dayplan.cannotBreakChronology':
@@ -30,6 +32,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Diperlukan minimal dua tempat untuk optimasi rute',
'dayplan.toast.routeOptimized': 'Rute dioptimalkan',
'dayplan.toast.routeOptimizedFromHotel':
'Rute dioptimalkan dari akomodasimu',
'dayplan.toast.noGeoPlaces':
'Tidak ditemukan tempat dengan koordinat untuk kalkulasi rute',
'dayplan.confirmed': 'Dikonfirmasi',
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Satuan Suhu',
'settings.timeFormat': 'Format Waktu',
'settings.blurBookingCodes': 'Sembunyikan Kode Pemesanan',
'settings.optimizeFromAccommodation': 'Optimalkan rute dari akomodasi',
'settings.optimizeFromAccommodationHint':
'Saat mengoptimalkan suatu hari, mulai rute dari hotel tempatmu bangun pagi dan akhiri di hotel tempatmu check-in malam itu.',
'settings.notifications': 'Notifikasi',
'settings.notifyTripInvite': 'Undangan perjalanan',
'settings.notifyBookingChange': 'Perubahan pemesanan',
+2
View File
@@ -41,6 +41,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Annulla',
'collab.notes.edit': 'Modifica',
'collab.notes.delete': 'Elimina',
'collab.notes.confirmDeleteTitle': 'Eliminare la nota?',
'collab.notes.confirmDeleteBody': 'Questa nota verrà eliminata definitivamente.',
'collab.notes.pin': 'Fissa',
'collab.notes.unpin': 'Rimuovi',
'collab.notes.daysAgo': '{n}g fa',
+5
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
"Servono almeno due luoghi per l'ottimizzazione del percorso",
'dayplan.toast.routeOptimized': 'Percorso ottimizzato',
'dayplan.toast.routeOptimizedFromHotel':
'Percorso ottimizzato a partire dal tuo alloggio',
'dayplan.toast.noGeoPlaces':
'Nessun luogo con coordinate trovato per il calcolo del percorso',
'dayplan.confirmed': 'Confermata',
@@ -33,6 +35,9 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
"Questo luogo ha un orario fisso ({time}). Spostarlo rimuoverà l'orario e consentirà l'ordinamento libero.",
'dayplan.confirmRemoveTimeAction': 'Rimuovi orario e sposta',
'dayplan.confirmDeleteNoteTitle': 'Eliminare la nota?',
'dayplan.confirmDeleteNoteBody':
'Questa nota verrà eliminata definitivamente.',
'dayplan.cannotDropOnTimed':
'Gli elementi non possono essere posizionati tra voci con orario fisso',
'dayplan.cannotBreakChronology':
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Unità di Temperatura',
'settings.timeFormat': 'Formato Ora',
'settings.blurBookingCodes': 'Nascondi codici di prenotazione',
'settings.optimizeFromAccommodation': "Ottimizza il percorso dall'alloggio",
'settings.optimizeFromAccommodationHint':
"Quando ottimizzi un giorno, fa iniziare il percorso dall'hotel in cui ti svegli e terminarlo in quello in cui fai il check-in quella sera.",
'settings.notifications': 'Notifiche',
'settings.notifyTripInvite': 'Inviti di viaggio',
'settings.notifyBookingChange': 'Modifiche alle prenotazioni',
+2
View File
@@ -40,6 +40,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'キャンセル',
'collab.notes.edit': '編集',
'collab.notes.delete': '削除',
'collab.notes.confirmDeleteTitle': 'メモを削除しますか?',
'collab.notes.confirmDeleteBody': 'このメモは完全に削除されます。',
'collab.notes.pin': '固定',
'collab.notes.unpin': '固定を解除',
'collab.notes.daysAgo': '{n}日前',
+3
View File
@@ -8,6 +8,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'この場所には固定時刻({time})があります。移動すると時刻が削除され、自由に並び替えできます。',
'dayplan.confirmRemoveTimeAction': '時刻を削除して移動',
'dayplan.confirmDeleteNoteTitle': 'メモを削除しますか?',
'dayplan.confirmDeleteNoteBody': 'このメモは完全に削除されます。',
'dayplan.cannotDropOnTimed': '時刻指定の項目の間には配置できません',
'dayplan.cannotBreakChronology': '時刻指定の項目や予約の時系列が崩れます',
'dayplan.addNote': 'メモを追加',
@@ -28,6 +30,7 @@ const dayplan: TranslationStrings = {
'dayplan.routeError': 'ルートの計算に失敗しました',
'dayplan.toast.needTwoPlaces': 'ルート最適化には2つ以上の場所が必要です',
'dayplan.toast.routeOptimized': 'ルートを最適化しました',
'dayplan.toast.routeOptimizedFromHotel': '宿泊先を起点にルートを最適化しました',
'dayplan.toast.noGeoPlaces': '座標付きの場所がありません',
'dayplan.confirmed': '確定',
'dayplan.pendingRes': '保留',
+3
View File
@@ -64,6 +64,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'地図に駅・空港名を表示。オフ時はアイコンのみ。',
'settings.blurBookingCodes': '予約コードをぼかす',
'settings.optimizeFromAccommodation': '宿泊先を起点にルートを最適化',
'settings.optimizeFromAccommodationHint':
'その日を最適化する際、朝に目覚める宿泊先を起点にし、その晩にチェックインする宿泊先を終点としてルートを組みます。',
'settings.notifications': '通知',
'settings.notifyTripInvite': '旅行の招待',
'settings.notifyBookingChange': '予約の変更',
+2
View File
@@ -39,6 +39,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': '취소',
'collab.notes.edit': '편집',
'collab.notes.delete': '삭제',
'collab.notes.confirmDeleteTitle': '메모를 삭제할까요?',
'collab.notes.confirmDeleteBody': '이 메모가 영구적으로 삭제됩니다.',
'collab.notes.pin': '고정',
'collab.notes.unpin': '고정 해제',
'collab.notes.daysAgo': '{n}일 전',
+3
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'이 장소에 고정된 시간 ({time})이 있습니다. 이동하면 시간이 제거되고 자유 정렬이 허용됩니다.',
'dayplan.confirmRemoveTimeAction': '시간 제거 및 이동',
'dayplan.confirmDeleteNoteTitle': '메모를 삭제할까요?',
'dayplan.confirmDeleteNoteBody': '이 메모가 영구적으로 삭제됩니다.',
'dayplan.cannotDropOnTimed': '시간이 고정된 항목 사이에 배치할 수 없습니다',
'dayplan.cannotBreakChronology':
'이 작업은 시간 고정 항목과 예약의 시간 순서를 깨뜨립니다',
@@ -31,6 +33,7 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'경로 최적화에는 최소 두 개의 장소가 필요합니다',
'dayplan.toast.routeOptimized': '경로가 최적화되었습니다',
'dayplan.toast.routeOptimizedFromHotel': '숙소를 기준으로 경로가 최적화되었습니다',
'dayplan.toast.noGeoPlaces': '경로 계산을 위한 좌표가 있는 장소가 없습니다',
'dayplan.confirmed': '확정됨',
'dayplan.pendingRes': '대기 중',
+3
View File
@@ -65,6 +65,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.',
'settings.blurBookingCodes': '예약 코드 흐리게',
'settings.optimizeFromAccommodation': '숙소 기준으로 경로 최적화',
'settings.optimizeFromAccommodationHint':
'하루 일정을 최적화할 때, 아침에 머무는 숙소에서 경로를 시작하고 그날 저녁에 체크인하는 숙소에서 경로를 끝냅니다.',
'settings.notifications': '알림',
'settings.notifyTripInvite': '여행 초대',
'settings.notifyBookingChange': '예약 변경',
+2
View File
@@ -39,6 +39,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Annuleren',
'collab.notes.edit': 'Bewerken',
'collab.notes.delete': 'Verwijderen',
'collab.notes.confirmDeleteTitle': 'Notitie verwijderen?',
'collab.notes.confirmDeleteBody': 'Deze notitie wordt definitief verwijderd.',
'collab.notes.pin': 'Vastpinnen',
'collab.notes.unpin': 'Losmaken',
'collab.notes.daysAgo': '{n}d geleden',
+4
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Minimaal twee plaatsen nodig voor route-optimalisatie',
'dayplan.toast.routeOptimized': 'Route geoptimaliseerd',
'dayplan.toast.routeOptimizedFromHotel':
'Route geoptimaliseerd vanaf je accommodatie',
'dayplan.toast.noGeoPlaces':
'Geen plaatsen met coördinaten gevonden voor routeberekening',
'dayplan.confirmed': 'Bevestigd',
@@ -33,6 +35,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Deze plek heeft een vast tijdstip ({time}). Verplaatsen verwijdert het tijdstip en maakt vrije sortering mogelijk.',
'dayplan.confirmRemoveTimeAction': 'Tijd verwijderen en verplaatsen',
'dayplan.confirmDeleteNoteTitle': 'Notitie verwijderen?',
'dayplan.confirmDeleteNoteBody': 'Deze notitie wordt definitief verwijderd.',
'dayplan.cannotDropOnTimed':
'Items kunnen niet tussen tijdgebonden items worden geplaatst',
'dayplan.cannotBreakChronology':
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Temperatuureenheid',
'settings.timeFormat': 'Tijdnotatie',
'settings.blurBookingCodes': 'Boekingscodes vervagen',
'settings.optimizeFromAccommodation': 'Route optimaliseren vanaf accommodatie',
'settings.optimizeFromAccommodationHint':
'Begin bij het optimaliseren van een dag de route bij het hotel waar je wakker wordt en eindig bij het hotel waar je die avond incheckt.',
'settings.notifications': 'Meldingen',
'settings.notifyTripInvite': 'Reisuitnodigingen',
'settings.notifyBookingChange': 'Boekingswijzigingen',
+2
View File
@@ -40,6 +40,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Anuluj',
'collab.notes.edit': 'Edytuj',
'collab.notes.delete': 'Usuń',
'collab.notes.confirmDeleteTitle': 'Usunąć notatkę?',
'collab.notes.confirmDeleteBody': 'Ta notatka zostanie trwale usunięta.',
'collab.notes.pin': 'Przypnij',
'collab.notes.unpin': 'Odepnij',
'collab.notes.daysAgo': '{n}d temu',
+4
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'To miejsce ma określoną godzinę ({time}). Przeniesienie go usunie godzinę i umożliwi swobodne sortowanie.',
'dayplan.confirmRemoveTimeAction': 'Usuń godzinę i przenieś',
'dayplan.confirmDeleteNoteTitle': 'Usunąć notatkę?',
'dayplan.confirmDeleteNoteBody': 'Ta notatka zostanie trwale usunięta.',
'dayplan.cannotDropOnTimed':
'Nie można umieszczać elementów pomiędzy wpisami z określoną godziną',
'dayplan.cannotBreakChronology':
@@ -30,6 +32,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Potrzeba co najmniej dwóch miejsc, aby zoptymalizować trasę',
'dayplan.toast.routeOptimized': 'Trasa została zoptymalizowana',
'dayplan.toast.routeOptimizedFromHotel':
'Trasa została zoptymalizowana względem Twojego zakwaterowania',
'dayplan.toast.noGeoPlaces':
'Nie znaleziono miejsc ze współrzędnymi do obliczenia trasy',
'dayplan.confirmed': 'Potwierdzono',
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Jednostka temperatury',
'settings.timeFormat': 'Format czasu',
'settings.blurBookingCodes': 'Rozmyj kody rezerwacji',
'settings.optimizeFromAccommodation': 'Optymalizuj trasę od zakwaterowania',
'settings.optimizeFromAccommodationHint':
'Przy optymalizacji dnia rozpocznij trasę w hotelu, w którym się budzisz, a zakończ ją w tym, do którego się zameldujesz tego wieczoru.',
'settings.notifications': 'Powiadomienia',
'settings.notifyTripInvite': 'Zaproszenia do podróży',
'settings.notifyBookingChange': 'Zmiany w rezerwacjach',
+2
View File
@@ -41,6 +41,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Отмена',
'collab.notes.edit': 'Редактировать',
'collab.notes.delete': 'Удалить',
'collab.notes.confirmDeleteTitle': 'Удалить заметку?',
'collab.notes.confirmDeleteBody': 'Эта заметка будет удалена безвозвратно.',
'collab.notes.pin': 'Закрепить',
'collab.notes.unpin': 'Открепить',
'collab.notes.daysAgo': '{n} дн. назад',
+4
View File
@@ -20,6 +20,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Для оптимизации маршрута нужно минимум два места',
'dayplan.toast.routeOptimized': 'Маршрут оптимизирован',
'dayplan.toast.routeOptimizedFromHotel':
'Маршрут оптимизирован от места проживания',
'dayplan.toast.noGeoPlaces':
'Не найдено мест с координатами для расчёта маршрута',
'dayplan.confirmed': 'Подтверждено',
@@ -33,6 +35,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'У этого места фиксированное время ({time}). При перемещении время будет удалено, и станет доступна свободная сортировка.',
'dayplan.confirmRemoveTimeAction': 'Удалить время и переместить',
'dayplan.confirmDeleteNoteTitle': 'Удалить заметку?',
'dayplan.confirmDeleteNoteBody': 'Эта заметка будет удалена безвозвратно.',
'dayplan.cannotDropOnTimed':
'Элементы нельзя размещать между записями с фиксированным временем',
'dayplan.cannotBreakChronology':
+3
View File
@@ -62,6 +62,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Единица температуры',
'settings.timeFormat': 'Формат времени',
'settings.blurBookingCodes': 'Скрыть коды бронирования',
'settings.optimizeFromAccommodation': 'Оптимизировать маршрут от места проживания',
'settings.optimizeFromAccommodationHint':
'При оптимизации дня маршрут начинается от отеля, в котором вы просыпаетесь, и заканчивается у того, в который вы заселяетесь вечером.',
'settings.notifications': 'Уведомления',
'settings.notifyTripInvite': 'Приглашения в поездку',
'settings.notifyBookingChange': 'Изменения бронирований',
+2
View File
@@ -40,6 +40,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'İptal etmek',
'collab.notes.edit': 'Düzenle',
'collab.notes.delete': 'Sil',
'collab.notes.confirmDeleteTitle': 'Not silinsin mi?',
'collab.notes.confirmDeleteBody': 'Bu not kalıcı olarak silinecek.',
'collab.notes.pin': 'Sabitle',
'collab.notes.unpin': 'Sabitlemeyi kaldır',
'collab.notes.daysAgo': '{n} gün önce',
+4
View File
@@ -9,6 +9,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'Bu yerin sabit bir saati var ({time}). Taşımak saati kaldırır ve serbest sıralamaya izin verir.',
'dayplan.confirmRemoveTimeAction': 'Saati kaldır ve taşı',
'dayplan.confirmDeleteNoteTitle': 'Not silinsin mi?',
'dayplan.confirmDeleteNoteBody': 'Bu not kalıcı olarak silinecek.',
'dayplan.cannotDropOnTimed':
'Öğeler saate bağlı girişler arasına yerleştirilemez',
'dayplan.cannotBreakChronology':
@@ -32,6 +34,8 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Rota optimizasyonu için en az iki yer gerekli',
'dayplan.toast.routeOptimized': 'Rota optimize edildi',
'dayplan.toast.routeOptimizedFromHotel':
'Rota konakladığınız yerden optimize edildi',
'dayplan.toast.noGeoPlaces': 'Rota için koordinatlı yer bulunamadı',
'dayplan.confirmed': 'Onaylandı',
'dayplan.pendingRes': 'Beklemede',
+3
View File
@@ -66,6 +66,9 @@ const settings: TranslationStrings = {
'settings.bookingLabelsHint':
'Haritada istasyon / havalimanı adlarını göster. Kapalıyken yalnızca simge görünür.',
'settings.blurBookingCodes': 'Rezervasyon Kodlarını Bulanıklaştır',
'settings.optimizeFromAccommodation': 'Rotayı konaklamadan optimize et',
'settings.optimizeFromAccommodationHint':
'Bir günü optimize ederken rotaya o sabah uyandığınız otelden başlayın ve akşam giriş yaptığınız otelde sonlandırın.',
'settings.notifications': 'Bildirimler',
'settings.notifyTripInvite': 'Seyahat davetleri',
'settings.notifyBookingChange': 'Rezervasyon değişiklikleri',
+2
View File
@@ -40,6 +40,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': 'Скасувати',
'collab.notes.edit': 'Редагувати',
'collab.notes.delete': 'Видалити',
'collab.notes.confirmDeleteTitle': 'Видалити нотатку?',
'collab.notes.confirmDeleteBody': 'Цю нотатку буде видалено назавжди.',
'collab.notes.pin': 'Закріпити',
'collab.notes.unpin': 'Відкріпити',
'collab.notes.daysAgo': '{n} дн. тому',
+3
View File
@@ -22,6 +22,7 @@ const dayplan: TranslationStrings = {
'dayplan.toast.needTwoPlaces':
'Для оптимизации маршрута нужно минимум два места',
'dayplan.toast.routeOptimized': 'Маршрут оптимизирован',
'dayplan.toast.routeOptimizedFromHotel': 'Маршрут оптимізовано від вашого житла',
'dayplan.toast.noGeoPlaces':
'Не знайдено місць з координатами для розрахунку маршруту',
'dayplan.confirmed': 'Подтверждено',
@@ -35,6 +36,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'У цього місця фіксований час ({time}). При переміщенні час буде видалено, і стане доступне вільне сортування.',
'dayplan.confirmRemoveTimeAction': 'Видалити час і перемістити',
'dayplan.confirmDeleteNoteTitle': 'Видалити нотатку?',
'dayplan.confirmDeleteNoteBody': 'Цю нотатку буде видалено назавжди.',
'dayplan.cannotDropOnTimed':
'Елементи не можна розміщувати між записами з фіксованим часом',
'dayplan.cannotBreakChronology':
+3
View File
@@ -63,6 +63,9 @@ const settings: TranslationStrings = {
'settings.temperature': 'Одиниця температури',
'settings.timeFormat': 'Формат часу',
'settings.blurBookingCodes': 'Приховати коди бронювання',
'settings.optimizeFromAccommodation': 'Оптимізувати маршрут від житла',
'settings.optimizeFromAccommodationHint':
'Під час оптимізації дня починайте маршрут від готелю, у якому ви прокидаєтеся, і завершуйте його тим, у який ви заселяєтеся ввечері.',
'settings.notifications': 'Сповіщення',
'settings.notifyTripInvite': 'Запрошення до поїздки',
'settings.notifyBookingChange': 'Зміни бронювань',
+2
View File
@@ -39,6 +39,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': '取消',
'collab.notes.edit': '編輯',
'collab.notes.delete': '刪除',
'collab.notes.confirmDeleteTitle': '刪除筆記?',
'collab.notes.confirmDeleteBody': '此筆記將被永久刪除。',
'collab.notes.pin': '置頂',
'collab.notes.unpin': '取消置頂',
'collab.notes.daysAgo': '{n} 天前',
+3
View File
@@ -19,6 +19,7 @@ const dayplan: TranslationStrings = {
'dayplan.routeError': '路線計算失敗',
'dayplan.toast.needTwoPlaces': '路線最佳化至少需要兩個地點',
'dayplan.toast.routeOptimized': '路線已最佳化',
'dayplan.toast.routeOptimizedFromHotel': '已從你的住宿地點最佳化路線',
'dayplan.toast.noGeoPlaces': '未找到有座標的地點用於路線計算',
'dayplan.confirmed': '已確認',
'dayplan.pendingRes': '待確認',
@@ -30,6 +31,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'此地點有固定時間({time})。移動後將移除時間並允許自由排序。',
'dayplan.confirmRemoveTimeAction': '移除時間並移動',
'dayplan.confirmDeleteNoteTitle': '刪除筆記?',
'dayplan.confirmDeleteNoteBody': '此筆記將被永久刪除。',
'dayplan.cannotDropOnTimed': '無法將專案放置在有固定時間的條目之間',
'dayplan.cannotBreakChronology': '這將打亂已計劃專案和預訂的時間順序',
'dayplan.mobile.addPlace': '新增地點',
+3
View File
@@ -59,6 +59,9 @@ const settings: TranslationStrings = {
'settings.temperature': '溫度單位',
'settings.timeFormat': '時間格式',
'settings.blurBookingCodes': '模糊預訂程式碼',
'settings.optimizeFromAccommodation': '從住宿地點最佳化路線',
'settings.optimizeFromAccommodationHint':
'最佳化某一天的行程時,路線從你早上起床的飯店出發,並在你當晚入住的飯店結束。',
'settings.notifications': '通知',
'settings.notifyTripInvite': '旅行邀請',
'settings.notifyBookingChange': '預訂變更',
+2
View File
@@ -39,6 +39,8 @@ const collab: TranslationStrings = {
'collab.notes.cancel': '取消',
'collab.notes.edit': '编辑',
'collab.notes.delete': '删除',
'collab.notes.confirmDeleteTitle': '删除备注?',
'collab.notes.confirmDeleteBody': '此备注将被永久删除。',
'collab.notes.pin': '置顶',
'collab.notes.unpin': '取消置顶',
'collab.notes.daysAgo': '{n} 天前',
+3
View File
@@ -19,6 +19,7 @@ const dayplan: TranslationStrings = {
'dayplan.routeError': '路线计算失败',
'dayplan.toast.needTwoPlaces': '路线优化至少需要两个地点',
'dayplan.toast.routeOptimized': '路线已优化',
'dayplan.toast.routeOptimizedFromHotel': '已根据您的住宿优化路线',
'dayplan.toast.noGeoPlaces': '未找到有坐标的地点用于路线计算',
'dayplan.confirmed': '已确认',
'dayplan.pendingRes': '待确认',
@@ -30,6 +31,8 @@ const dayplan: TranslationStrings = {
'dayplan.confirmRemoveTimeBody':
'此地点有固定时间({time})。移动后将移除时间并允许自由排序。',
'dayplan.confirmRemoveTimeAction': '移除时间并移动',
'dayplan.confirmDeleteNoteTitle': '删除备注?',
'dayplan.confirmDeleteNoteBody': '此备注将被永久删除。',
'dayplan.cannotDropOnTimed': '无法将项目放置在有固定时间的条目之间',
'dayplan.cannotBreakChronology': '这将打乱已计划项目和预订的时间顺序',
'dayplan.mobile.addPlace': '添加地点',
+3
View File
@@ -59,6 +59,9 @@ const settings: TranslationStrings = {
'settings.temperature': '温度单位',
'settings.timeFormat': '时间格式',
'settings.blurBookingCodes': '模糊预订代码',
'settings.optimizeFromAccommodation': '从住宿地优化路线',
'settings.optimizeFromAccommodationHint':
'优化某一天时,路线将从您醒来时所在的酒店出发,并在当晚入住的酒店结束。',
'settings.notifications': '通知',
'settings.notifyTripInvite': '旅行邀请',
'settings.notifyBookingChange': '预订变更',