feat(assistant): change cards in the style of the planner's day list

A waiting change is one row as in the day list: a round choice in front,
what it touches in bold, a faint line with what happens and when. No switch,
nothing to fold open, no day band: the planner's day list already shows the
suggestion among the day's stops. Whether the days still fit is one pill in
the card's head, with what to look at on hover.

Also mends the comment the rebase onto dev split in json-columns.ts.
This commit is contained in:
Maurice
2026-10-21 22:39:43 +02:00
parent 55c6bad295
commit 2fbff51299
35 changed files with 176 additions and 759 deletions
@@ -128,13 +128,10 @@ describe('AssistantChangeCard: deciding', () => {
renderCard([pending('c1', 'Kinkaku-ji')]);
expect(screen.getByText('Kinkaku-ji')).toBeInTheDocument();
expect(screen.getByText('Add place')).toBeInTheDocument();
// The time is a chip of its own, old to new; the address waits behind "Details".
// The faint line says when, old to new; the address is for the planner, not the card.
expect(screen.getByText('09:00')).toBeInTheDocument();
expect(screen.getByText('10:30')).toBeInTheDocument();
expect(screen.queryByText('Kyoto')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Details' }));
expect(screen.getByText('Address')).toBeInTheDocument();
expect(screen.getByText('Kyoto')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Include this change' })).toBeNull();
expect(screen.getByRole('status')).toHaveTextContent('1 change is waiting for you');
});
@@ -239,36 +236,24 @@ describe('AssistantChangeCard: on the map', () => {
expect(row).toHaveClass('bg-surface-hover');
});
it('FE-AI-CARD-009: the days a waiting card touches read as named stops, with the schedule folded away', () => {
it('FE-AI-CARD-009: the head says whether the days still fit, without drawing the days again', () => {
plannerHere();
renderCard([onTheMap('c1', 'Pantheon')]);
expect(screen.getByText('Rome')).toBeInTheDocument();
// Folded, each stop is a chip with its name, not a blank block behind a tooltip.
expect(screen.getByText('Louvre')).toBeInTheDocument();
expect(screen.getAllByText('Pantheon')).toHaveLength(2);
expect(screen.queryByText('New')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Show schedule' }));
expect(screen.getByRole('button', { name: 'Hide schedule' })).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByText('Louvre')).toBeInTheDocument();
expect(screen.getAllByText('Pantheon')).toHaveLength(2);
expect(screen.getByText('New')).toBeInTheDocument();
expect(screen.getByText(/min travel/)).toBeInTheDocument();
// Hovering the stop in the schedule lights the same pin as its row.
fireEvent.mouseEnter(screen.getByText('New'));
expect(useAssistantStore.getState().mapLink?.hoveredId).toBe('c1');
expect(screen.getByText(/Fits the time|to check/)).toBeInTheDocument();
// The day list of the planner shows the suggestion; the card keeps to one line per change.
expect(screen.getAllByText('Pantheon')).toHaveLength(1);
expect(screen.queryByRole('button', { name: 'Show schedule' })).toBeNull();
});
it('FE-AI-CARD-010: no band while the planner holds another trip, or once nothing waits', () => {
it('FE-AI-CARD-010: no check while the planner holds another trip, or once nothing waits', () => {
plannerHere(TRIP + 1);
const first = renderCard([onTheMap('c1', 'Pantheon')]);
expect(screen.queryByRole('button', { name: 'Show schedule' })).toBeNull();
expect(screen.queryByText(/Fits the time|to check/)).toBeNull();
first.unmount();
plannerHere();
renderCard([{ ...onTheMap('c1', 'Pantheon'), state: 'applied' }]);
expect(screen.queryByRole('button', { name: 'Show schedule' })).toBeNull();
expect(screen.queryByText(/Fits the time|to check/)).toBeNull();
});
it('FE-AI-CARD-011: on the phone the card hops to the map and frames what it would change', () => {
@@ -26,12 +26,11 @@ import { useTranslation } from '../../i18n';
import { blockKeyOf } from '../../store/assistantMapModel';
import { chatOf, useAssistantStore } from '../../store/assistantStore';
import { useTripStore } from '../../store/tripStore';
import ToggleSwitch from '../Settings/ToggleSwitch';
import { useToast } from '../shared/Toast';
import { Tooltip } from '../shared/Tooltip';
import AssistantChangeFields from './AssistantChangeFields';
import AssistantCompareSwitch from './AssistantCompareSwitch';
import AssistantDayBand from './AssistantDayBand';
import AssistantFitCheck from './AssistantFitCheck';
import AssistantWeekOverview from './AssistantWeekOverview';
import { type ChangeItem, type ChangeState, undoLeftMs } from './assistantParts';
import { useAssistantView } from './assistantViewContext';
@@ -137,18 +136,18 @@ function ChangeHead({ preview, struck = false }: { preview: AssistantChangePrevi
if (!preview) return <span className="trek-skeleton inline-block h-4 w-40" aria-hidden="true" />;
return (
<span className="flex min-w-0 flex-col">
<span className="flex min-w-0 items-center gap-1.5 text-caption text-content-muted">
<bdi className={`truncate text-body font-semibold leading-tight text-content ${struck ? 'line-through' : ''}`}>
{preview.subject}
</bdi>
<span className="mt-0.5 flex min-w-0 items-center gap-2.5 text-caption text-content-faint">
<span className="truncate">{verbLabel(preview, t)}</span>
{preview.domain && (
// Where in TREK a change outside the trip plan lands: the vacation planner, the journal.
<span className="flex-shrink-0 rounded-full bg-surface-secondary px-1.5 font-medium text-content-secondary">
<span className="flex-shrink-0 rounded-full bg-surface-secondary px-1.5 text-content-secondary">
{t(`assistant.domain.${preview.domain}`)}
</span>
)}
</span>
<bdi className={`truncate text-body font-semibold text-content ${struck ? 'line-through' : ''}`}>
{preview.subject}
</bdi>
</span>
);
}
@@ -245,38 +244,69 @@ function Receipt({
);
}
/** A change waiting for its decision; resting on it lights its pin on the map. */
/**
* A change waiting for its decision, as a row of the planner's day list: the
* round choice in front (only when there is more than one to choose from),
* what it touches in bold, and a faint line saying what happens and when.
* Resting on it lights its pin on the map.
*/
function PendingRow({
item,
excluded,
withSwitch,
withChoice,
disabled,
onToggle,
}: {
item: ChangeItem;
excluded: boolean;
withSwitch: boolean;
withChoice: boolean;
disabled: boolean;
onToggle: () => void;
}) {
const { t } = useTranslation();
const { tripId } = useAssistantView();
const { litFromMap, handlers } = useChangeHover(tripId, item.preview?.effect ? item.toolCallId : null);
const preview = item.preview;
return (
<div
{...handlers}
className={`-mx-1.5 flex gap-2.5 rounded-lg px-1.5 py-2.5 transition-[opacity,background-color] ${
excluded ? 'opacity-45' : ''
} ${litFromMap ? 'bg-surface-hover' : ''}`}
className={`-mx-1.5 flex items-center gap-2.5 rounded-[12px] px-1.5 py-2 transition-[opacity,background-color] ${
excluded ? 'opacity-50' : ''
} ${litFromMap ? 'bg-surface-hover' : 'hover:bg-surface-hover'}`}
>
<KindTile verbKey={item.preview?.verbKey ?? null} />
<div className="min-w-0 flex-1">
<ChangeHead preview={item.preview} />
{item.preview && <AssistantChangeFields rows={item.preview.rows} />}
</div>
{withSwitch && (
<ToggleSwitch on={!excluded} onToggle={onToggle} label={t('assistant.change.include')} disabled={disabled} />
{withChoice && (
<button
type="button"
onClick={onToggle}
disabled={disabled}
aria-pressed={!excluded}
aria-label={t('assistant.change.include')}
className={`grid h-[18px] w-[18px] flex-none place-items-center rounded-full outline-none transition-colors focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50 ${
excluded ? 'border-[1.5px] border-edge bg-surface-card' : 'bg-accent'
}`}
>
{!excluded && <Check size={11} strokeWidth={3} aria-hidden="true" className="text-accent-text" />}
</button>
)}
<KindTile verbKey={preview?.verbKey ?? null} size="sm" dimmed={excluded} />
<div className="min-w-0 flex-1">
{preview ? (
<>
<bdi className="block truncate text-body font-semibold leading-tight text-content">{preview.subject}</bdi>
<span className="mt-0.5 flex min-w-0 items-center gap-2.5 text-caption text-content-faint">
<span className="flex-shrink-0">{verbLabel(preview, t)}</span>
{preview.domain && (
<span className="flex-shrink-0 rounded-full bg-surface-secondary px-1.5 text-content-secondary">
{t(`assistant.domain.${preview.domain}`)}
</span>
)}
<AssistantChangeFields rows={preview.rows} />
</span>
</>
) : (
<span className="trek-skeleton inline-block h-4 w-40" aria-hidden="true" />
)}
</div>
</div>
);
}
@@ -368,7 +398,12 @@ function HeadActions({
const here = useTripStore((s) => s.trip?.id === tripId);
if (open) {
const toMap = variant === 'fullscreen' && !!onShowOnMap && here && changes.length > 0;
return toMap ? <ViewOnMapButton changes={changes} /> : null;
return (
<>
<AssistantFitCheck changes={changes} />
{toMap && <ViewOnMapButton changes={changes} />}
</>
);
}
return (
<>
@@ -440,7 +475,6 @@ export default function AssistantChangeCard({ items }: { items: ChangeItem[] })
{open && week.length > 0 && (
<AssistantWeekOverview days={week} excluded={excluded} disabled={isRunning} onToggleDay={toggleDay} />
)}
{open && !folded && <AssistantDayBand changes={changes} />}
{folded && (
<button
type="button"
@@ -450,7 +484,7 @@ export default function AssistantChangeCard({ items }: { items: ChangeItem[] })
{t('assistant.week.showAll', { count: pending.length })}
</button>
)}
<div className={open ? 'divide-y divide-edge-faint' : ''}>
<div>
{items.map((item) =>
item.state === 'pending' ? (
folded ? null : (
@@ -458,7 +492,7 @@ export default function AssistantChangeCard({ items }: { items: ChangeItem[] })
key={item.toolCallId}
item={item}
excluded={excluded.has(item.toolCallId)}
withSwitch={pending.length > 1}
withChoice={pending.length > 1}
disabled={isRunning}
onToggle={() => toggle(item.toolCallId)}
/>
@@ -3,28 +3,26 @@ import {
ArrowRight,
CalendarClock,
CalendarDays,
ChevronDown,
Clock,
Hash,
ListOrdered,
LogIn,
LogOut,
type LucideIcon,
Tag,
Wallet,
} from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from '../../i18n';
import { CHANGE_LABELS } from './assistantParts';
/**
* The fields of one change, read at a glance: the few that decide whether it
* fits (when, what, what it costs) as quiet icon chips, a value that changes
* as old to new, everything else folded behind one "Details" line. Positions
* and colours never show; the map and the trip show those better.
* The few facts that tell a change apart at a glance, on the faint line under
* its name the way the planner's day list shows a place: when, what kind,
* what it costs, a new order. A value that changes reads old to new. The rest
* of what the change sets is not listed: the planner shows it once applied.
*/
/** The fields worth a chip of their own, each with its glyph, in this order. */
/** The fields worth a place on the line, each with its glyph, in this order. */
const KEY_FIELDS: Partial<Record<AssistantChangeLabelKey, LucideIcon>> = {
day: CalendarDays,
time: Clock,
@@ -35,17 +33,10 @@ const KEY_FIELDS: Partial<Record<AssistantChangeLabelKey, LucideIcon>> = {
category: Tag,
price: Wallet,
quantity: Hash,
order: ListOrdered,
};
const KEY_ORDER = Object.keys(KEY_FIELDS) as AssistantChangeLabelKey[];
/** Fields that mean nothing written out. */
const HIDDEN: ReadonlySet<AssistantChangeLabelKey> = new Set(['coordinates', 'position', 'icon', 'color', 'currency']);
/** What a row is called: its label, or for a tool's own argument the argument's name in words. */
function labelOf(row: AssistantChangeRow, t: (key: string) => string): string {
if (row.labelKey === 'field' && row.field) return row.field.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2');
return t(CHANGE_LABELS[row.labelKey]?.labelKey ?? 'assistant.change.label.field');
}
const MAX_FACTS = 3;
/** "#3" is how a day comes from the server; it reads as "Day 3". */
function shown(row: AssistantChangeRow, value: string | null | undefined, dayWord: string): string {
@@ -53,92 +44,32 @@ function shown(row: AssistantChangeRow, value: string | null | undefined, dayWor
return value ?? '';
}
function Chip({ row }: { row: AssistantChangeRow }) {
export default function AssistantChangeFields({ rows }: { rows: AssistantChangeRow[] }) {
const { t } = useTranslation();
const Icon = KEY_FIELDS[row.labelKey] ?? Tag;
const label = labelOf(row, t);
const dayWord = t('assistant.week.day');
const changed = row.op !== 'add' && !!row.before;
const facts = rows
.filter((row) => row.labelKey in KEY_FIELDS && row.after)
.sort((a, b) => KEY_ORDER.indexOf(a.labelKey) - KEY_ORDER.indexOf(b.labelKey))
.slice(0, MAX_FACTS);
if (facts.length === 0) return null;
return (
<span
title={label}
className="inline-flex max-w-full items-center gap-1 rounded-md bg-surface-secondary px-1.5 py-0.5 text-caption text-content-secondary"
>
<Icon size={11} aria-label={label} className="flex-shrink-0 text-content-faint" />
{changed && (
<>
<bdi className="min-w-0 truncate text-content-faint line-through">{shown(row, row.before, dayWord)}</bdi>
<ArrowRight size={10} aria-hidden="true" className="flex-shrink-0 text-content-faint rtl:-scale-x-100" />
</>
)}
<bdi className="min-w-0 truncate font-medium text-content">{shown(row, row.after, dayWord)}</bdi>
<span className="flex min-w-0 items-center gap-2.5">
{facts.map((row, i) => {
const Icon = KEY_FIELDS[row.labelKey] ?? Tag;
const changed = row.op !== 'add' && !!row.before;
return (
<span key={`${row.labelKey}-${i}`} className="inline-flex min-w-0 items-center gap-1">
<Icon size={11} aria-hidden="true" className="flex-shrink-0" />
{changed && (
<>
<bdi className="truncate line-through">{shown(row, row.before, dayWord)}</bdi>
<ArrowRight size={10} aria-hidden="true" className="flex-shrink-0 rtl:-scale-x-100" />
</>
)}
<bdi className="truncate text-content-muted">{shown(row, row.after, dayWord)}</bdi>
</span>
);
})}
</span>
);
}
/** A field behind "Details": its name, then the value, old to new where it changes. */
function DetailLine({ row }: { row: AssistantChangeRow }) {
const { t } = useTranslation();
const changed = row.op !== 'add' && !!row.before;
return (
<div className="flex min-w-0 gap-2 text-caption">
<span className="w-24 flex-shrink-0 truncate text-content-faint">{labelOf(row, t)}</span>
<span className="flex min-w-0 flex-1 items-center gap-1 text-content-secondary">
{changed && (
<>
<bdi className="min-w-0 truncate text-content-faint line-through">{row.before}</bdi>
<ArrowRight size={10} aria-hidden="true" className="flex-shrink-0 text-content-faint rtl:-scale-x-100" />
</>
)}
<bdi className="min-w-0 truncate">{row.after || t('assistant.change.empty')}</bdi>
</span>
</div>
);
}
export default function AssistantChangeFields({ rows }: { rows: AssistantChangeRow[] }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const visible = rows.filter((row) => !HIDDEN.has(row.labelKey) && row.labelKey !== 'name');
const key = visible
.filter((row) => row.labelKey in KEY_FIELDS && row.after)
.sort((a, b) => KEY_ORDER.indexOf(a.labelKey) - KEY_ORDER.indexOf(b.labelKey));
const rest = visible.filter((row) => !key.includes(row));
if (key.length === 0 && rest.length === 0) return null;
return (
<div className="mt-1.5 flex flex-col gap-1.5">
{key.length > 0 && (
<div className="flex flex-wrap gap-1">
{key.map((row, i) => (
<Chip key={`${row.labelKey}-${i}`} row={row} />
))}
</div>
)}
{rest.length > 0 && (
<>
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
className="inline-flex items-center gap-1 self-start rounded-md text-caption text-content-faint transition-colors hover:text-content"
>
<ChevronDown
size={12}
aria-hidden="true"
className={`transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
/>
{t(open ? 'assistant.change.detailsHide' : 'assistant.change.details')}
</button>
{open && (
<div className="trek-assistant-enter flex flex-col gap-1 rounded-lg bg-surface-secondary px-2.5 py-2">
{rest.map((row, i) => (
<DetailLine key={`${row.labelKey}-${i}`} row={row} />
))}
</div>
)}
</>
)}
</div>
);
}
@@ -1,235 +0,0 @@
import { ChevronDown, CircleCheck, Minus, Plus, TriangleAlert } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useTranslation } from '../../i18n';
import { useTripStore } from '../../store/tripStore';
import { dayLabel } from '../../utils/dayLabel';
import { Tooltip } from '../shared/Tooltip';
import { useAssistantView } from './assistantViewContext';
import {
buildDayAfter,
changedDays,
type DayAfter,
type DayChange,
dayIssues,
stopIssue,
type TimelineStop,
} from './dayAfterModel';
import { useChangeHover } from './useChangeHover';
/**
* The days a waiting card touches, as they would read once it ran: per day its
* date and its stops in order. What stays reads as quiet text; only what the
* card adds is a filled pill and what it takes away is struck, so a glance
* finds the suggestion among the rest. The details (times, the way between stops,
* free time, a clash) stay folded away until asked for. Two days at most; the
* rest are counted. Shown only while the planner holds this trip, since the day
* plans come from it.
*/
/** Days shown; the rest are counted. */
const MAX_DAYS = 2;
/** Stops shown as chips on a day; the rest are counted. */
const MAX_CHIPS = 6;
const CHIP: Record<TimelineStop['status'], string> = {
kept: 'px-0.5 text-content-muted',
new: 'rounded-full bg-accent px-2 py-0.5 font-medium text-accent-text shadow-sm',
removed: 'rounded-full px-1.5 py-0.5 text-content-faint line-through',
};
/** One stop of the day, named; the change it belongs to lights up with it. */
function StopChip({ stop }: { stop: TimelineStop }) {
const { tripId } = useAssistantView();
const { lit, handlers } = useChangeHover(tripId, stop.toolCallId);
const issue = stop.lead?.overlapsWith ? 'ring-2 ring-danger' : stopIssue(stop) ? 'ring-2 ring-warning' : '';
const Mark = stop.status === 'new' ? Plus : stop.status === 'removed' ? Minus : null;
return (
<span
{...(stop.toolCallId ? handlers : {})}
className={`inline-flex max-w-[12rem] items-center gap-1 text-caption transition-all ${CHIP[stop.status]} ${issue} ${lit ? 'scale-[1.04]' : ''}`}
>
{Mark && <Mark size={11} strokeWidth={2.6} aria-hidden="true" className="flex-shrink-0" />}
<bdi className="truncate">{stop.name}</bdi>
</span>
);
}
/** The thin rule between two stops, read as "then". */
function Then() {
return <span aria-hidden="true" className="h-px w-2 flex-shrink-0 bg-edge" />;
}
function Lead({ stop, timed }: { stop: TimelineStop; timed: boolean }) {
const { t } = useTranslation();
const lead = stop.lead;
if (!lead) return null;
const travel = lead.travelMinutes !== null && lead.travelMinutes > 0;
if (!travel && lead.freeMinutes === null && !lead.overlapsWith && lead.shortBy === null) return null;
return (
<li
className={`flex flex-wrap items-center gap-x-2 text-caption text-content-faint ${timed ? 'ps-[3.25rem]' : 'ps-1'}`}
>
{travel && <span>{t('assistant.change.timeline.travel', { count: lead.travelMinutes ?? 0 })}</span>}
{lead.freeMinutes !== null && <span>{t('assistant.change.timeline.gap', { count: lead.freeMinutes })}</span>}
{lead.shortBy !== null && (
<span className="inline-flex items-center gap-1 font-medium text-warning">
<TriangleAlert size={11} aria-hidden="true" className="flex-shrink-0" />
{t('assistant.change.timeline.tight', { count: lead.shortBy })}
</span>
)}
{lead.overlapsWith && (
<span className="inline-flex items-center gap-1 text-content-muted">
<TriangleAlert size={11} aria-hidden="true" className="flex-shrink-0 text-danger" />
<bdi>{t('assistant.change.timeline.overlap', { place: lead.overlapsWith })}</bdi>
</span>
)}
</li>
);
}
function StopLine({ stop, timed }: { stop: TimelineStop; timed: boolean }) {
const { t } = useTranslation();
const { tripId } = useAssistantView();
const { lit, handlers } = useChangeHover(tripId, stop.toolCallId);
const removed = stop.status === 'removed';
return (
<li
{...(stop.toolCallId ? handlers : {})}
className={`flex items-center gap-2 rounded-md px-1 py-0.5 ${lit ? 'bg-surface-hover' : ''}`}
>
{timed && (
<span className="w-10 flex-shrink-0 text-caption tabular-nums text-content-faint">{stop.time ?? ''}</span>
)}
<bdi className={`min-w-0 truncate text-caption ${removed ? 'text-content-faint line-through' : 'text-content'}`}>
{stop.name}
</bdi>
{stop.status === 'new' && (
<span className="flex-shrink-0 rounded-full bg-surface-secondary px-1.5 text-caption text-content-muted">
{t('assistant.change.timeline.new')}
</span>
)}
</li>
);
}
function DayRow({ day, label, open }: { day: DayAfter; label: string; open: boolean }) {
const timed = day.stops.some((s) => s.time !== null);
const chips = day.stops.slice(0, MAX_CHIPS);
return (
<div>
<bdi className="block truncate text-caption font-semibold uppercase tracking-wide text-content-faint">
{label}
</bdi>
{!open && (
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1.5">
{chips.map((stop, i) => (
<span key={stop.key} className="contents">
{i > 0 && <Then />}
<StopChip stop={stop} />
</span>
))}
{day.stops.length > MAX_CHIPS && (
<span className="text-caption tabular-nums text-content-faint">+{day.stops.length - MAX_CHIPS}</span>
)}
</div>
)}
{open && (
<ol className="trek-assistant-enter mt-1 space-y-0.5" aria-label={label}>
{day.stops.map((stop) => (
<StopGroup key={stop.key} stop={stop} timed={timed} />
))}
</ol>
)}
</div>
);
}
function StopGroup({ stop, timed }: { stop: TimelineStop; timed: boolean }) {
return (
<>
<Lead stop={stop} timed={timed} />
<StopLine stop={stop} timed={timed} />
</>
);
}
export default function AssistantDayBand({ changes }: { changes: readonly DayChange[] }) {
const { t, locale } = useTranslation();
const { tripId } = useAssistantView();
const [open, setOpen] = useState(false);
const plan = useTripStore(
useShallow((s) => ({
here: s.trip?.id === tripId,
days: s.days,
assignments: s.assignments,
places: s.places,
}))
);
const rows = useMemo(() => {
if (!plan.here || changes.length === 0) return [];
return changedDays(changes).flatMap((dayId) => {
const index = plan.days.findIndex((d) => d.id === dayId);
const day = plan.days[index];
if (!day) return [];
const after = buildDayAfter({
dayId,
assignments: plan.assignments,
places: plan.places,
changes,
defaultMode: day.default_transport_mode,
});
return after.stops.length > 0 ? [{ after, label: dayLabel(day, index, t, locale) }] : [];
});
}, [plan, changes, t, locale]);
if (rows.length === 0) return null;
const shown = rows.slice(0, MAX_DAYS);
const issues = rows.reduce((sum, row) => sum + dayIssues(row.after), 0);
const measured = rows.some((row) => row.after.stops.some((stop) => stop.lead?.travelMinutes != null));
const toggleLabel = t(open ? 'assistant.change.timeline.hide' : 'assistant.change.timeline.show');
return (
<div className="mb-1 flex gap-1.5">
<div className="min-w-0 flex-1 space-y-1.5">
{shown.map(({ after, label }) => (
<DayRow key={after.dayId} day={after} label={label} open={open} />
))}
{rows.length > MAX_DAYS && (
<p className="text-caption text-content-faint">
{t('assistant.change.timeline.moreDays', { count: rows.length - MAX_DAYS })}
</p>
)}
</div>
{(issues > 0 || measured) && (
// What the check found, at a glance; a tap opens the schedule that says where.
<button
type="button"
onClick={() => setOpen(true)}
className={`inline-flex h-6 flex-shrink-0 items-center gap-1 self-start rounded-full px-2 text-caption font-medium transition-colors ${
issues > 0 ? 'bg-warning-soft text-warning hover:brightness-95' : 'bg-success-soft text-success'
}`}
>
{issues > 0 ? <TriangleAlert size={12} aria-hidden="true" /> : <CircleCheck size={12} aria-hidden="true" />}
{issues > 0 ? t('assistant.check.issues', { count: issues }) : t('assistant.check.ok')}
</button>
)}
<Tooltip label={toggleLabel} placement="top">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-label={toggleLabel}
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-content-muted outline-none transition-colors hover:bg-surface-hover hover:text-content focus-visible:ring-2 focus-visible:ring-accent"
>
<ChevronDown
size={14}
aria-hidden="true"
className={`transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
/>
</button>
</Tooltip>
</div>
);
}
@@ -0,0 +1,74 @@
import { CircleCheck, TriangleAlert } from 'lucide-react';
import { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useTranslation } from '../../i18n';
import { useTripStore } from '../../store/tripStore';
import { Tooltip } from '../shared/Tooltip';
import { useAssistantView } from './assistantViewContext';
import { buildDayAfter, changedDays, type DayChange, type TimelineStop } from './dayAfterModel';
/** Lines the tooltip names; the rest are only counted on the pill. */
const MAX_LINES = 3;
/** What does not fit on a stop of the day: arriving after the next thing starts, or a way too tight. */
function problemOf(stop: TimelineStop, t: (key: string, params?: Record<string, string | number>) => string) {
const lead = stop.lead;
if (lead?.overlapsWith) return t('assistant.change.timeline.overlap', { place: lead.overlapsWith });
if (lead?.shortBy != null) return `${stop.name}: ${t('assistant.change.timeline.tight', { count: lead.shortBy })}`;
return null;
}
/**
* Whether the days a waiting card changes still work out in time, as one
* small pill in the card's head: "fits" in green, or how many things to look
* at in amber, with what they are on hover. The days themselves show the
* suggestion in the planner's day list, so the card does not draw them again.
* Shown only while the planner holds this trip, whose day plans it reads.
*/
export default function AssistantFitCheck({ changes }: { changes: readonly DayChange[] }) {
const { t } = useTranslation();
const { tripId } = useAssistantView();
const plan = useTripStore(
useShallow((s) => ({ here: s.trip?.id === tripId, days: s.days, assignments: s.assignments, places: s.places }))
);
const check = useMemo(() => {
if (!plan.here || changes.length === 0) return null;
const stops = changedDays(changes).flatMap((dayId) => {
const day = plan.days.find((d) => d.id === dayId);
if (!day) return [];
return buildDayAfter({
dayId,
assignments: plan.assignments,
places: plan.places,
changes,
defaultMode: day.default_transport_mode,
}).stops;
});
const measured = stops.some((stop) => stop.lead?.travelMinutes != null);
const problems = stops.map((stop) => problemOf(stop, t)).filter((line): line is string => line !== null);
return measured || problems.length > 0 ? problems : null;
}, [plan, changes, t]);
if (!check) return null;
const ok = check.length === 0;
const pill = (
<span
className={`inline-flex h-6 flex-shrink-0 items-center gap-1 rounded-full px-2 text-caption font-medium ${
ok ? 'bg-success-soft text-success' : 'bg-warning-soft text-warning'
}`}
>
{ok ? <CircleCheck size={12} aria-hidden="true" /> : <TriangleAlert size={12} aria-hidden="true" />}
{ok ? t('assistant.check.ok') : t('assistant.check.issues', { count: check.length })}
</span>
);
if (ok) return pill;
return (
<Tooltip label={check.slice(0, MAX_LINES).join('\n')} placement="top">
<span tabIndex={0} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-accent">
{pill}
</span>
</Tooltip>
);
}
@@ -10,15 +10,7 @@ import { describe, expect, it } from 'vitest';
import { buildAssignment, buildPlace } from '../../../tests/helpers/factories';
import type { AssignmentsMap } from '../../types';
import {
buildDayAfter,
changedDays,
changePoints,
type DayChange,
dayIssues,
minutesOf,
stopIssue,
} from './dayAfterModel';
import { buildDayAfter, changedDays, changePoints, type DayChange, minutesOf } from './dayAfterModel';
// 0.01° of latitude is 1.112 km: with the detour factor that is 19 minutes on foot, 6 by bike.
const louvre = buildPlace({ id: 10, name: 'Louvre', lat: 48.0, lng: 2.0, duration_minutes: 60 });
@@ -148,14 +140,11 @@ describe('dayAfterModel', () => {
]);
});
it('FE-AI-DAYAFTER-009: a way longer than the time between two stops is too tight, and counts as an issue', () => {
it('FE-AI-DAYAFTER-009: a way longer than the time between two stops is too tight by the difference', () => {
// Louvre until 10:00, then 19 minutes on foot to Orsay, which starts at 10:10: nine minutes short.
const tight = plan();
tight[3] = (tight[3] ?? []).map((a) => (a.id === 101 ? { ...a, assignment_time: '10:10' } : a));
const after = day(3, [], tight);
expect(after.stops[1]?.lead).toMatchObject({ travelMinutes: 19, shortBy: 9 });
expect(after.stops.map(stopIssue)).toEqual([false, true]);
expect(dayIssues(after)).toBe(1);
expect(dayIssues(day(3, []))).toBe(0);
});
});
@@ -234,27 +234,3 @@ export function changedDays(changes: readonly DayChange[]): number[] {
}
return days;
}
/** A leg this long without a time says the day asks a lot of travel. */
const FAR_MINUTES = 60;
/**
* Whether a day still works once the changes ran: stops that take the same
* time, a way there longer than the time between two stops, and legs over an
* hour. Counted over the stops that stay on the day.
*/
export function dayIssues(day: DayAfter): number {
return day.stops.reduce((sum, stop) => {
const lead = stop.lead;
if (stop.status === 'removed' || !lead) return sum;
const far = lead.travelMinutes !== null && lead.travelMinutes >= FAR_MINUTES;
return sum + (lead.overlapsWith || lead.shortBy !== null || far ? 1 : 0);
}, 0);
}
/** Whether a stop is one of those `dayIssues` counts. */
export function stopIssue(stop: TimelineStop): boolean {
const lead = stop.lead;
if (stop.status === 'removed' || !lead) return false;
return !!lead.overlapsWith || lead.shortBy !== null || (lead.travelMinutes ?? 0) >= FAR_MINUTES;
}
+2
View File
@@ -332,6 +332,8 @@ export const JOURNEY_RECORDED_DAY_SEGMENTS: JsonColumn<DawarichTrackSegment[]> =
schema: z.array(dawarichTrackSegmentSchema),
fallback: () => [],
};
/**
* The parts of an assistant message as the client renders them: text, tool
* calls with their results and approvals, change previews and receipts. A
* broken row reads as an empty message and is logged.
-24
View File
@@ -141,7 +141,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'تغيّر في هذه الأثناء. يرجى تعديله يدويًا.',
'assistant.change.undoFailed': 'تعذر التراجع عن هذا.',
'assistant.change.showOnMap': 'عرض على الخريطة',
'assistant.change.empty': 'فارغ',
'assistant.change.pending': '{count} تغيير بانتظارك',
'assistant.change.pending.zero': 'لا تغييرات بانتظارك',
'assistant.change.pending.one': 'تغيير واحد بانتظارك',
@@ -268,28 +267,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'يبحث بالقرب من {place}',
'assistant.step.searchingFor': 'يبحث عن {query}',
'assistant.step.weatherAt': 'يتحقق من الطقس في {place}',
'assistant.change.timeline.show': 'عرض الجدول',
'assistant.change.timeline.hide': 'إخفاء الجدول',
'assistant.change.timeline.new': 'جديد',
'assistant.change.timeline.travel': '≈ {count} دقيقة تنقل',
'assistant.change.timeline.travel.zero': '≈ {count} دقيقة تنقل',
'assistant.change.timeline.travel.one': '≈ دقيقة تنقل واحدة',
'assistant.change.timeline.travel.two': '≈ دقيقتا تنقل',
'assistant.change.timeline.travel.few': '≈ {count} دقائق تنقل',
'assistant.change.timeline.travel.many': '≈ {count} دقيقة تنقل',
'assistant.change.timeline.gap': '{count} دقيقة فراغ',
'assistant.change.timeline.gap.zero': '{count} دقيقة فراغ',
'assistant.change.timeline.gap.one': 'دقيقة فراغ واحدة',
'assistant.change.timeline.gap.two': 'دقيقتا فراغ',
'assistant.change.timeline.gap.few': '{count} دقائق فراغ',
'assistant.change.timeline.gap.many': '{count} دقيقة فراغ',
'assistant.change.timeline.overlap': 'يتداخل مع {place}',
'assistant.change.timeline.moreDays': '{count} يوم آخر',
'assistant.change.timeline.moreDays.zero': 'لا أيام أخرى',
'assistant.change.timeline.moreDays.one': 'يوم آخر',
'assistant.change.timeline.moreDays.two': 'يومان آخران',
'assistant.change.timeline.moreDays.few': '{count} أيام أخرى',
'assistant.change.timeline.moreDays.many': '{count} يومًا آخر',
'assistant.change.viewOnMap': 'عرض على الخريطة',
'assistant.change.before': 'قبل',
'assistant.change.after': 'بعد',
@@ -490,8 +468,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.many': 'عرض التغييرات الـ {count}',
'assistant.change.verb.updateBudgetItem': 'تعديل المصروف',
'assistant.change.verb.rememberNote': 'تذكّر لهذه الرحلة',
'assistant.change.details': 'التفاصيل',
'assistant.change.detailsHide': 'إخفاء التفاصيل',
'assistant.change.verb.genericCreate': 'إضافة',
'assistant.change.verb.genericUpdate': 'تغيير',
'assistant.change.verb.genericRemove': 'إزالة',
-12
View File
@@ -133,7 +133,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Bu arada dəyişdirilib. Zəhmət olmasa, əl ilə düzəldin.',
'assistant.change.undoFailed': 'Bunu geri almaq olmadı.',
'assistant.change.showOnMap': 'Xəritədə göstər',
'assistant.change.empty': 'boş',
'assistant.change.pending': '{count} dəyişiklik sizi gözləyir',
'assistant.change.pending.one': '{count} dəyişiklik sizi gözləyir',
'assistant.change.verb.addPlace': 'Yer əlavə et',
@@ -249,16 +248,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': '{place} yaxınlığında axtarır',
'assistant.step.searchingFor': '{query} axtarır',
'assistant.step.weatherAt': '{place} üçün havaya baxır',
'assistant.change.timeline.show': 'Cədvəli göstər',
'assistant.change.timeline.hide': 'Cədvəli gizlət',
'assistant.change.timeline.new': 'Yeni',
'assistant.change.timeline.travel': '≈ {count} dəq yol',
'assistant.change.timeline.travel.one': '≈ {count} dəq yol',
'assistant.change.timeline.gap': '{count} dəq boş',
'assistant.change.timeline.gap.one': '{count} dəq boş',
'assistant.change.timeline.overlap': '{place} ilə üst-üstə düşür',
'assistant.change.timeline.moreDays': 'Daha {count} gün',
'assistant.change.timeline.moreDays.one': 'Daha {count} gün',
'assistant.change.viewOnMap': 'Xəritədə bax',
'assistant.change.before': 'Əvvəl',
'assistant.change.after': 'Sonra',
@@ -398,8 +388,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': '{count} dəyişikliyi göstər',
'assistant.change.verb.updateBudgetItem': 'Xərci dəyiş',
'assistant.change.verb.rememberNote': 'Bu səyahət üçün yadda saxla',
'assistant.change.details': 'Təfərrüatlar',
'assistant.change.detailsHide': 'Təfərrüatları gizlət',
'assistant.change.verb.genericCreate': 'Əlavə et',
'assistant.change.verb.genericUpdate': 'Dəyiş',
'assistant.change.verb.genericRemove': 'Sil',
-12
View File
@@ -134,7 +134,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Foi alterado nesse meio-tempo. Ajuste manualmente.',
'assistant.change.undoFailed': 'Não foi possível desfazer.',
'assistant.change.showOnMap': 'Mostrar no mapa',
'assistant.change.empty': 'vazio',
'assistant.change.pending': '{count} mudanças esperam por você',
'assistant.change.pending.one': '{count} mudança espera por você',
'assistant.change.verb.addPlace': 'Adicionar lugar',
@@ -249,16 +248,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Buscando perto de {place}',
'assistant.step.searchingFor': 'Procurando {query}',
'assistant.step.weatherAt': 'Verificando o tempo em {place}',
'assistant.change.timeline.show': 'Mostrar roteiro',
'assistant.change.timeline.hide': 'Ocultar roteiro',
'assistant.change.timeline.new': 'Novo',
'assistant.change.timeline.travel': '≈ {count} min de trajeto',
'assistant.change.timeline.travel.one': '≈ {count} min de trajeto',
'assistant.change.timeline.gap': '{count} min livres',
'assistant.change.timeline.gap.one': '{count} min livre',
'assistant.change.timeline.overlap': 'Coincide com {place}',
'assistant.change.timeline.moreDays': 'Mais {count} dias',
'assistant.change.timeline.moreDays.one': 'Mais {count} dia',
'assistant.change.viewOnMap': 'Ver no mapa',
'assistant.change.before': 'Antes',
'assistant.change.after': 'Depois',
@@ -398,8 +388,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': "Diskouez {count} c'hemm",
'assistant.change.verb.updateBudgetItem': 'Kemmañ an dispign',
'assistant.change.verb.rememberNote': 'Derc’hel soñj evit ar veaj-mañ',
'assistant.change.details': 'Munudoù',
'assistant.change.detailsHide': 'Kuzhat ar munudoù',
'assistant.change.verb.genericCreate': 'Ouzhpennañ',
'assistant.change.verb.genericUpdate': 'Kemmañ',
'assistant.change.verb.genericRemove': 'Lemel',
-12
View File
@@ -134,7 +134,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Ha canviat mentrestant. Ajusta-ho a mà.',
'assistant.change.undoFailed': 'No s’ha pogut desfer.',
'assistant.change.showOnMap': 'Mostra al mapa',
'assistant.change.empty': 'buit',
'assistant.change.pending': 'Tens {count} canvis pendents',
'assistant.change.pending.one': 'Tens {count} canvi pendent',
'assistant.change.verb.addPlace': 'Afegeix un lloc',
@@ -251,16 +250,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Cercant a prop de {place}',
'assistant.step.searchingFor': 'Cercant {query}',
'assistant.step.weatherAt': 'Consultant el temps a {place}',
'assistant.change.timeline.show': 'Mostra l’horari',
'assistant.change.timeline.hide': 'Amaga l’horari',
'assistant.change.timeline.new': 'Nou',
'assistant.change.timeline.travel': '≈ {count} min de trajecte',
'assistant.change.timeline.travel.one': '≈ {count} min de trajecte',
'assistant.change.timeline.gap': '{count} min lliures',
'assistant.change.timeline.gap.one': '{count} min lliure',
'assistant.change.timeline.overlap': 'Coincideix amb {place}',
'assistant.change.timeline.moreDays': '{count} dies més',
'assistant.change.timeline.moreDays.one': '{count} dia més',
'assistant.change.viewOnMap': 'Mira-ho al mapa',
'assistant.change.before': 'Abans',
'assistant.change.after': 'Després',
@@ -401,8 +391,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Mostra {count} canvi',
'assistant.change.verb.updateBudgetItem': 'Canvia la despesa',
'assistant.change.verb.rememberNote': 'Recorda per a aquest viatge',
'assistant.change.details': 'Detalls',
'assistant.change.detailsHide': 'Amaga els detalls',
'assistant.change.verb.genericCreate': 'Afegeix',
'assistant.change.verb.genericUpdate': 'Canvia',
'assistant.change.verb.genericRemove': 'Elimina',
-15
View File
@@ -135,7 +135,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Mezitím se to změnilo. Uprav to prosím ručně.',
'assistant.change.undoFailed': 'Tohle se nepodařilo vrátit.',
'assistant.change.showOnMap': 'Zobrazit na mapě',
'assistant.change.empty': 'prázdné',
'assistant.change.pending': 'Čeká na tebe {count} změn',
'assistant.change.pending.one': 'Čeká na tebe {count} změna',
'assistant.change.pending.few': 'Čekají na tebe {count} změny',
@@ -253,19 +252,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Hledá poblíž místa {place}',
'assistant.step.searchingFor': 'Hledá {query}',
'assistant.step.weatherAt': 'Zjišťuje počasí v místě {place}',
'assistant.change.timeline.show': 'Zobrazit program',
'assistant.change.timeline.hide': 'Skrýt program',
'assistant.change.timeline.new': 'Nové',
'assistant.change.timeline.travel': '≈ {count} min cesty',
'assistant.change.timeline.travel.one': '≈ {count} min cesty',
'assistant.change.timeline.travel.few': '≈ {count} min cesty',
'assistant.change.timeline.gap': '{count} min volno',
'assistant.change.timeline.gap.one': '{count} min volno',
'assistant.change.timeline.gap.few': '{count} min volno',
'assistant.change.timeline.overlap': 'Překrývá se s {place}',
'assistant.change.timeline.moreDays': 'Dalších {count} dní',
'assistant.change.timeline.moreDays.one': 'Další {count} den',
'assistant.change.timeline.moreDays.few': 'Další {count} dny',
'assistant.change.viewOnMap': 'Zobrazit na mapě',
'assistant.change.before': 'Předtím',
'assistant.change.after': 'Potom',
@@ -420,8 +407,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.few': 'Zobrazit všechny {count} změny',
'assistant.change.verb.updateBudgetItem': 'Změnit výdaj',
'assistant.change.verb.rememberNote': 'Zapamatovat pro tuto cestu',
'assistant.change.details': 'Podrobnosti',
'assistant.change.detailsHide': 'Skrýt podrobnosti',
'assistant.change.verb.genericCreate': 'Přidat',
'assistant.change.verb.genericUpdate': 'Změnit',
'assistant.change.verb.genericRemove': 'Odebrat',
-12
View File
@@ -133,7 +133,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Inzwischen geändert. Bitte pass es von Hand an.',
'assistant.change.undoFailed': 'Das ließ sich nicht rückgängig machen.',
'assistant.change.showOnMap': 'Auf Karte zeigen',
'assistant.change.empty': 'leer',
'assistant.change.pending': '{count} Änderungen warten auf deine Bestätigung',
'assistant.change.pending.one': '{count} Änderung wartet auf deine Bestätigung',
'assistant.change.verb.addPlace': 'Ort hinzufügen',
@@ -248,16 +247,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Sucht in der Nähe von {place}',
'assistant.step.searchingFor': 'Sucht nach {query}',
'assistant.step.weatherAt': 'Prüft das Wetter in {place}',
'assistant.change.timeline.show': 'Ablauf zeigen',
'assistant.change.timeline.hide': 'Ablauf ausblenden',
'assistant.change.timeline.new': 'Neu',
'assistant.change.timeline.travel': '≈ {count} Min. Weg',
'assistant.change.timeline.travel.one': '≈ {count} Min. Weg',
'assistant.change.timeline.gap': '{count} Min. frei',
'assistant.change.timeline.gap.one': '{count} Min. frei',
'assistant.change.timeline.overlap': 'Überschneidet sich mit {place}',
'assistant.change.timeline.moreDays': '{count} weitere Tage',
'assistant.change.timeline.moreDays.one': '{count} weiterer Tag',
'assistant.change.viewOnMap': 'Auf Karte ansehen',
'assistant.change.before': 'Vorher',
'assistant.change.after': 'Nachher',
@@ -398,8 +388,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': '{count} Änderung zeigen',
'assistant.change.verb.updateBudgetItem': 'Ausgabe ändern',
'assistant.change.verb.rememberNote': 'Für diese Reise merken',
'assistant.change.details': 'Mehr Angaben',
'assistant.change.detailsHide': 'Angaben ausblenden',
'assistant.change.verb.genericCreate': 'Hinzufügen',
'assistant.change.verb.genericUpdate': 'Ändern',
'assistant.change.verb.genericRemove': 'Entfernen',
-12
View File
@@ -130,7 +130,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Changed in the meantime. Please adjust it by hand.',
'assistant.change.undoFailed': 'Couldn’t undo this.',
'assistant.change.showOnMap': 'Show on map',
'assistant.change.empty': 'empty',
'assistant.change.pending': '{count} changes are waiting for you',
'assistant.change.pending.one': '{count} change is waiting for you',
'assistant.change.verb.addPlace': 'Add place',
@@ -245,16 +244,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Searching near {place}',
'assistant.step.searchingFor': 'Looking for {query}',
'assistant.step.weatherAt': 'Checking the weather in {place}',
'assistant.change.timeline.show': 'Show schedule',
'assistant.change.timeline.hide': 'Hide schedule',
'assistant.change.timeline.new': 'New',
'assistant.change.timeline.travel': '≈ {count} min travel',
'assistant.change.timeline.travel.one': '≈ {count} min travel',
'assistant.change.timeline.gap': '{count} min free',
'assistant.change.timeline.gap.one': '{count} min free',
'assistant.change.timeline.overlap': 'Overlaps with {place}',
'assistant.change.timeline.moreDays': '{count} more days',
'assistant.change.timeline.moreDays.one': '{count} more day',
'assistant.change.viewOnMap': 'View on map',
'assistant.change.before': 'Before',
'assistant.change.after': 'After',
@@ -393,8 +383,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Show {count} change',
'assistant.change.verb.updateBudgetItem': 'Change expense',
'assistant.change.verb.rememberNote': 'Remember for this trip',
'assistant.change.details': 'Details',
'assistant.change.detailsHide': 'Hide details',
'assistant.change.verb.genericCreate': 'Add',
'assistant.change.verb.genericUpdate': 'Change',
'assistant.change.verb.genericRemove': 'Remove',
-12
View File
@@ -132,7 +132,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Ha cambiado mientras tanto. Ajústalo a mano.',
'assistant.change.undoFailed': 'No se pudo deshacer.',
'assistant.change.showOnMap': 'Mostrar en el mapa',
'assistant.change.empty': 'vacío',
'assistant.change.pending': '{count} cambios te esperan',
'assistant.change.pending.one': '{count} cambio te espera',
'assistant.change.verb.addPlace': 'Añadir lugar',
@@ -247,16 +246,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Buscando cerca de {place}',
'assistant.step.searchingFor': 'Buscando {query}',
'assistant.step.weatherAt': 'Consultando el tiempo en {place}',
'assistant.change.timeline.show': 'Ver horario',
'assistant.change.timeline.hide': 'Ocultar horario',
'assistant.change.timeline.new': 'Nuevo',
'assistant.change.timeline.travel': '≈ {count} min de trayecto',
'assistant.change.timeline.travel.one': '≈ {count} min de trayecto',
'assistant.change.timeline.gap': '{count} min libres',
'assistant.change.timeline.gap.one': '{count} min libre',
'assistant.change.timeline.overlap': 'Se solapa con {place}',
'assistant.change.timeline.moreDays': '{count} días más',
'assistant.change.timeline.moreDays.one': '{count} día más',
'assistant.change.viewOnMap': 'Ver en el mapa',
'assistant.change.before': 'Antes',
'assistant.change.after': 'Después',
@@ -396,8 +386,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Ver {count} cambio',
'assistant.change.verb.updateBudgetItem': 'Cambiar gasto',
'assistant.change.verb.rememberNote': 'Recordar para este viaje',
'assistant.change.details': 'Detalles',
'assistant.change.detailsHide': 'Ocultar detalles',
'assistant.change.verb.genericCreate': 'Añadir',
'assistant.change.verb.genericUpdate': 'Cambiar',
'assistant.change.verb.genericRemove': 'Quitar',
-12
View File
@@ -133,7 +133,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Vahepeal muudetud. Kohanda palun käsitsi.',
'assistant.change.undoFailed': 'Seda ei õnnestunud tagasi võtta.',
'assistant.change.showOnMap': 'Näita kaardil',
'assistant.change.empty': 'tühi',
'assistant.change.pending': '{count} muudatust ootab sind',
'assistant.change.pending.one': '{count} muudatus ootab sind',
'assistant.change.verb.addPlace': 'Lisa koht',
@@ -248,16 +247,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Otsin {place} lähedalt',
'assistant.step.searchingFor': 'Otsin: {query}',
'assistant.step.weatherAt': 'Vaatan ilma kohas {place}',
'assistant.change.timeline.show': 'Näita ajakava',
'assistant.change.timeline.hide': 'Peida ajakava',
'assistant.change.timeline.new': 'Uus',
'assistant.change.timeline.travel': '≈ {count} min teed',
'assistant.change.timeline.travel.one': '≈ {count} min teed',
'assistant.change.timeline.gap': '{count} min vaba',
'assistant.change.timeline.gap.one': '{count} min vaba',
'assistant.change.timeline.overlap': 'Kattub kohaga {place}',
'assistant.change.timeline.moreDays': 'Veel {count} päeva',
'assistant.change.timeline.moreDays.one': 'Veel {count} päev',
'assistant.change.viewOnMap': 'Vaata kaardil',
'assistant.change.before': 'Enne',
'assistant.change.after': 'Pärast',
@@ -397,8 +387,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Näita {count} muudatust',
'assistant.change.verb.updateBudgetItem': 'Muuda kulu',
'assistant.change.verb.rememberNote': 'Jäta selle reisi jaoks meelde',
'assistant.change.details': 'Üksikasjad',
'assistant.change.detailsHide': 'Peida üksikasjad',
'assistant.change.verb.genericCreate': 'Lisa',
'assistant.change.verb.genericUpdate': 'Muuda',
'assistant.change.verb.genericRemove': 'Eemalda',
-12
View File
@@ -134,7 +134,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Modifié entre-temps. Veuillez l’ajuster à la main.',
'assistant.change.undoFailed': 'Impossible d’annuler ceci.',
'assistant.change.showOnMap': 'Afficher sur la carte',
'assistant.change.empty': 'vide',
'assistant.change.pending': '{count} modifications vous attendent',
'assistant.change.pending.one': '{count} modification vous attend',
'assistant.change.verb.addPlace': 'Ajouter un lieu',
@@ -252,16 +251,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Recherche près de {place}',
'assistant.step.searchingFor': 'Recherche de {query}',
'assistant.step.weatherAt': 'Vérifie la météo à {place}',
'assistant.change.timeline.show': 'Afficher le déroulé',
'assistant.change.timeline.hide': 'Masquer le déroulé',
'assistant.change.timeline.new': 'Nouveau',
'assistant.change.timeline.travel': '≈ {count} min de trajet',
'assistant.change.timeline.travel.one': '≈ {count} min de trajet',
'assistant.change.timeline.gap': '{count} min libres',
'assistant.change.timeline.gap.one': '{count} min libre',
'assistant.change.timeline.overlap': 'Chevauche {place}',
'assistant.change.timeline.moreDays': '{count} autres jours',
'assistant.change.timeline.moreDays.one': '{count} autre jour',
'assistant.change.viewOnMap': 'Voir sur la carte',
'assistant.change.before': 'Avant',
'assistant.change.after': 'Après',
@@ -403,8 +393,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Afficher {count} modification',
'assistant.change.verb.updateBudgetItem': 'Modifier la dépense',
'assistant.change.verb.rememberNote': 'Retenir pour ce voyage',
'assistant.change.details': 'Détails',
'assistant.change.detailsHide': 'Masquer les détails',
'assistant.change.verb.genericCreate': 'Ajouter',
'assistant.change.verb.genericUpdate': 'Modifier',
'assistant.change.verb.genericRemove': 'Supprimer',
-12
View File
@@ -136,7 +136,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Άλλαξε στο μεταξύ. Προσαρμόστε το χειροκίνητα.',
'assistant.change.undoFailed': 'Δεν ήταν δυνατή η αναίρεση.',
'assistant.change.showOnMap': 'Εμφάνιση στον χάρτη',
'assistant.change.empty': 'κενό',
'assistant.change.pending': '{count} αλλαγές σας περιμένουν',
'assistant.change.pending.one': '{count} αλλαγή σας περιμένει',
'assistant.change.verb.addPlace': 'Προσθήκη μέρους',
@@ -253,16 +252,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Αναζήτηση κοντά σε {place}',
'assistant.step.searchingFor': 'Αναζήτηση για {query}',
'assistant.step.weatherAt': 'Έλεγχος καιρού σε {place}',
'assistant.change.timeline.show': 'Εμφάνιση προγράμματος',
'assistant.change.timeline.hide': 'Απόκρυψη προγράμματος',
'assistant.change.timeline.new': 'Νέο',
'assistant.change.timeline.travel': '≈ {count} λεπτά διαδρομή',
'assistant.change.timeline.travel.one': '≈ {count} λεπτό διαδρομή',
'assistant.change.timeline.gap': '{count} λεπτά ελεύθερα',
'assistant.change.timeline.gap.one': '{count} λεπτό ελεύθερο',
'assistant.change.timeline.overlap': 'Επικαλύπτεται με {place}',
'assistant.change.timeline.moreDays': 'Ακόμη {count} ημέρες',
'assistant.change.timeline.moreDays.one': 'Ακόμη {count} ημέρα',
'assistant.change.viewOnMap': 'Δείτε στον χάρτη',
'assistant.change.before': 'Πριν',
'assistant.change.after': 'Μετά',
@@ -402,8 +392,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Εμφάνιση {count} αλλαγής',
'assistant.change.verb.updateBudgetItem': 'Αλλαγή εξόδου',
'assistant.change.verb.rememberNote': 'Απομνημόνευση για αυτό το ταξίδι',
'assistant.change.details': 'Λεπτομέρειες',
'assistant.change.detailsHide': 'Απόκρυψη λεπτομερειών',
'assistant.change.verb.genericCreate': 'Προσθήκη',
'assistant.change.verb.genericUpdate': 'Αλλαγή',
'assistant.change.verb.genericRemove': 'Αφαίρεση',
-12
View File
@@ -135,7 +135,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Időközben megváltozott. Kérlek, igazítsd kézzel.',
'assistant.change.undoFailed': 'Nem sikerült visszavonni.',
'assistant.change.showOnMap': 'Megjelenítés a térképen',
'assistant.change.empty': 'üres',
'assistant.change.pending': '{count} módosítás vár rád',
'assistant.change.pending.one': '{count} módosítás vár rád',
'assistant.change.verb.addPlace': 'Hely hozzáadása',
@@ -253,16 +252,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Keresés itt: {place} környéke',
'assistant.step.searchingFor': 'Keresés: {query}',
'assistant.step.weatherAt': 'Időjárás ellenőrzése: {place}',
'assistant.change.timeline.show': 'Menetrend mutatása',
'assistant.change.timeline.hide': 'Menetrend elrejtése',
'assistant.change.timeline.new': 'Új',
'assistant.change.timeline.travel': '≈ {count} perc út',
'assistant.change.timeline.travel.one': '≈ {count} perc út',
'assistant.change.timeline.gap': '{count} perc szabad',
'assistant.change.timeline.gap.one': '{count} perc szabad',
'assistant.change.timeline.overlap': 'Ütközik ezzel: {place}',
'assistant.change.timeline.moreDays': 'További {count} nap',
'assistant.change.timeline.moreDays.one': 'További {count} nap',
'assistant.change.viewOnMap': 'Megnézem a térképen',
'assistant.change.before': 'Előtte',
'assistant.change.after': 'Utána',
@@ -403,8 +393,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': '{count} módosítás mutatása',
'assistant.change.verb.updateBudgetItem': 'Kiadás módosítása',
'assistant.change.verb.rememberNote': 'Megjegyezni ehhez az utazáshoz',
'assistant.change.details': 'Részletek',
'assistant.change.detailsHide': 'Részletek elrejtése',
'assistant.change.verb.genericCreate': 'Hozzáadás',
'assistant.change.verb.genericUpdate': 'Módosítás',
'assistant.change.verb.genericRemove': 'Eltávolítás',
-9
View File
@@ -129,7 +129,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Sudah berubah sementara itu. Silakan sesuaikan secara manual.',
'assistant.change.undoFailed': 'Tidak bisa mengurungkan ini.',
'assistant.change.showOnMap': 'Tampilkan di peta',
'assistant.change.empty': 'kosong',
'assistant.change.pending': '{count} perubahan menunggumu',
'assistant.change.verb.addPlace': 'Tambah tempat',
'assistant.change.verb.addPlaceToDay': 'Tambah tempat ke hari',
@@ -243,13 +242,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Mencari di sekitar {place}',
'assistant.step.searchingFor': 'Mencari {query}',
'assistant.step.weatherAt': 'Memeriksa cuaca di {place}',
'assistant.change.timeline.show': 'Tampilkan jadwal',
'assistant.change.timeline.hide': 'Sembunyikan jadwal',
'assistant.change.timeline.new': 'Baru',
'assistant.change.timeline.travel': '≈ {count} menit perjalanan',
'assistant.change.timeline.gap': '{count} menit kosong',
'assistant.change.timeline.overlap': 'Bertabrakan dengan {place}',
'assistant.change.timeline.moreDays': '{count} hari lagi',
'assistant.change.viewOnMap': 'Lihat di peta',
'assistant.change.before': 'Sebelum',
'assistant.change.after': 'Sesudah',
@@ -375,8 +368,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': 'Tampilkan semua {count} perubahan',
'assistant.change.verb.updateBudgetItem': 'Ubah pengeluaran',
'assistant.change.verb.rememberNote': 'Ingat untuk perjalanan ini',
'assistant.change.details': 'Detail',
'assistant.change.detailsHide': 'Sembunyikan detail',
'assistant.change.verb.genericCreate': 'Tambah',
'assistant.change.verb.genericUpdate': 'Ubah',
'assistant.change.verb.genericRemove': 'Hapus',
-12
View File
@@ -135,7 +135,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Nel frattempo è cambiata. Sistemala a mano.',
'assistant.change.undoFailed': 'Impossibile annullarla.',
'assistant.change.showOnMap': 'Mostra sulla mappa',
'assistant.change.empty': 'vuoto',
'assistant.change.pending': '{count} modifiche ti aspettano',
'assistant.change.pending.one': '{count} modifica ti aspetta',
'assistant.change.verb.addPlace': 'Aggiungi luogo',
@@ -251,16 +250,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Cerco vicino a {place}',
'assistant.step.searchingFor': 'Cerco {query}',
'assistant.step.weatherAt': 'Controllo il meteo a {place}',
'assistant.change.timeline.show': 'Mostra programma',
'assistant.change.timeline.hide': 'Nascondi programma',
'assistant.change.timeline.new': 'Nuovo',
'assistant.change.timeline.travel': '≈ {count} min di tragitto',
'assistant.change.timeline.travel.one': '≈ {count} min di tragitto',
'assistant.change.timeline.gap': '{count} min liberi',
'assistant.change.timeline.gap.one': '{count} min libero',
'assistant.change.timeline.overlap': 'Si sovrappone a {place}',
'assistant.change.timeline.moreDays': 'Altri {count} giorni',
'assistant.change.timeline.moreDays.one': 'Altro {count} giorno',
'assistant.change.viewOnMap': 'Vedi sulla mappa',
'assistant.change.before': 'Prima',
'assistant.change.after': 'Dopo',
@@ -404,8 +394,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Mostra {count} modifica',
'assistant.change.verb.updateBudgetItem': 'Modifica spesa',
'assistant.change.verb.rememberNote': 'Ricorda per questo viaggio',
'assistant.change.details': 'Dettagli',
'assistant.change.detailsHide': 'Nascondi dettagli',
'assistant.change.verb.genericCreate': 'Aggiungi',
'assistant.change.verb.genericUpdate': 'Modifica',
'assistant.change.verb.genericRemove': 'Rimuovi',
-9
View File
@@ -129,7 +129,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'その間に変更されています。手動で調整してください。',
'assistant.change.undoFailed': '元に戻せませんでした。',
'assistant.change.showOnMap': '地図に表示',
'assistant.change.empty': '空',
'assistant.change.pending': '{count} 件の変更が確認待ちです',
'assistant.change.verb.addPlace': '場所を追加',
'assistant.change.verb.addPlaceToDay': '日程に場所を追加',
@@ -242,13 +241,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': '{place}の近くを検索中',
'assistant.step.searchingFor': '{query}を検索中',
'assistant.step.weatherAt': '{place}の天気を確認中',
'assistant.change.timeline.show': '予定を表示',
'assistant.change.timeline.hide': '予定を隠す',
'assistant.change.timeline.new': '新規',
'assistant.change.timeline.travel': '移動 約{count}分',
'assistant.change.timeline.gap': '空き {count}分',
'assistant.change.timeline.overlap': '{place}と重なっています',
'assistant.change.timeline.moreDays': 'ほか{count}日',
'assistant.change.viewOnMap': '地図で見る',
'assistant.change.before': '変更前',
'assistant.change.after': '変更後',
@@ -370,8 +363,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': '{count} 件の変更をすべて表示',
'assistant.change.verb.updateBudgetItem': '支出を変更',
'assistant.change.verb.rememberNote': 'この旅行のために覚えておく',
'assistant.change.details': '詳細',
'assistant.change.detailsHide': '詳細を隠す',
'assistant.change.verb.genericCreate': '追加',
'assistant.change.verb.genericUpdate': '変更',
'assistant.change.verb.genericRemove': '削除',
-9
View File
@@ -128,7 +128,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': '그 사이에 변경되었습니다. 직접 조정해 주세요.',
'assistant.change.undoFailed': '되돌리지 못했습니다.',
'assistant.change.showOnMap': '지도에 표시',
'assistant.change.empty': '비어 있음',
'assistant.change.pending': '{count}개의 변경이 확인을 기다리고 있습니다',
'assistant.change.verb.addPlace': '장소 추가',
'assistant.change.verb.addPlaceToDay': '날짜에 장소 추가',
@@ -240,13 +239,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': '{place} 근처를 찾는 중',
'assistant.step.searchingFor': '{query} 찾는 중',
'assistant.step.weatherAt': '{place}의 날씨 확인 중',
'assistant.change.timeline.show': '일정 보기',
'assistant.change.timeline.hide': '일정 숨기기',
'assistant.change.timeline.new': '새 항목',
'assistant.change.timeline.travel': '이동 약 {count}분',
'assistant.change.timeline.gap': '{count}분 비어 있음',
'assistant.change.timeline.overlap': '{place}와 겹침',
'assistant.change.timeline.moreDays': '{count}일 더',
'assistant.change.viewOnMap': '지도에서 보기',
'assistant.change.before': '변경 전',
'assistant.change.after': '변경 후',
@@ -367,8 +360,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': '변경 {count}개 모두 보기',
'assistant.change.verb.updateBudgetItem': '지출 변경',
'assistant.change.verb.rememberNote': '이 여행에 기억하기',
'assistant.change.details': '세부 정보',
'assistant.change.detailsHide': '세부 정보 숨기기',
'assistant.change.verb.genericCreate': '추가',
'assistant.change.verb.genericUpdate': '변경',
'assistant.change.verb.genericRemove': '삭제',
-12
View File
@@ -133,7 +133,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Intussen gewijzigd. Pas het met de hand aan.',
'assistant.change.undoFailed': 'Kon dit niet ongedaan maken.',
'assistant.change.showOnMap': 'Op kaart tonen',
'assistant.change.empty': 'leeg',
'assistant.change.pending': '{count} wijzigingen wachten op je',
'assistant.change.pending.one': '{count} wijziging wacht op je',
'assistant.change.verb.addPlace': 'Plek toevoegen',
@@ -249,16 +248,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Zoekt in de buurt van {place}',
'assistant.step.searchingFor': 'Zoekt naar {query}',
'assistant.step.weatherAt': 'Bekijkt het weer in {place}',
'assistant.change.timeline.show': 'Planning tonen',
'assistant.change.timeline.hide': 'Planning verbergen',
'assistant.change.timeline.new': 'Nieuw',
'assistant.change.timeline.travel': '≈ {count} min onderweg',
'assistant.change.timeline.travel.one': '≈ {count} min onderweg',
'assistant.change.timeline.gap': '{count} min vrij',
'assistant.change.timeline.gap.one': '{count} min vrij',
'assistant.change.timeline.overlap': 'Overlapt met {place}',
'assistant.change.timeline.moreDays': 'Nog {count} dagen',
'assistant.change.timeline.moreDays.one': 'Nog {count} dag',
'assistant.change.viewOnMap': 'Bekijk op de kaart',
'assistant.change.before': 'Voor',
'assistant.change.after': 'Na',
@@ -399,8 +389,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': '{count} wijziging tonen',
'assistant.change.verb.updateBudgetItem': 'Uitgave wijzigen',
'assistant.change.verb.rememberNote': 'Onthouden voor deze reis',
'assistant.change.details': 'Meer gegevens',
'assistant.change.detailsHide': 'Gegevens verbergen',
'assistant.change.verb.genericCreate': 'Toevoegen',
'assistant.change.verb.genericUpdate': 'Wijzigen',
'assistant.change.verb.genericRemove': 'Verwijderen',
-18
View File
@@ -138,7 +138,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'W międzyczasie coś się zmieniło. Popraw to ręcznie.',
'assistant.change.undoFailed': 'Nie udało się tego cofnąć.',
'assistant.change.showOnMap': 'Pokaż na mapie',
'assistant.change.empty': 'puste',
'assistant.change.pending': '{count} zmiany czeka na Ciebie',
'assistant.change.pending.one': '{count} zmiana czeka na Ciebie',
'assistant.change.pending.few': '{count} zmiany czekają na Ciebie',
@@ -260,22 +259,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Szuka w pobliżu: {place}',
'assistant.step.searchingFor': 'Szuka: {query}',
'assistant.step.weatherAt': 'Sprawdza pogodę: {place}',
'assistant.change.timeline.show': 'Pokaż plan dnia',
'assistant.change.timeline.hide': 'Ukryj plan dnia',
'assistant.change.timeline.new': 'Nowe',
'assistant.change.timeline.travel': '≈ {count} min drogi',
'assistant.change.timeline.travel.one': '≈ {count} min drogi',
'assistant.change.timeline.travel.few': '≈ {count} min drogi',
'assistant.change.timeline.travel.many': '≈ {count} min drogi',
'assistant.change.timeline.gap': '{count} min wolnego',
'assistant.change.timeline.gap.one': '{count} min wolnego',
'assistant.change.timeline.gap.few': '{count} min wolnego',
'assistant.change.timeline.gap.many': '{count} min wolnego',
'assistant.change.timeline.overlap': 'Nakłada się na {place}',
'assistant.change.timeline.moreDays': 'Jeszcze {count} dnia',
'assistant.change.timeline.moreDays.one': 'Jeszcze {count} dzień',
'assistant.change.timeline.moreDays.few': 'Jeszcze {count} dni',
'assistant.change.timeline.moreDays.many': 'Jeszcze {count} dni',
'assistant.change.viewOnMap': 'Zobacz na mapie',
'assistant.change.before': 'Przed',
'assistant.change.after': 'Po',
@@ -447,8 +431,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.many': 'Pokaż wszystkie {count} zmian',
'assistant.change.verb.updateBudgetItem': 'Zmień wydatek',
'assistant.change.verb.rememberNote': 'Zapamiętaj dla tej podróży',
'assistant.change.details': 'Szczegóły',
'assistant.change.detailsHide': 'Ukryj szczegóły',
'assistant.change.verb.genericCreate': 'Dodaj',
'assistant.change.verb.genericUpdate': 'Zmień',
'assistant.change.verb.genericRemove': 'Usuń',
-18
View File
@@ -138,7 +138,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'За это время что-то изменилось. Поправьте вручную.',
'assistant.change.undoFailed': 'Не удалось отменить.',
'assistant.change.showOnMap': 'Показать на карте',
'assistant.change.empty': 'пусто',
'assistant.change.pending': '{count} изменения ждут вас',
'assistant.change.pending.one': '{count} изменение ждёт вас',
'assistant.change.pending.few': '{count} изменения ждут вас',
@@ -259,22 +258,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Ищет рядом: {place}',
'assistant.step.searchingFor': 'Ищет: {query}',
'assistant.step.weatherAt': 'Проверяет погоду: {place}',
'assistant.change.timeline.show': 'Показать расписание',
'assistant.change.timeline.hide': 'Скрыть расписание',
'assistant.change.timeline.new': 'Новое',
'assistant.change.timeline.travel': '≈ {count} мин в пути',
'assistant.change.timeline.travel.one': '≈ {count} мин в пути',
'assistant.change.timeline.travel.few': '≈ {count} мин в пути',
'assistant.change.timeline.travel.many': '≈ {count} мин в пути',
'assistant.change.timeline.gap': '{count} мин свободно',
'assistant.change.timeline.gap.one': '{count} мин свободно',
'assistant.change.timeline.gap.few': '{count} мин свободно',
'assistant.change.timeline.gap.many': '{count} мин свободно',
'assistant.change.timeline.overlap': 'Пересекается с {place}',
'assistant.change.timeline.moreDays': 'Ещё {count} дня',
'assistant.change.timeline.moreDays.one': 'Ещё {count} день',
'assistant.change.timeline.moreDays.few': 'Ещё {count} дня',
'assistant.change.timeline.moreDays.many': 'Ещё {count} дней',
'assistant.change.viewOnMap': 'Посмотреть на карте',
'assistant.change.before': 'До',
'assistant.change.after': 'После',
@@ -447,8 +431,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.many': 'Показать все {count} изменений',
'assistant.change.verb.updateBudgetItem': 'Изменить расход',
'assistant.change.verb.rememberNote': 'Запомнить для этой поездки',
'assistant.change.details': 'Подробности',
'assistant.change.detailsHide': 'Скрыть подробности',
'assistant.change.verb.genericCreate': 'Добавить',
'assistant.change.verb.genericUpdate': 'Изменить',
'assistant.change.verb.genericRemove': 'Удалить',
-15
View File
@@ -135,7 +135,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Medzitým sa to zmenilo. Uprav to ručne.',
'assistant.change.undoFailed': 'Toto sa nepodarilo vrátiť späť.',
'assistant.change.showOnMap': 'Zobraziť na mape',
'assistant.change.empty': 'prázdne',
'assistant.change.pending': '{count} zmien na teba čaká',
'assistant.change.pending.one': '{count} zmena na teba čaká',
'assistant.change.pending.few': '{count} zmeny na teba čakajú',
@@ -253,19 +252,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Hľadá v blízkosti {place}',
'assistant.step.searchingFor': 'Hľadá {query}',
'assistant.step.weatherAt': 'Zisťuje počasie v {place}',
'assistant.change.timeline.show': 'Zobraziť program',
'assistant.change.timeline.hide': 'Skryť program',
'assistant.change.timeline.new': 'Nové',
'assistant.change.timeline.travel': '≈ {count} min cesty',
'assistant.change.timeline.travel.one': '≈ {count} min cesty',
'assistant.change.timeline.travel.few': '≈ {count} min cesty',
'assistant.change.timeline.gap': '{count} min voľno',
'assistant.change.timeline.gap.one': '{count} min voľno',
'assistant.change.timeline.gap.few': '{count} min voľno',
'assistant.change.timeline.overlap': 'Prekrýva sa s {place}',
'assistant.change.timeline.moreDays': 'Ďalších {count} dní',
'assistant.change.timeline.moreDays.one': 'Ďalší {count} deň',
'assistant.change.timeline.moreDays.few': 'Ďalšie {count} dni',
'assistant.change.viewOnMap': 'Pozrieť na mape',
'assistant.change.before': 'Predtým',
'assistant.change.after': 'Potom',
@@ -420,8 +407,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.few': 'Zobraziť všetky {count} zmeny',
'assistant.change.verb.updateBudgetItem': 'Zmeniť výdavok',
'assistant.change.verb.rememberNote': 'Zapamätať pre túto cestu',
'assistant.change.details': 'Podrobnosti',
'assistant.change.detailsHide': 'Skryť podrobnosti',
'assistant.change.verb.genericCreate': 'Pridať',
'assistant.change.verb.genericUpdate': 'Zmeniť',
'assistant.change.verb.genericRemove': 'Odstrániť',
-12
View File
@@ -134,7 +134,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Har ändrats under tiden. Justera det för hand.',
'assistant.change.undoFailed': 'Det gick inte att ångra.',
'assistant.change.showOnMap': 'Visa på kartan',
'assistant.change.empty': 'tom',
'assistant.change.pending': '{count} ändringar väntar på dig',
'assistant.change.pending.one': '{count} ändring väntar på dig',
'assistant.change.verb.addPlace': 'Lägg till plats',
@@ -249,16 +248,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Söker nära {place}',
'assistant.step.searchingFor': 'Letar efter {query}',
'assistant.step.weatherAt': 'Kollar vädret i {place}',
'assistant.change.timeline.show': 'Visa schema',
'assistant.change.timeline.hide': 'Dölj schema',
'assistant.change.timeline.new': 'Ny',
'assistant.change.timeline.travel': '≈ {count} min resväg',
'assistant.change.timeline.travel.one': '≈ {count} min resväg',
'assistant.change.timeline.gap': '{count} min ledigt',
'assistant.change.timeline.gap.one': '{count} min ledigt',
'assistant.change.timeline.overlap': 'Krockar med {place}',
'assistant.change.timeline.moreDays': '{count} dagar till',
'assistant.change.timeline.moreDays.one': '{count} dag till',
'assistant.change.viewOnMap': 'Visa på kartan',
'assistant.change.before': 'Före',
'assistant.change.after': 'Efter',
@@ -397,8 +387,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': 'Visa {count} ändring',
'assistant.change.verb.updateBudgetItem': 'Ändra utgift',
'assistant.change.verb.rememberNote': 'Kom ihåg för den här resan',
'assistant.change.details': 'Detaljer',
'assistant.change.detailsHide': 'Dölj detaljer',
'assistant.change.verb.genericCreate': 'Lägg till',
'assistant.change.verb.genericUpdate': 'Ändra',
'assistant.change.verb.genericRemove': 'Ta bort',
-9
View File
@@ -127,7 +127,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'มีการเปลี่ยนแปลงในระหว่างนั้น โปรดปรับด้วยตนเอง',
'assistant.change.undoFailed': 'เลิกทำไม่ได้',
'assistant.change.showOnMap': 'แสดงบนแผนที่',
'assistant.change.empty': 'ว่าง',
'assistant.change.pending': 'มี {count} การเปลี่ยนแปลงรอคุณอยู่',
'assistant.change.verb.addPlace': 'เพิ่มสถานที่',
'assistant.change.verb.addPlaceToDay': 'เพิ่มสถานที่ลงในวัน',
@@ -239,13 +238,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'กำลังค้นหาใกล้ {place}',
'assistant.step.searchingFor': 'กำลังค้นหา {query}',
'assistant.step.weatherAt': 'กำลังตรวจสอบสภาพอากาศที่ {place}',
'assistant.change.timeline.show': 'แสดงกำหนดการ',
'assistant.change.timeline.hide': 'ซ่อนกำหนดการ',
'assistant.change.timeline.new': 'ใหม่',
'assistant.change.timeline.travel': '≈ เดินทาง {count} นาที',
'assistant.change.timeline.gap': 'ว่าง {count} นาที',
'assistant.change.timeline.overlap': 'ทับซ้อนกับ {place}',
'assistant.change.timeline.moreDays': 'อีก {count} วัน',
'assistant.change.viewOnMap': 'ดูบนแผนที่',
'assistant.change.before': 'ก่อน',
'assistant.change.after': 'หลัง',
@@ -367,8 +360,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': 'แสดงการเปลี่ยนแปลงทั้ง {count} รายการ',
'assistant.change.verb.updateBudgetItem': 'แก้ไขค่าใช้จ่าย',
'assistant.change.verb.rememberNote': 'จำไว้สำหรับทริปนี้',
'assistant.change.details': 'รายละเอียด',
'assistant.change.detailsHide': 'ซ่อนรายละเอียด',
'assistant.change.verb.genericCreate': 'เพิ่ม',
'assistant.change.verb.genericUpdate': 'เปลี่ยน',
'assistant.change.verb.genericRemove': 'ลบ',
-12
View File
@@ -132,7 +132,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Bu arada değiştirildi. Lütfen elle düzelt.',
'assistant.change.undoFailed': 'Bu geri alınamadı.',
'assistant.change.showOnMap': 'Haritada göster',
'assistant.change.empty': 'boş',
'assistant.change.pending': '{count} değişiklik seni bekliyor',
'assistant.change.pending.one': '{count} değişiklik seni bekliyor',
'assistant.change.verb.addPlace': 'Yer ekle',
@@ -247,16 +246,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': '{place} yakınında arıyor',
'assistant.step.searchingFor': '{query} arıyor',
'assistant.step.weatherAt': '{place} için hava durumuna bakıyor',
'assistant.change.timeline.show': 'Akışı göster',
'assistant.change.timeline.hide': 'Akışı gizle',
'assistant.change.timeline.new': 'Yeni',
'assistant.change.timeline.travel': '≈ {count} dk yol',
'assistant.change.timeline.travel.one': '≈ {count} dk yol',
'assistant.change.timeline.gap': '{count} dk boş',
'assistant.change.timeline.gap.one': '{count} dk boş',
'assistant.change.timeline.overlap': '{place} ile çakışıyor',
'assistant.change.timeline.moreDays': '{count} gün daha',
'assistant.change.timeline.moreDays.one': '{count} gün daha',
'assistant.change.viewOnMap': 'Haritada gör',
'assistant.change.before': 'Önce',
'assistant.change.after': 'Sonra',
@@ -397,8 +387,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.one': '{count} değişikliği göster',
'assistant.change.verb.updateBudgetItem': 'Harcamayı değiştir',
'assistant.change.verb.rememberNote': 'Bu gezi için hatırla',
'assistant.change.details': 'Ayrıntılar',
'assistant.change.detailsHide': 'Ayrıntıları gizle',
'assistant.change.verb.genericCreate': 'Ekle',
'assistant.change.verb.genericUpdate': 'Değiştir',
'assistant.change.verb.genericRemove': 'Kaldır',
-18
View File
@@ -138,7 +138,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Тим часом це змінилося. Виправте вручну.',
'assistant.change.undoFailed': 'Не вдалося скасувати.',
'assistant.change.showOnMap': 'Показати на карті',
'assistant.change.empty': 'порожньо',
'assistant.change.pending': '{count} зміни чекають на вас',
'assistant.change.pending.one': '{count} зміна чекає на вас',
'assistant.change.pending.few': '{count} зміни чекають на вас',
@@ -259,22 +258,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Шукає поруч: {place}',
'assistant.step.searchingFor': 'Шукає: {query}',
'assistant.step.weatherAt': 'Перевіряє погоду: {place}',
'assistant.change.timeline.show': 'Показати розклад',
'assistant.change.timeline.hide': 'Сховати розклад',
'assistant.change.timeline.new': 'Нове',
'assistant.change.timeline.travel': '≈ {count} хв у дорозі',
'assistant.change.timeline.travel.one': '≈ {count} хв у дорозі',
'assistant.change.timeline.travel.few': '≈ {count} хв у дорозі',
'assistant.change.timeline.travel.many': '≈ {count} хв у дорозі',
'assistant.change.timeline.gap': '{count} хв вільно',
'assistant.change.timeline.gap.one': '{count} хв вільно',
'assistant.change.timeline.gap.few': '{count} хв вільно',
'assistant.change.timeline.gap.many': '{count} хв вільно',
'assistant.change.timeline.overlap': 'Перетинається з {place}',
'assistant.change.timeline.moreDays': 'Ще {count} дня',
'assistant.change.timeline.moreDays.one': 'Ще {count} день',
'assistant.change.timeline.moreDays.few': 'Ще {count} дні',
'assistant.change.timeline.moreDays.many': 'Ще {count} днів',
'assistant.change.viewOnMap': 'Переглянути на мапі',
'assistant.change.before': 'До',
'assistant.change.after': 'Після',
@@ -445,8 +429,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll.many': 'Показати всі {count} змін',
'assistant.change.verb.updateBudgetItem': 'Змінити витрату',
'assistant.change.verb.rememberNote': 'Запам’ятати для цієї подорожі',
'assistant.change.details': 'Подробиці',
'assistant.change.detailsHide': 'Сховати подробиці',
'assistant.change.verb.genericCreate': 'Додати',
'assistant.change.verb.genericUpdate': 'Змінити',
'assistant.change.verb.genericRemove': 'Видалити',
-9
View File
@@ -129,7 +129,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': 'Đã có thay đổi trong lúc đó. Vui lòng chỉnh lại thủ công.',
'assistant.change.undoFailed': 'Không thể hoàn tác.',
'assistant.change.showOnMap': 'Xem trên bản đồ',
'assistant.change.empty': 'trống',
'assistant.change.pending': '{count} thay đổi đang chờ bạn',
'assistant.change.verb.addPlace': 'Thêm địa điểm',
'assistant.change.verb.addPlaceToDay': 'Thêm địa điểm vào ngày',
@@ -241,13 +240,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': 'Đang tìm gần {place}',
'assistant.step.searchingFor': 'Đang tìm {query}',
'assistant.step.weatherAt': 'Đang xem thời tiết ở {place}',
'assistant.change.timeline.show': 'Xem lịch trình',
'assistant.change.timeline.hide': 'Ẩn lịch trình',
'assistant.change.timeline.new': 'Mới',
'assistant.change.timeline.travel': '≈ {count} phút di chuyển',
'assistant.change.timeline.gap': 'Trống {count} phút',
'assistant.change.timeline.overlap': 'Trùng với {place}',
'assistant.change.timeline.moreDays': 'Thêm {count} ngày',
'assistant.change.viewOnMap': 'Xem trên bản đồ',
'assistant.change.before': 'Trước',
'assistant.change.after': 'Sau',
@@ -370,8 +363,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': 'Hiện tất cả {count} thay đổi',
'assistant.change.verb.updateBudgetItem': 'Sửa khoản chi',
'assistant.change.verb.rememberNote': 'Ghi nhớ cho chuyến đi này',
'assistant.change.details': 'Chi tiết',
'assistant.change.detailsHide': 'Ẩn chi tiết',
'assistant.change.verb.genericCreate': 'Thêm',
'assistant.change.verb.genericUpdate': 'Thay đổi',
'assistant.change.verb.genericRemove': 'Xóa',
-9
View File
@@ -126,7 +126,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': '這段期間已有變更,請手動調整。',
'assistant.change.undoFailed': '無法復原。',
'assistant.change.showOnMap': '在地圖上顯示',
'assistant.change.empty': '空白',
'assistant.change.pending': '有 {count} 項修改等你處理',
'assistant.change.verb.addPlace': '新增地點',
'assistant.change.verb.addPlaceToDay': '將地點加到某天',
@@ -238,13 +237,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': '正在{place}附近搜尋',
'assistant.step.searchingFor': '正在搜尋{query}',
'assistant.step.weatherAt': '正在查看{place}的天氣',
'assistant.change.timeline.show': '顯示行程表',
'assistant.change.timeline.hide': '隱藏行程表',
'assistant.change.timeline.new': '新增',
'assistant.change.timeline.travel': '路程約 {count} 分鐘',
'assistant.change.timeline.gap': '空檔 {count} 分鐘',
'assistant.change.timeline.overlap': '與{place}時間重疊',
'assistant.change.timeline.moreDays': '另有 {count} 天',
'assistant.change.viewOnMap': '在地圖上查看',
'assistant.change.before': '修改前',
'assistant.change.after': '修改後',
@@ -364,8 +357,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': '顯示全部 {count} 項變更',
'assistant.change.verb.updateBudgetItem': '修改支出',
'assistant.change.verb.rememberNote': '為此行程記住',
'assistant.change.details': '詳細資料',
'assistant.change.detailsHide': '隱藏詳細資料',
'assistant.change.verb.genericCreate': '新增',
'assistant.change.verb.genericUpdate': '變更',
'assistant.change.verb.genericRemove': '刪除',
-9
View File
@@ -126,7 +126,6 @@ const assistant: TranslationStrings = {
'assistant.change.undoStale': '期间已有改动,请手动调整。',
'assistant.change.undoFailed': '无法撤销。',
'assistant.change.showOnMap': '在地图上显示',
'assistant.change.empty': '空',
'assistant.change.pending': '有 {count} 项修改等你处理',
'assistant.change.verb.addPlace': '添加地点',
'assistant.change.verb.addPlaceToDay': '将地点添加到某天',
@@ -238,13 +237,7 @@ const assistant: TranslationStrings = {
'assistant.step.nearbyAt': '正在{place}附近搜索',
'assistant.step.searchingFor': '正在搜索{query}',
'assistant.step.weatherAt': '正在查看{place}的天气',
'assistant.change.timeline.show': '显示日程',
'assistant.change.timeline.hide': '隐藏日程',
'assistant.change.timeline.new': '新增',
'assistant.change.timeline.travel': '路程约 {count} 分钟',
'assistant.change.timeline.gap': '空闲 {count} 分钟',
'assistant.change.timeline.overlap': '与{place}时间重叠',
'assistant.change.timeline.moreDays': '另有 {count} 天',
'assistant.change.viewOnMap': '在地图上查看',
'assistant.change.before': '修改前',
'assistant.change.after': '修改后',
@@ -364,8 +357,6 @@ const assistant: TranslationStrings = {
'assistant.week.showAll': '显示全部 {count} 项更改',
'assistant.change.verb.updateBudgetItem': '修改支出',
'assistant.change.verb.rememberNote': '为此行程记住',
'assistant.change.details': '详情',
'assistant.change.detailsHide': '隐藏详情',
'assistant.change.verb.genericCreate': '添加',
'assistant.change.verb.genericUpdate': '更改',
'assistant.change.verb.genericRemove': '删除',