fix(dayplan): calmer day dialogs, and the shrink question side by side

The day dialog hangs from a fixed top edge, so switching to the delete
question only moves its bottom edge, and the question opens at its top
with the focus on Cancel. Cancel, Close and the quiet add share one
outline; the dated add is drawn in the accent instead of filled with it.
Dates in the dialog, its hints and the new-day toast read the way the day
list writes them, and captions use the muted text colour for contrast. A
stay booked twice is named once in the warning. On the phone the dated
pill shows only its date, so both pills fit side by side.

When the trip dialog asks both how bookings follow a moved start and
which days the new dates remove, the two now stand side by side in a wide
dialog instead of one long column.
This commit is contained in:
Maurice
2026-09-23 22:20:59 +02:00
parent 83be759c49
commit 954758082b
35 changed files with 152 additions and 118 deletions
@@ -1,6 +1,6 @@
import { useId, useState } from 'react'
import { CalendarPlus, Plus } from 'lucide-react'
import { formatDate } from '../../utils/formatters'
import { dayDate } from '../../utils/dayLabel'
import type { DayAddControls } from '../../utils/dayAdd'
interface DayAddFooterProps {
@@ -15,10 +15,11 @@ interface DayAddFooterProps {
/** One button size for the whole foot of the reorder dialog, the delete question's included. */
export const FOOTER_BTN =
'inline-flex min-h-9 flex-shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5 text-body font-medium transition-colors disabled:cursor-default disabled:opacity-40'
const SECONDARY_BTN = `${FOOTER_BTN} border border-edge text-content-secondary hover:bg-surface-hover disabled:hover:bg-transparent`
const CLOSE_BTN = `${FOOTER_BTN} ml-auto border border-edge text-content-muted hover:bg-surface-hover hover:text-content`
/** The add that matters most, tinted from the accent instead of filled with it. */
const MAIN_BTN = `${FOOTER_BTN} border border-[color:color-mix(in_srgb,var(--accent)_30%,transparent)] bg-[color:color-mix(in_srgb,var(--accent)_10%,transparent)] font-semibold text-accent-on hover:bg-[color:color-mix(in_srgb,var(--accent)_16%,transparent)] disabled:hover:bg-[color:color-mix(in_srgb,var(--accent)_10%,transparent)]`
/** Cancel, Close and the quiet add: one outline, one text colour, so none of them looks switched off beside another. */
export const SECONDARY_BTN = `${FOOTER_BTN} border border-edge text-content-secondary hover:bg-surface-hover hover:text-content disabled:hover:bg-transparent`
const CLOSE_BTN = `${SECONDARY_BTN} ml-auto`
/** The add that matters most: drawn in the accent, not filled with it, so it leads without weighing on the row. */
const MAIN_BTN = `${FOOTER_BTN} border border-accent-on bg-transparent font-semibold text-accent-on hover:bg-accent-subtle disabled:hover:bg-transparent`
/**
* The foot of the reorder dialog: close it, or add a day.
@@ -45,7 +46,7 @@ export function DayAddFooter({ dayAdd, onAddDay, onClose, t, locale }: DayAddFoo
if (!dayAdd?.nextDate) {
return (
<div className="flex flex-col gap-2.5">
{blocked && <p className="m-0 text-caption text-content-faint">{blocked}</p>}
{blocked && <p className="m-0 text-caption text-content-muted">{blocked}</p>}
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={onAddDay} disabled={addOff} className={MAIN_BTN}>
<Plus size={15} strokeWidth={2} aria-hidden="true" />
@@ -57,7 +58,7 @@ export function DayAddFooter({ dayAdd, onAddDay, onClose, t, locale }: DayAddFoo
)
}
const date = formatDate(dayAdd.nextDate, locale) ?? dayAdd.nextDate
const date = dayDate(dayAdd.nextDate, locale) ?? dayAdd.nextDate
const hint = blocked
?? (aboutUndated ? t('dayplan.addUndatedDayHint') : dayAdd.datedBlocked ?? t('dayplan.addDatedDayHint', { date }))
const pointAtUndated = {
@@ -69,7 +70,7 @@ export function DayAddFooter({ dayAdd, onAddDay, onClose, t, locale }: DayAddFoo
return (
<div className="flex flex-col gap-2.5">
<p id={hintId} aria-live="polite" className="m-0 text-caption text-content-faint">
<p id={hintId} aria-live="polite" className="m-0 text-caption text-content-muted">
{hint}
</p>
{/* Should a language run long, the row wraps between buttons, never inside one. */}
@@ -3,7 +3,7 @@ import { GripVertical, ArrowUp, ArrowDown, Trash2, AlertTriangle } from 'lucide-
import Modal from '../shared/Modal'
import Tooltip from '../shared/Tooltip'
import DayImpactList from '../shared/DayImpactList'
import { DayAddFooter, FOOTER_BTN } from './DayAddFooter'
import { DayAddFooter, FOOTER_BTN, SECONDARY_BTN } from './DayAddFooter'
import { useNetworkMode } from '../../hooks/useNetworkMode'
import { dayLabel } from '../../utils/dayLabel'
import { deleteDayBlockedReason } from '../../utils/dayDeleteImpact'
@@ -35,7 +35,6 @@ interface ListSpot {
dayId: number
index: number
scrollTop: number
panelHeight: number
}
const ICON_BTN =
@@ -50,8 +49,10 @@ const DELETE_BTN =
* so the list reflects each move immediately.
*
* A delete only asks, and it asks right here: the dialog swaps its list for the
* question, what goes with the day and the two ways out, at the same size and in
* the same place. Cancel and Escape bring the list back where it was.
* question, what goes with the day and the two ways out, at the same width. The
* dialog hangs from a fixed top edge, so the swap only moves its bottom edge,
* and the question takes the height it needs. Cancel and Escape bring the list
* back where it was.
*/
export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay, dayAdd, onDeleteDay, deleteQuestion, onClose }: DayReorderPopupProps) {
const [dragIndex, setDragIndex] = useState<number | null>(null)
@@ -77,13 +78,7 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
}
const ask = (dayId: number, index: number) => {
const body = bodyRef.current
spot.current = {
dayId,
index,
scrollTop: body?.parentElement?.scrollTop ?? 0,
panelHeight: body?.closest<HTMLElement>('[role="presentation"]')?.offsetHeight ?? 0,
}
spot.current = { dayId, index, scrollTop: bodyRef.current?.parentElement?.scrollTop ?? 0 }
onDeleteDay?.(dayId)
}
@@ -108,43 +103,35 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
return () => document.removeEventListener('keydown', onKey, true)
}, [asking])
// The question keeps the dialog at the height the list had, so nothing moves
// under the pointer; a longer one grows it and scrolls inside. Back at the
// list, the scroll position and the focus return to the row asked about.
// The question opens at its top with the focus on Cancel. Back at the list,
// the scroll position and the focus return to the row asked about.
const askedDayId = asking?.dayId ?? null
const lineCount = asking?.lines.length ?? 0
const shownDayId = useRef<number | null>(null)
useLayoutEffect(() => {
const body = bodyRef.current
if (!body) return
const scroller = body.parentElement
const turned = shownDayId.current !== askedDayId
// Closed with the question open: the next opening starts at the list, fresh.
if (!body) { shownDayId.current = null; spot.current = null; return }
if (shownDayId.current === askedDayId) return
shownDayId.current = askedDayId
body.style.minHeight = ''
const scroller = body.parentElement
if (askedDayId != null) {
const target = spot.current?.panelHeight ?? 0
const panel = body.closest<HTMLElement>('[role="presentation"]')
const short = target - (panel?.offsetHeight ?? target)
if (short > 0) body.style.minHeight = `${body.offsetHeight + short}px`
if (turned) {
if (scroller) scroller.scrollTop = 0
cancelRef.current?.focus()
}
if (scroller) scroller.scrollTop = 0
cancelRef.current?.focus()
return
}
const from = spot.current
if (!from || !turned) return
if (!from) return
spot.current = null
if (scroller) scroller.scrollTop = from.scrollTop
const buttons = body.querySelectorAll<HTMLButtonElement>('[data-delete-day]')
const again = body.querySelector<HTMLButtonElement>(`[data-delete-day="${from.dayId}"]`)
?? buttons[Math.min(from.index, buttons.length - 1)]
again?.focus()
}, [askedDayId, lineCount])
}, [askedDayId])
const footer = asking ? (
<div className="flex items-center justify-end gap-2">
<button ref={cancelRef} type="button" onClick={asking.onCancel} className={`${FOOTER_BTN} border border-edge text-content-secondary hover:bg-surface-hover`}>
<button ref={cancelRef} type="button" onClick={asking.onCancel} className={SECONDARY_BTN}>
{t('common.cancel')}
</button>
<button type="button" onClick={asking.onConfirm} className={`${FOOTER_BTN} bg-danger font-semibold text-white hover:opacity-90`}>
@@ -157,7 +144,7 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
)
return (
<Modal isOpen={isOpen} onClose={close} title={t('dayplan.reorderTitle')} size="lg" footer={footer}>
<Modal isOpen={isOpen} onClose={close} title={t('dayplan.reorderTitle')} size="lg" align="top" footer={footer}>
<div ref={bodyRef}>
{asking ? (
<section aria-labelledby={titleId} aria-describedby={bodyId} className="trek-page-enter">
@@ -166,7 +153,7 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
<AlertTriangle size={16} strokeWidth={2} aria-hidden="true" />
</span>
<div className="min-w-0 flex-1">
<h3 id={titleId} className="text-body font-semibold leading-snug text-content">{asking.title}</h3>
<h3 id={titleId} className="text-subtitle font-semibold leading-snug text-content">{asking.title}</h3>
<p id={bodyId} className="mt-0.5 text-caption text-content-muted">{t('dayplan.deleteDayBody')}</p>
</div>
</div>
@@ -174,7 +161,7 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
</section>
) : (
<>
<p className="mb-3 text-caption text-content-faint">{t('dayplan.reorderHint')}</p>
<p className="mb-3 text-caption text-content-muted">{t('dayplan.reorderHint')}</p>
{/* The popup is a modal, so it portals out of the planner and has to opt
into the long-press drag itself (#1616). Without this a finger only
+19 -5
View File
@@ -14,11 +14,20 @@ interface TripDateReviewProps {
error?: string
}
/**
* Whether the step shows both of its parts. Then they stand side by side in a wide
* dialog, the question on the left and the removed days on the right, instead of one
* long column that has to be scrolled through before the buttons.
*/
export function dateReviewIsWide(askShift: boolean, removal: RangeCheck): boolean {
return askShift && !!removal && removal !== 'unknown'
}
/**
* The step the trip dialog puts between a change of dates and the save: how
* bookings follow a moved start, and which days the new range removes with what
* is on them. Either part can stand alone. The list's booking hint follows the
* shift mode picked above it, because the two modes leave those bookings in
* shift mode picked beside it, because the two modes leave those bookings in
* different places.
*/
export default function TripDateReview({ askShift, shiftMode, onShiftMode, removal, error }: TripDateReviewProps) {
@@ -28,13 +37,16 @@ export default function TripDateReview({ askShift, shiftMode, onShiftMode, remov
{ mode: 'shift_all', label: t('dashboard.dateShiftAll'), desc: t('dashboard.dateShiftAllDesc') },
]
const wide = dateReviewIsWide(askShift, removal)
return (
<div className="space-y-3">
{error && (
<div className="p-3 bg-danger-soft border border-danger/30 rounded-xl text-body text-danger">{error}</div>
)}
<div className={wide ? 'grid gap-5 md:grid-cols-2' : 'space-y-3'}>
{askShift && (
<>
<div className="space-y-3">
<p className="text-body text-content-secondary">{t('dashboard.dateShiftIntro')}</p>
{modes.map(({ mode, label, desc }) => (
<label key={mode}
@@ -47,7 +59,8 @@ export default function TripDateReview({ askShift, shiftMode, onShiftMode, remov
</span>
</label>
))}
</>
<p className="text-caption text-content-muted">{t('dashboard.dateShiftHint')}</p>
</div>
)}
{removal === 'unknown' && (
<div role="alert" className="flex items-start gap-3 rounded-xl border border-edge-faint bg-surface-secondary p-3">
@@ -58,7 +71,8 @@ export default function TripDateReview({ askShift, shiftMode, onShiftMode, remov
</div>
)}
{removal && removal !== 'unknown' && (
<div className={askShift ? 'border-t border-edge-faint pt-3' : ''}>
// Beside the question on a wide screen, below it on a narrow one.
<div className={wide ? 'border-t border-edge-faint pt-3 md:border-l md:border-t-0 md:pl-5 md:pt-0' : ''}>
<p className="text-body text-content-secondary">{t('dashboard.shrinkIntro')}</p>
<DayImpactList
lines={shrinkTripLines(removal, t, shiftMode)}
@@ -67,7 +81,7 @@ export default function TripDateReview({ askShift, shiftMode, onShiftMode, remov
/>
</div>
)}
{askShift && <p className="text-caption text-content-faint">{t('dashboard.dateShiftHint')}</p>}
</div>
</div>
)
}
@@ -14,7 +14,7 @@ import { getApiErrorMessage, type Trip } from '../../types'
import { MAX_TRIP_DAYS, tripSpanDays, type TripCreateRequest } from '@trek/shared'
import { NumericInput } from '../shared/NumericInput'
import { currenciesWith, SYMBOLS } from '../Budget/BudgetPanel.constants'
import TripDateReview from './TripDateReview'
import TripDateReview, { dateReviewIsWide } from './TripDateReview'
import { useTripRangeGuard, type RangeCheck } from '../../hooks/useTripRangeGuard'
import type { ShiftMode } from '../../utils/dayImpactLines'
@@ -410,9 +410,10 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp
title={pendingReview
? t(pendingReview.askShift ? 'dashboard.dateShiftTitle' : 'dashboard.shrinkTitle')
: isEditing ? t('dashboard.editTrip') : t('dashboard.createTrip')}
/* The review step is a few radio buttons and a short list; it would look
lost across the width the form itself needs. */
size={pendingReview ? 'md' : '2xl'}
/* The review step is a few radio buttons and a short list; alone, either
takes the width of the day dialog, where the same list stands. With both,
they stand side by side at the form's own width. */
size={pendingReview && !dateReviewIsWide(pendingReview.askShift, pendingReview.removal) ? 'lg' : '2xl'}
footer={
<div className="flex gap-3 justify-end">
{pendingReview ? (
+8 -1
View File
@@ -24,6 +24,12 @@ interface ModalProps {
size?: string
footer?: React.ReactNode
hideCloseButton?: boolean
/**
* Where the panel stands on a wide screen. Centred by default; 'top' pins its
* upper edge, so a dialog whose content changes height grows and shrinks at
* the bottom only instead of jumping. Phones always start at the top.
*/
align?: 'center' | 'top'
}
export default function Modal({
@@ -34,6 +40,7 @@ export default function Modal({
size = 'md',
footer,
hideCloseButton = false,
align = 'center',
}: ModalProps) {
const handleEsc = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
@@ -63,7 +70,7 @@ export default function Modal({
// click-away, the panel only keeps that click from reaching it. Escape
// and the header's close button are the keyboard route out.
role="presentation"
className="fixed inset-0 z-[10000] flex items-start sm:items-center justify-center px-4 trek-modal-backdrop trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
className={`fixed inset-0 z-[10000] flex items-start ${align === 'top' ? '' : 'sm:items-center'} justify-center px-4 trek-modal-backdrop trek-backdrop-enter bg-[rgba(15,23,42,0.5)]`}
style={{ paddingTop: 70, paddingBottom: 'calc(20px + var(--bottom-nav-h))', overflow: 'hidden' }}
onMouseDown={e => { mouseDownTarget.current = e.target }}
onClick={e => {
@@ -3,8 +3,7 @@ import MSheet from '../../../components/MSheet'
import { ReorderStack } from '../plan/MPlanTimelineRows'
import { INNER_CLS, TileHeader } from './MTripSheetUi'
import { useTranslation } from '../../../../i18n'
import { dayLabel } from '../../../../utils/dayLabel'
import { formatDate } from '../../../../utils/formatters'
import { dayDate, dayLabel } from '../../../../utils/dayLabel'
import type { MTripSheetsProps } from '../MTripShell'
/** A 44px pill, the tap size of the sheet's other actions; the label never wraps, the row does. */
@@ -21,7 +20,9 @@ const PILL_QUIET = `${PILL} border border-[color:var(--m-rowbr)] bg-[color:var(-
*
* Adding sits under the list and stays in reach while the list scrolls: on a
* trip with dates the next date, which extends the trip, and a day without a
* date, with one line above them saying what the dated one does.
* date, with one line above them saying what the dated one does. The dated
* pill shows only its date beside the calendar icon, so both pills stand side
* by side in every language; its accessible name keeps the whole action.
*/
export default function MDaysSheet({ planner, shell }: MTripSheetsProps) {
const { t, locale } = useTranslation()
@@ -31,7 +32,7 @@ export default function MDaysSheet({ planner, shell }: MTripSheetsProps) {
const deleteBlocked = planner.deleteDayBlocked
const { dayAdd } = planner
const addOff = dayAdd.busy || !!dayAdd.blocked
const date = dayAdd.nextDate ? formatDate(dayAdd.nextDate, locale) ?? dayAdd.nextDate : null
const date = dayAdd.nextDate ? dayDate(dayAdd.nextDate, locale) ?? dayAdd.nextDate : null
// The lines above the buttons: why a day cannot be deleted, and what the
// dated button does or why adding is off. Offline both say the same
// sentence; it is shown once.
@@ -83,7 +84,8 @@ export default function MDaysSheet({ planner, shell }: MTripSheetsProps) {
disabled={!!deleteBlocked}
aria-label={t('dayplan.deleteDay')}
title={deleteBlocked ?? undefined}
className="flex h-[30px] w-[30px] flex-none items-center justify-center rounded-full bg-[color:var(--m-ic)] text-m-muted active:text-[color:var(--m-st-danger)] disabled:opacity-30"
// 30px to the eye, 44px to the finger: the pseudo-element widens the hit area to the row's height.
className="relative flex h-[30px] w-[30px] flex-none items-center justify-center rounded-full bg-[color:var(--m-ic)] text-m-muted after:absolute after:-inset-[7px] active:text-[color:var(--m-st-danger)] disabled:opacity-30"
>
<Trash2 size={13} strokeWidth={2.2} />
</button>
@@ -101,9 +103,15 @@ export default function MDaysSheet({ planner, shell }: MTripSheetsProps) {
<div className="flex flex-wrap gap-[8px]">
{date ? (
<>
<button type="button" onClick={dayAdd.onAddDated} disabled={addOff || !!dayAdd.datedBlocked} className={PILL_MAIN}>
<CalendarPlus size={15} strokeWidth={2.2} />
{t('dayplan.addDatedDay', { date })}
<button
type="button"
onClick={dayAdd.onAddDated}
disabled={addOff || !!dayAdd.datedBlocked}
aria-label={t('dayplan.addDatedDay', { date })}
className={PILL_MAIN}
>
<CalendarPlus size={15} strokeWidth={2.2} aria-hidden="true" />
{date}
</button>
<button type="button" onClick={() => planner.handleAddDay()} disabled={addOff} className={PILL_QUIET}>
<Plus size={15} strokeWidth={2.2} />
+2 -2
View File
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'
import { useTripStore } from '../../store/tripStore'
import { useNetworkMode } from '../../hooks/useNetworkMode'
import { datedDayOption, type DayAddControls } from '../../utils/dayAdd'
import { formatDate } from '../../utils/formatters'
import { dayDate } from '../../utils/dayLabel'
import type { Day, Trip } from '../../types'
type Translate = (key: string, params?: Record<string, string | number>) => string
@@ -65,7 +65,7 @@ export function useDayAdd(options: DayAddOptions): DayAdd {
const onAddDated = useCallback(() => {
if (!canEditDays || blocked || datedBlocked || !nextDate) return
runOnce(() => useTripStore.getState().appendDatedDay(tripId).then(
day => { toast.success(t('dayplan.tripExtended', { date: formatDate(day.date ?? nextDate, locale) ?? nextDate })) },
day => { toast.success(t('dayplan.tripExtended', { date: dayDate(day.date ?? nextDate, locale) ?? nextDate })) },
(err: unknown) => {
// The server's sentences are English, and the button already keeps a trip
// without dates or at the day limit from asking, so a refusal here is a
+2 -3
View File
@@ -3,8 +3,7 @@ import { useTripStore } from '../../store/tripStore'
import { useNetworkMode } from '../../hooks/useNetworkMode'
import { dayDeleteImpact, deleteDayBlockedReason } from '../../utils/dayDeleteImpact'
import { deleteDayLines as buildDeleteDayLines, type DayDeleteQuestion, type ImpactLine } from '../../utils/dayImpactLines'
import { dayLabel } from '../../utils/dayLabel'
import { formatDate } from '../../utils/formatters'
import { dayDate, dayLabel } from '../../utils/dayLabel'
import type { Accommodation, Day, Place, Reservation, Trip } from '../../types'
type Translate = (key: string, params?: Record<string, string | number>) => string
@@ -77,7 +76,7 @@ export function useDayDelete(options: DayDeleteOptions): DayDelete {
if (!target) return []
const budget = { items: budgetItems, currency: trip?.currency, locale }
const impact = dayDeleteImpact(target, days, { assignments, dayNotes, reservations, accommodations, places, budget }, trip)
return buildDeleteDayLines(impact, t, iso => formatDate(iso, locale) ?? iso, (day, i) => dayLabel(day, i, t, locale))
return buildDeleteDayLines(impact, t, iso => dayDate(iso, locale) ?? iso, (day, i) => dayLabel(day, i, t, locale))
}, [target, days, assignments, dayNotes, reservations, accommodations, places, budgetItems, trip, t, locale])
const handleDeleteDay = useCallback((dayId: number) => {
@@ -2509,7 +2509,7 @@ describe('useTripPlanner: deleting a day', () => {
expect(lines[0].hint).toContain('together with the booking “Alma, 1 night” and its expense of')
expect(lines[0].hint).toContain('95')
expect(lines[1].text).toBe('Stay at Harbour Hotel: one night less')
expect(lines[1].hint).toMatch(/^It runs across this day and now checks out on .*Jun 2/)
expect(lines[1].hint).toMatch(/^Check-out is now on .*Jun 2, as it runs across this day\.$/)
expect(lines[4].text).toMatch(/^Day 4 takes the date .*Jun 3/)
})
})
+12 -2
View File
@@ -86,6 +86,16 @@ const byNumber = (a: Day, b: Day): number => (a.day_number ?? 0) - (b.day_number
/** A reservation's link to a stay, which the wire carries as a number or a string. */
const stayOf = (r: Reservation): number | null => (r.accommodation_id == null ? null : Number(r.accommodation_id))
/**
* Each stay once. The stays list joins in the bookings, so a stay with two of
* them comes over the wire twice; the warning names it once all the same, and
* counts its bookings through describeStay.
*/
function distinctStays(stays: Accommodation[]): Accommodation[] {
const seen = new Set<number>()
return stays.filter(stay => !seen.has(stay.id) && seen.add(stay.id))
}
/** The bookings linked to a stay, its first booking title, and the name the list gives it. */
function describeStay(stay: Accommodation, data: DayContentData) {
const linked = data.reservations.filter(r => stayOf(r) === stay.id)
@@ -121,7 +131,7 @@ function stayExpense(linked: Reservation[], budget: DayContentBudget | undefined
*/
export function contentOnDays(days: Pick<Day, 'id' | 'title' | 'notes'>[], data: DayContentData): DayContent {
const ids = new Set(days.map(d => d.id))
const stays = data.accommodations.filter(stay => ids.has(stay.start_day_id) || ids.has(stay.end_day_id))
const stays = distinctStays(data.accommodations).filter(stay => ids.has(stay.start_day_id) || ids.has(stay.end_day_id))
const stayIds = new Set(stays.map(stay => stay.id))
const placeIds = new Set<number>()
@@ -188,7 +198,7 @@ export function dayDeleteImpact(
return start || end
}).length
const shortenedStays = data.accommodations.flatMap((stay): ShortenedStay[] => {
const shortenedStays = distinctStays(data.accommodations).flatMap((stay): ShortenedStay[] => {
const from = position.get(stay.start_day_id)
const to = position.get(stay.end_day_id)
if (cancelled.has(stay.id) || from === undefined || to === undefined || from >= at || to <= at) return []
+14 -7
View File
@@ -13,11 +13,18 @@ type Translate = (key: string, params?: Record<string, string | number>) => stri
*/
export function dayLabel(day: Pick<Day, 'title' | 'date'>, index: number, t: Translate, locale: string): string {
if (day.title) return day.title
if (day.date) {
const date = new Date(`${day.date.slice(0, 10)}T00:00:00`)
if (!Number.isNaN(date.getTime())) {
return date.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })
}
}
return t('dayplan.dayN', { n: index + 1 })
return (day.date && dayDate(day.date, locale)) || t('dayplan.dayN', { n: index + 1 })
}
/**
* A calendar date the way the day list writes it: short weekday, day, month,
* and no year, which the trip around it already says. Anything that talks
* about the days in that list (the next day to add, a moved check-out, the new
* end of the trip) writes its dates this way too, so a date reads the same in
* the button as in the row it becomes. Null when the value does not parse.
*/
export function dayDate(iso: string, locale: string): string | null {
const date = new Date(`${iso.slice(0, 10)}T00:00:00`)
if (Number.isNaN(date.getTime())) return null
return date.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })
}
@@ -153,8 +153,8 @@ describe('MDaysSheet', () => {
it('FE-MOB-DAYSS-016: two buttons, the next date and a day without one, under a line saying what the dated one does', () => {
const { planner } = renderSheet({ dayAdd: dayAdd() })
// formatDate adds the year once the date lies outside the current year.
expect(screen.getByText(/^Extends the trip by one day, until .*Oct 13(, \d{4})?\.$/)).toBeInTheDocument()
// The date reads as in the day list: weekday, day and month, no year.
expect(screen.getByText(/^Adds .*Oct 13 and extends the trip by one day\.$/)).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Add day' })).not.toBeInTheDocument()
fireEvent.click(datedButton())
@@ -182,7 +182,7 @@ describe('MDaysSheet', () => {
expect(undatedButton()).toBeDisabled()
expect(screen.getAllByText(offline)).toHaveLength(1)
// The offline sentence stands in for the dated line, not beside it.
expect(screen.queryByText(/^Extends the trip/)).not.toBeInTheDocument()
expect(screen.queryByText(/and extends the trip by one day/)).not.toBeInTheDocument()
})
it('FE-MOB-DAYSS-019: a trip at the day limit keeps the undated button and says why the dated one is off', () => {
+1 -1
View File
@@ -125,6 +125,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'يضيف يومًا بلا تاريخ في النهاية. تبقى تواريخ الرحلة كما هي.',
'dayplan.addDatedDay': 'إضافة {date}',
'dayplan.addDatedDayHint': 'يمدّد الرحلة يومًا واحدًا حتى {date}.',
'dayplan.tripExtended': 'تمت إضافة اليوم. تنتهي الرحلة الآن في {date}.',
'dayplan.tripExtended': 'تمت إضافة اليوم. تنتهي الرحلة الآن في {date}، أي بعد يوم واحد.',
};
export default dayplan;
+3 -3
View File
@@ -114,7 +114,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'A viagem agora termina em {date}',
'dayplan.deleteDayShrinkHint': 'Não há nenhum dia sem data para assumir a última data.',
'dayplan.impactStayShorter': 'Hospedagem em {name}: uma noite a menos',
'dayplan.deleteDayStayShorterHint': 'Ela passa por este dia e agora o check-out é em {date}.',
'dayplan.deleteDayStayShorterHint': 'O check-out agora é em {date}, pois ela passa por este dia.',
'dayplan.deleteDayStayShorterUndatedHint': 'Ela passa por este dia e agora termina um dia antes.',
'dayplan.deleteDaySpareDated': '{day} recebe a data {date}',
'dayplan.deleteDaySpareDatedHint': 'É o primeiro dia sem data e assume a última data da viagem.',
@@ -125,7 +125,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Sem data',
'dayplan.addUndatedDayHint': 'Adiciona um dia sem data no final. As datas da viagem continuam as mesmas.',
'dayplan.addDatedDay': 'Adicionar {date}',
'dayplan.addDatedDayHint': 'Estende a viagem em um dia, até {date}.',
'dayplan.tripExtended': 'Dia adicionado. A viagem agora termina em {date}.',
'dayplan.addDatedDayHint': 'Adiciona {date} e estende a viagem em um dia.',
'dayplan.tripExtended': 'Dia adicionado. A viagem agora termina em {date}, um dia depois.',
};
export default dayplan;
+3 -3
View File
@@ -114,7 +114,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Ara el viatge acaba el {date}',
'dayplan.deleteDayShrinkHint': "No queda cap dia sense data que pugui agafar l'última data.",
'dayplan.impactStayShorter': 'Estada a {name}: una nit menys',
'dayplan.deleteDayStayShorterHint': 'Travessa aquest dia i ara la sortida és el {date}.',
'dayplan.deleteDayStayShorterHint': 'Ara la sortida és el {date}, perquè travessa aquest dia.',
'dayplan.deleteDayStayShorterUndatedHint': 'Travessa aquest dia i ara acaba un dia abans.',
'dayplan.deleteDaySpareDated': '{day} pren la data {date}',
'dayplan.deleteDaySpareDatedHint': "És el primer dia sense data i passa a tenir l'última data del viatge.",
@@ -125,7 +125,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Sense data',
'dayplan.addUndatedDayHint': 'Afegeix un dia sense data al final. Les dates del viatge no canvien.',
'dayplan.addDatedDay': 'Afegeix {date}',
'dayplan.addDatedDayHint': 'Allarga el viatge un dia, fins al {date}.',
'dayplan.tripExtended': "S'ha afegit el dia. Ara el viatge acaba el {date}.",
'dayplan.addDatedDayHint': 'Afegeix el {date} i allarga el viatge un dia.',
'dayplan.tripExtended': "S'ha afegit el dia. Ara el viatge acaba el {date}, un dia més tard.",
};
export default dayplan;
+3 -3
View File
@@ -115,7 +115,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Cesta nyní končí {date}',
'dayplan.deleteDayShrinkHint': 'Nezbývá žádný den bez data, který by převzal poslední datum.',
'dayplan.impactStayShorter': 'Ubytování v {name}: o noc kratší',
'dayplan.deleteDayStayShorterHint': 'Zasahuje přes tento den, odjezd je teď {date}.',
'dayplan.deleteDayStayShorterHint': 'Odjezd je teď {date}, protože zasahuje přes tento den.',
'dayplan.deleteDayStayShorterUndatedHint': 'Zasahuje přes tento den, teď končí o den dříve.',
'dayplan.deleteDaySpareDated': '{day} dostane datum {date}',
'dayplan.deleteDaySpareDatedHint': 'Je to první den bez data a převezme poslední datum cesty.',
@@ -126,7 +126,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Bez data',
'dayplan.addUndatedDayHint': 'Přidá na konec den bez data. Termín cesty zůstane stejný.',
'dayplan.addDatedDay': 'Přidat {date}',
'dayplan.addDatedDayHint': 'Prodlouží cestu o jeden den, do {date}.',
'dayplan.tripExtended': 'Den přidán. Cesta teď končí {date}.',
'dayplan.addDatedDayHint': 'Přidá {date} a prodlouží cestu o jeden den.',
'dayplan.tripExtended': 'Den přidán. Cesta teď končí {date}, o den později.',
};
export default dayplan;
+3 -3
View File
@@ -115,7 +115,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Die Reise endet jetzt am {date}',
'dayplan.deleteDayShrinkHint': 'Es gibt keinen Tag ohne Datum, der das letzte Datum übernehmen könnte.',
'dayplan.impactStayShorter': 'Übernachtung in {name}: eine Nacht weniger',
'dayplan.deleteDayStayShorterHint': 'Sie läuft über diesen Tag, der Check-out ist jetzt am {date}.',
'dayplan.deleteDayStayShorterHint': 'Der Check-out ist jetzt am {date}, weil sie über diesen Tag läuft.',
'dayplan.deleteDayStayShorterUndatedHint': 'Sie läuft über diesen Tag und endet jetzt einen Tag früher.',
'dayplan.deleteDaySpareDated': '{day} bekommt das Datum {date}',
'dayplan.deleteDaySpareDatedHint': 'Er ist der erste Tag ohne Datum und übernimmt das letzte Datum der Reise.',
@@ -126,7 +126,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Ohne Datum',
'dayplan.addUndatedDayHint': 'Hängt hinten einen Tag ohne Datum an. Die Reisedaten bleiben, wie sie sind.',
'dayplan.addDatedDay': '{date} hinzufügen',
'dayplan.addDatedDayHint': 'Verlängert die Reise um einen Tag, bis {date}.',
'dayplan.tripExtended': 'Tag hinzugefügt. Die Reise endet jetzt am {date}.',
'dayplan.addDatedDayHint': 'Fügt {date} hinzu und verlängert die Reise um einen Tag.',
'dayplan.tripExtended': 'Tag hinzugefügt. Die Reise endet jetzt am {date}, einen Tag später.',
};
export default dayplan;
+3 -3
View File
@@ -114,7 +114,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Trip now ends on {date}',
'dayplan.deleteDayShrinkHint': 'There is no day without a date to take over the last date.',
'dayplan.impactStayShorter': 'Stay at {name}: one night less',
'dayplan.deleteDayStayShorterHint': 'It runs across this day and now checks out on {date}.',
'dayplan.deleteDayStayShorterHint': 'Check-out is now on {date}, as it runs across this day.',
'dayplan.deleteDayStayShorterUndatedHint': 'It runs across this day and now ends one day earlier.',
'dayplan.deleteDaySpareDated': '{day} takes the date {date}',
'dayplan.deleteDaySpareDatedHint': 'It is the first day without a date, and it takes over the last date of the trip.',
@@ -125,7 +125,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Without date',
'dayplan.addUndatedDayHint': 'Adds a day without a date at the end. The trip dates stay as they are.',
'dayplan.addDatedDay': 'Add {date}',
'dayplan.addDatedDayHint': 'Extends the trip by one day, until {date}.',
'dayplan.tripExtended': 'Day added. The trip now ends on {date}.',
'dayplan.addDatedDayHint': 'Adds {date} and extends the trip by one day.',
'dayplan.tripExtended': 'Day added. The trip now ends on {date}, one day later.',
};
export default dayplan;
+1 -1
View File
@@ -126,6 +126,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Añade un día sin fecha al final. Las fechas del viaje no cambian.',
'dayplan.addDatedDay': 'Añadir {date}',
'dayplan.addDatedDayHint': 'Alarga el viaje un día, hasta el {date}.',
'dayplan.tripExtended': 'Día añadido. El viaje termina ahora el {date}.',
'dayplan.tripExtended': 'Día añadido. El viaje termina ahora el {date}, un día más tarde.',
};
export default dayplan;
+3 -3
View File
@@ -114,7 +114,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Le voyage se termine désormais le {date}',
'dayplan.deleteDayShrinkHint': 'Aucun jour sans date ne peut reprendre la dernière date.',
'dayplan.impactStayShorter': 'Séjour à {name} : une nuit de moins',
'dayplan.deleteDayStayShorterHint': 'Il couvre ce jour et le départ a désormais lieu le {date}.',
'dayplan.deleteDayStayShorterHint': 'Le départ a désormais lieu le {date}, car il couvre ce jour.',
'dayplan.deleteDayStayShorterUndatedHint': 'Il couvre ce jour et se termine désormais un jour plus tôt.',
'dayplan.deleteDaySpareDated': '{day} reçoit la date {date}',
'dayplan.deleteDaySpareDatedHint': "C'est le premier jour sans date, et il reprend la dernière date du voyage.",
@@ -125,7 +125,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Sans date',
'dayplan.addUndatedDayHint': 'Ajoute un jour sans date à la fin. Les dates du voyage ne changent pas.',
'dayplan.addDatedDay': 'Ajouter {date}',
'dayplan.addDatedDayHint': "Prolonge le voyage d'un jour, jusqu'au {date}.",
'dayplan.tripExtended': 'Jour ajouté. Le voyage se termine désormais le {date}.',
'dayplan.addDatedDayHint': "Ajoute le {date} et prolonge le voyage d'un jour.",
'dayplan.tripExtended': 'Jour ajouté. Le voyage se termine désormais le {date}, un jour plus tard.',
};
export default dayplan;
+1 -1
View File
@@ -130,6 +130,6 @@ const dayplan: TranslationStrings = {
'Προσθέτει στο τέλος μια ημέρα χωρίς ημερομηνία. Οι ημερομηνίες του ταξιδιού μένουν ίδιες.',
'dayplan.addDatedDay': 'Προσθήκη {date}',
'dayplan.addDatedDayHint': 'Επεκτείνει το ταξίδι κατά μία ημέρα, έως {date}.',
'dayplan.tripExtended': 'Η ημέρα προστέθηκε. Το ταξίδι τελειώνει πλέον στις {date}.',
'dayplan.tripExtended': 'Η ημέρα προστέθηκε. Το ταξίδι τελειώνει πλέον στις {date}, μία ημέρα αργότερα.',
};
export default dayplan;
+1 -1
View File
@@ -127,6 +127,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Egy dátum nélküli napot ad a végéhez. Az utazás dátumai nem változnak.',
'dayplan.addDatedDay': '{date} hozzáadása',
'dayplan.addDatedDayHint': 'Egy nappal meghosszabbítja az utazást. Új utolsó nap: {date}.',
'dayplan.tripExtended': 'Nap hozzáadva. Az utazás utolsó napja most: {date}.',
'dayplan.tripExtended': 'Nap hozzáadva. Az utazás egy nappal tovább tart, utolsó napja most: {date}.',
};
export default dayplan;
+1 -1
View File
@@ -128,6 +128,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Menambahkan hari tanpa tanggal di akhir. Tanggal perjalanan tetap sama.',
'dayplan.addDatedDay': 'Tambah {date}',
'dayplan.addDatedDayHint': 'Memperpanjang perjalanan satu hari, hingga {date}.',
'dayplan.tripExtended': 'Hari ditambahkan. Perjalanan kini berakhir pada {date}.',
'dayplan.tripExtended': 'Hari ditambahkan. Perjalanan kini berakhir pada {date}, satu hari lebih lambat.',
};
export default dayplan;
+1 -1
View File
@@ -128,6 +128,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Aggiunge un giorno senza data alla fine. Le date del viaggio restano invariate.',
'dayplan.addDatedDay': 'Aggiungi {date}',
'dayplan.addDatedDayHint': 'Allunga il viaggio di un giorno, fino a {date}.',
'dayplan.tripExtended': 'Giorno aggiunto. Il viaggio ora finisce il {date}.',
'dayplan.tripExtended': 'Giorno aggiunto. Il viaggio ora finisce il {date}, un giorno dopo.',
};
export default dayplan;
+1 -1
View File
@@ -126,6 +126,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': '日付のない日を最後に追加します。旅行の日程は変わりません。',
'dayplan.addDatedDay': '{date}を追加',
'dayplan.addDatedDayHint': '旅行を1日延長し、{date}までにします。',
'dayplan.tripExtended': '日を追加しました。旅行の最終日は{date}になりました。',
'dayplan.tripExtended': '日を追加しました。旅行が1日延びて、最終日は{date}になりました。',
};
export default dayplan;
+1 -1
View File
@@ -125,6 +125,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': '날짜 없는 날을 마지막에 추가합니다. 여행 날짜는 그대로입니다.',
'dayplan.addDatedDay': '{date} 추가',
'dayplan.addDatedDayHint': '여행을 하루 늘려 {date}까지로 만듭니다.',
'dayplan.tripExtended': '날짜를 추가했습니다. 이제 여행은 {date}에 끝납니다.',
'dayplan.tripExtended': '날짜를 추가했습니다. 여행이 하루 늘어나 이제 {date}에 끝납니다.',
};
export default dayplan;
+1 -1
View File
@@ -126,6 +126,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Voegt aan het eind een dag zonder datum toe. De reisdata blijven zoals ze zijn.',
'dayplan.addDatedDay': '{date} toevoegen',
'dayplan.addDatedDayHint': 'Verlengt de reis met één dag, tot {date}.',
'dayplan.tripExtended': 'Dag toegevoegd. De reis eindigt nu op {date}.',
'dayplan.tripExtended': 'Dag toegevoegd. De reis eindigt nu op {date}, een dag later.',
};
export default dayplan;
+1 -1
View File
@@ -128,6 +128,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Dodaje na końcu dzień bez daty. Daty podróży się nie zmieniają.',
'dayplan.addDatedDay': 'Dodaj {date}',
'dayplan.addDatedDayHint': 'Wydłuża podróż o jeden dzień, do {date}.',
'dayplan.tripExtended': 'Dodano dzień. Podróż kończy się teraz {date}.',
'dayplan.tripExtended': 'Dodano dzień. Podróż kończy się teraz {date}, dzień później.',
};
export default dayplan;
+3 -3
View File
@@ -115,7 +115,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Теперь поездка заканчивается {date}',
'dayplan.deleteDayShrinkHint': 'Нет дня без даты, который мог бы занять последнюю дату.',
'dayplan.impactStayShorter': 'Проживание в {name}: на одну ночь меньше',
'dayplan.deleteDayStayShorterHint': 'Оно проходит через этот день, и выезд теперь {date}.',
'dayplan.deleteDayStayShorterHint': 'Выезд теперь {date}, потому что оно проходит через этот день.',
'dayplan.deleteDayStayShorterUndatedHint': 'Оно проходит через этот день и теперь заканчивается на день раньше.',
'dayplan.deleteDaySpareDated': '{day} получает дату {date}',
'dayplan.deleteDaySpareDatedHint': 'Это первый день без даты, и он получает последнюю дату поездки.',
@@ -126,7 +126,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Без даты',
'dayplan.addUndatedDayHint': 'Добавляет в конец день без даты. Даты поездки не меняются.',
'dayplan.addDatedDay': 'Добавить {date}',
'dayplan.addDatedDayHint': 'Продлевает поездку на один день, до {date}.',
'dayplan.tripExtended': 'День добавлен. Теперь поездка заканчивается {date}.',
'dayplan.addDatedDayHint': 'Добавляет {date} и продлевает поездку на один день.',
'dayplan.tripExtended': 'День добавлен. Теперь поездка заканчивается {date}, на день позже.',
};
export default dayplan;
+3 -3
View File
@@ -115,7 +115,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Resan slutar nu {date}',
'dayplan.deleteDayShrinkHint': 'Det finns ingen dag utan datum som kan ta över det sista datumet.',
'dayplan.impactStayShorter': 'Boende på {name}: en natt kortare',
'dayplan.deleteDayStayShorterHint': 'Det sträcker sig över den här dagen och utcheckningen är nu {date}.',
'dayplan.deleteDayStayShorterHint': 'Utcheckningen är nu {date}, eftersom det sträcker sig över den här dagen.',
'dayplan.deleteDayStayShorterUndatedHint': 'Det sträcker sig över den här dagen och slutar nu en dag tidigare.',
'dayplan.deleteDaySpareDated': '{day} får datumet {date}',
'dayplan.deleteDaySpareDatedHint': 'Det är den första dagen utan datum, och den tar över resans sista datum.',
@@ -126,7 +126,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Utan datum',
'dayplan.addUndatedDayHint': 'Lägger till en dag utan datum i slutet. Resans datum förblir desamma.',
'dayplan.addDatedDay': 'Lägg till {date}',
'dayplan.addDatedDayHint': 'Förlänger resan med en dag, till {date}.',
'dayplan.tripExtended': 'Dag tillagd. Resan slutar nu {date}.',
'dayplan.addDatedDayHint': 'Lägger till {date} och förlänger resan med en dag.',
'dayplan.tripExtended': 'Dag tillagd. Resan slutar nu {date}, en dag senare.',
};
export default dayplan;
+1 -1
View File
@@ -126,6 +126,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Sona tarihsiz bir gün ekler. Seyahat tarihleri aynı kalır.',
'dayplan.addDatedDay': '{date} ekle',
'dayplan.addDatedDayHint': 'Seyahati bir gün uzatır. Yeni son gün: {date}.',
'dayplan.tripExtended': 'Gün eklendi. Seyahatin yeni son günü: {date}.',
'dayplan.tripExtended': 'Gün eklendi. Seyahat bir gün uzadı, yeni son gün: {date}.',
};
export default dayplan;
+3 -3
View File
@@ -115,7 +115,7 @@ const dayplan: TranslationStrings = {
'dayplan.deleteDayShrink': 'Тепер подорож закінчується {date}',
'dayplan.deleteDayShrinkHint': 'Немає дня без дати, який міг би зайняти останню дату.',
'dayplan.impactStayShorter': 'Проживання в {name}: на одну ніч менше',
'dayplan.deleteDayStayShorterHint': 'Воно проходить через цей день, і виїзд тепер {date}.',
'dayplan.deleteDayStayShorterHint': 'Виїзд тепер {date}, бо воно проходить через цей день.',
'dayplan.deleteDayStayShorterUndatedHint': 'Воно проходить через цей день і тепер закінчується на день раніше.',
'dayplan.deleteDaySpareDated': '{day} отримує дату {date}',
'dayplan.deleteDaySpareDatedHint': 'Це перший день без дати, і він отримує останню дату подорожі.',
@@ -126,7 +126,7 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDay': 'Без дати',
'dayplan.addUndatedDayHint': 'Додає в кінець день без дати. Дати подорожі не змінюються.',
'dayplan.addDatedDay': 'Додати {date}',
'dayplan.addDatedDayHint': 'Подовжує подорож на один день, до {date}.',
'dayplan.tripExtended': 'День додано. Тепер подорож закінчується {date}.',
'dayplan.addDatedDayHint': 'Додає {date} і подовжує подорож на один день.',
'dayplan.tripExtended': 'День додано. Тепер подорож закінчується {date}, на день пізніше.',
};
export default dayplan;
+1 -1
View File
@@ -126,6 +126,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': 'Thêm một ngày không có ngày tháng vào cuối. Ngày của chuyến đi giữ nguyên.',
'dayplan.addDatedDay': 'Thêm {date}',
'dayplan.addDatedDayHint': 'Kéo dài chuyến đi thêm một ngày, đến {date}.',
'dayplan.tripExtended': 'Đã thêm ngày. Chuyến đi giờ kết thúc vào {date}.',
'dayplan.tripExtended': 'Đã thêm ngày. Chuyến đi giờ kết thúc vào {date}, muộn hơn một ngày.',
};
export default dayplan;
+1 -1
View File
@@ -123,6 +123,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': '在最後新增一天沒有日期的行程。旅程日期不變。',
'dayplan.addDatedDay': '新增 {date}',
'dayplan.addDatedDayHint': '旅程延長一天,到 {date} 為止。',
'dayplan.tripExtended': '已新增一天。旅程現在於 {date} 結束。',
'dayplan.tripExtended': '已新增一天。旅程延長一天,現在於 {date} 結束。',
};
export default dayplan;
+1 -1
View File
@@ -123,6 +123,6 @@ const dayplan: TranslationStrings = {
'dayplan.addUndatedDayHint': '在末尾添加一天没有日期的行程。旅行日期不变。',
'dayplan.addDatedDay': '添加 {date}',
'dayplan.addDatedDayHint': '旅行延长一天,到 {date} 为止。',
'dayplan.tripExtended': '已添加一天。旅行现在于 {date} 结束。',
'dayplan.tripExtended': '已添加一天。旅行延长一天,现在于 {date} 结束。',
};
export default dayplan;