mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
feat(assistant): curated cards and undo for the changes of a journal itself
Starting a journal, changing its settings, linking or unlinking a trip, bringing back its suggestions, the user's own view of it and deleting it fell back to the generic card, which names ids and offers no way back. They now share a journeyHeader card: the journal with its trips, the one the change links or unlinks marked, what a new journal brings in and what a deletion or an unlink takes, and each setting old to new in the journal's own words. A new journal is deleted again while nothing was written, added or laid out in it, changed settings are written back from what the tool answered, a linked trip is unlinked by its owner while none of its suggestions was written up, and the own switch flips back.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* FE-AI-JOURNEYHEADER-001 to FE-AI-JOURNEYHEADER-005
|
||||
*
|
||||
* A change of a journal itself in the chat: the settings an update sets by
|
||||
* the journal's own names, old to new; the trips under the row with the one
|
||||
* the change links or unlinks marked; what a new journal brings and what a
|
||||
* deletion or an unlink takes; the receipt that opens the journal.
|
||||
*/
|
||||
import type { AssistantApplied, AssistantChangePreview, JourneyHeaderCard } from '@trek/shared';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { fireEvent, render, screen } from '../../../../../tests/helpers/render';
|
||||
import { useAssistantStore } from '../../../../store/assistantStore';
|
||||
import AssistantChangeCard from '../../AssistantChangeCard';
|
||||
import type { ChangeItem } from '../../assistantParts';
|
||||
import { AssistantViewContext } from '../../assistantViewContext';
|
||||
import { headerValueKey, settingRows } from './journeyHeaderModel';
|
||||
|
||||
const ASSISTANT = useAssistantStore.getState();
|
||||
|
||||
afterEach(() => {
|
||||
useAssistantStore.setState(ASSISTANT, true);
|
||||
});
|
||||
|
||||
const JAPAN: JourneyHeaderCard = {
|
||||
kind: 'journeyHeader',
|
||||
journeyId: 12,
|
||||
title: 'Japan 2026',
|
||||
subtitle: null,
|
||||
change: 'changed',
|
||||
trips: [{ id: 5, title: 'Tokyo days', startDate: '2026-11-02', endDate: '2026-11-08', change: 'kept' }],
|
||||
entries: 3,
|
||||
photos: 40,
|
||||
suggestions: null,
|
||||
};
|
||||
|
||||
function change(
|
||||
state: ChangeItem['state'],
|
||||
card: JourneyHeaderCard,
|
||||
over: Partial<AssistantChangePreview> = {}
|
||||
): ChangeItem {
|
||||
const preview: AssistantChangePreview = {
|
||||
toolCallId: 'h1',
|
||||
verbKey: 'updateJournalSettings',
|
||||
subject: 'Japan 2026',
|
||||
rows: [],
|
||||
dayId: null,
|
||||
placeIds: [],
|
||||
undoable: true,
|
||||
domain: 'journey',
|
||||
card,
|
||||
...over,
|
||||
};
|
||||
const applied: AssistantApplied = { toolCallId: 'h1', entity: 'journey', ids: [12], dayId: null, undoUntil: null };
|
||||
return { toolCallId: 'h1', preview, applied: state === 'applied' ? applied : null, state, card };
|
||||
}
|
||||
|
||||
function shown(item: ChangeItem, onOpen?: (target: unknown) => void) {
|
||||
return render(
|
||||
<AssistantViewContext.Provider value={{ tripId: -12, variant: 'window', onOpen }}>
|
||||
<AssistantChangeCard items={[item]} />
|
||||
</AssistantViewContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('a change of a journal itself', () => {
|
||||
it('FE-AI-JOURNEYHEADER-001: an update names each setting by the journal own words, old to new', () => {
|
||||
const rows: AssistantChangePreview['rows'] = [
|
||||
{ op: 'change', labelKey: 'name', before: 'Japan', after: 'Japan 2026' },
|
||||
{ op: 'change', labelKey: 'field', field: 'show_mood', before: '✓', after: '✗' },
|
||||
{ op: 'change', labelKey: 'field', field: 'status_override', before: '', after: 'live' },
|
||||
];
|
||||
shown(change('pending', JAPAN, { rows }));
|
||||
expect(screen.getByText("Change the journal's settings")).toBeInTheDocument();
|
||||
expect(screen.getByText('Japan')).toHaveClass('line-through');
|
||||
expect(screen.getByText('Mood: On')).toHaveClass('line-through');
|
||||
expect(screen.getByText('Mood: Off')).toBeInTheDocument();
|
||||
expect(screen.getByText('Shown as: Automatic')).toHaveClass('line-through');
|
||||
expect(screen.getByText('Shown as: Live')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tokyo days')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-AI-JOURNEYHEADER-002: a new journal shows its subtitle, the trips it links and the places that come in', () => {
|
||||
const fresh: JourneyHeaderCard = {
|
||||
...JAPAN,
|
||||
journeyId: null,
|
||||
change: 'new',
|
||||
subtitle: 'Autumn',
|
||||
trips: [{ ...JAPAN.trips[0], change: 'link' }],
|
||||
entries: null,
|
||||
photos: null,
|
||||
suggestions: 9,
|
||||
};
|
||||
shown(change('pending', fresh, { verbKey: 'createJournal', undoable: true }));
|
||||
expect(screen.getByText('Start a journal')).toBeInTheDocument();
|
||||
expect(screen.getByText('Autumn')).toBeInTheDocument();
|
||||
expect(screen.getByText('Will be added')).toBeInTheDocument();
|
||||
expect(screen.getByText('9 places come in as suggestions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-AI-JOURNEYHEADER-003: a deletion warns that everything goes; an unlink says what stays and what goes', () => {
|
||||
const first = shown(change('pending', { ...JAPAN, change: 'willDelete' }, { verbKey: 'deleteJournal' }));
|
||||
expect(screen.getByText('Delete the journal')).toBeInTheDocument();
|
||||
expect(screen.getByText(/This cannot be undone\./)).toBeInTheDocument();
|
||||
first.unmount();
|
||||
const unlink: JourneyHeaderCard = {
|
||||
...JAPAN,
|
||||
trips: [{ ...JAPAN.trips[0], change: 'unlink' }],
|
||||
entries: 1,
|
||||
suggestions: 4,
|
||||
};
|
||||
shown(change('pending', unlink, { verbKey: 'unlinkJournalTrip' }));
|
||||
expect(screen.getByText('Tokyo days')).toHaveClass('line-through');
|
||||
expect(screen.getByText('Will be taken out')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 written entry stays')).toBeInTheDocument();
|
||||
expect(screen.getByText('4 open suggestions go')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-AI-JOURNEYHEADER-004: the receipt opens the journal, a change that failed shows its row alone', () => {
|
||||
const onOpen = vi.fn();
|
||||
const applied = shown(change('applied', JAPAN), onOpen);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open the journal' }));
|
||||
expect(onOpen).toHaveBeenCalledWith({ kind: 'page', path: '/journey/12' });
|
||||
applied.unmount();
|
||||
shown(change('failed', JAPAN));
|
||||
expect(screen.queryByText('Tokyo days')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-AI-JOURNEYHEADER-005: the model reads a status by its name and a switch as on or off', () => {
|
||||
expect(headerValueKey('status', 'archived')).toBe('journey.status.archived');
|
||||
expect(headerValueKey('status_override', null)).toBe('journey.settings.statusAuto');
|
||||
expect(headerValueKey('show_mood', '✓')).toBe('settings.on');
|
||||
expect(headerValueKey('cover_gradient', 'linear-gradient(red, blue)')).toBeNull();
|
||||
expect(headerValueKey('status', 'toString')).toBeNull();
|
||||
const rows: AssistantChangePreview['rows'] = [
|
||||
{ op: 'add', labelKey: 'field', field: 'trip', before: null, after: 'Kyoto' },
|
||||
{ op: 'add', labelKey: 'field', field: 'show_weather', before: null, after: '✓' },
|
||||
{ op: 'add', labelKey: 'field', field: 'unknown', before: null, after: 'x' },
|
||||
{ op: 'change', labelKey: 'name', before: 'A', after: 'B' },
|
||||
];
|
||||
expect(settingRows(rows).map((row) => row.field)).toEqual(['show_weather']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { AssistantChangeRow, JourneyHeaderCard, JourneyHeaderTrip } from '@trek/shared';
|
||||
import { AlignLeft, Luggage, Type } from 'lucide-react';
|
||||
|
||||
import { useTranslation } from '../../../../i18n';
|
||||
import { type PillTone, SoftPill } from '../../../Planner/planParts';
|
||||
import type { ChangeItem } from '../../assistantParts';
|
||||
import { FactChange } from '../parts/CardFacts';
|
||||
import { useCardFormat } from '../parts/useCardFormat';
|
||||
import { headerFieldOf, headerValueKey, settingRows } from './journeyHeaderModel';
|
||||
|
||||
/**
|
||||
* What a change row of a journal itself adds to the row it already is (its
|
||||
* verb and the journal's name): on the faint line the name, subtitle and
|
||||
* settings it changes, each old to new; under the row the journal's trips,
|
||||
* the one the change links or unlinks marked, and what the change brings or
|
||||
* takes along. A change that failed, was discarded or ran out shows the row
|
||||
* alone.
|
||||
*/
|
||||
|
||||
const GONE: ReadonlySet<ChangeItem['state']> = new Set(['failed', 'discarded', 'expired']);
|
||||
const WORD_ICONS = { name: Type, description: AlignLeft } as const;
|
||||
const NOTE = 'm-0 mt-1.5 text-caption';
|
||||
|
||||
const TRIP_TONE: Readonly<Record<JourneyHeaderTrip['change'], PillTone>> = {
|
||||
kept: 'neutral',
|
||||
link: 'success',
|
||||
unlink: 'danger',
|
||||
};
|
||||
|
||||
/** One setting a change sets, by its name, old to new. */
|
||||
function SettingChange({ row }: { row: AssistantChangeRow }) {
|
||||
const { t } = useTranslation();
|
||||
const look = headerFieldOf(row.field);
|
||||
if (!look) return null;
|
||||
const named = (value: string | null) => {
|
||||
const key = headerValueKey(row.field, value);
|
||||
if (key) return t(key);
|
||||
return value || null;
|
||||
};
|
||||
const before = row.op === 'add' ? null : named(row.before);
|
||||
const after = named(row.after) ?? t('assistant.card.common.removed');
|
||||
const name = t(look.labelKey);
|
||||
return (
|
||||
<FactChange
|
||||
icon={look.Icon}
|
||||
before={before === null ? null : t('assistant.card.journeyHeader.setting', { name, value: before })}
|
||||
after={t('assistant.card.journeyHeader.setting', { name, value: after })}
|
||||
tone="info"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** The journal's new name and subtitle and the settings a change sets. */
|
||||
export function JourneyHeaderFacts({ item }: { card: JourneyHeaderCard; item: ChangeItem }) {
|
||||
const { t } = useTranslation();
|
||||
const rows = item.preview?.rows ?? [];
|
||||
const words = rows.filter(
|
||||
(row): row is AssistantChangeRow & { labelKey: keyof typeof WORD_ICONS } =>
|
||||
row.op === 'change' && row.labelKey in WORD_ICONS
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{words.map((row) => (
|
||||
<FactChange
|
||||
key={row.labelKey}
|
||||
icon={WORD_ICONS[row.labelKey]}
|
||||
before={row.before || null}
|
||||
after={row.after || t('assistant.card.common.removed')}
|
||||
tone="info"
|
||||
/>
|
||||
))}
|
||||
{settingRows(rows).map((row) => (
|
||||
<SettingChange key={row.field} row={row} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TripChip({ trip }: { trip: JourneyHeaderTrip }) {
|
||||
const { t } = useTranslation();
|
||||
const format = useCardFormat();
|
||||
const days = trip.startDate ? format.date(trip.startDate) : null;
|
||||
return (
|
||||
<SoftPill
|
||||
tone={TRIP_TONE[trip.change]}
|
||||
className="max-w-full"
|
||||
icon={<Luggage size={10} strokeWidth={2.2} aria-hidden="true" className="flex-none" />}
|
||||
>
|
||||
{trip.change === 'link' && <span className="sr-only">{t('assistant.card.journeyHeader.tripLink')}</span>}
|
||||
{trip.change === 'unlink' && <span className="sr-only">{t('assistant.card.journeyHeader.tripUnlink')}</span>}
|
||||
<bdi className={`truncate ${trip.change === 'unlink' ? 'line-through' : ''}`}>{trip.title}</bdi>
|
||||
{days && <span className="text-content-faint">{days}</span>}
|
||||
</SoftPill>
|
||||
);
|
||||
}
|
||||
|
||||
/** What the change brings along or takes with it, in the journal's words. */
|
||||
function HeaderNotes({ card }: { card: JourneyHeaderCard }) {
|
||||
const { t } = useTranslation();
|
||||
const unlinking = card.trips.some((trip) => trip.change === 'unlink');
|
||||
if (card.change === 'willDelete') {
|
||||
return <p className={`${NOTE} text-danger`}>{t('assistant.card.journeyHeader.deletes')}</p>;
|
||||
}
|
||||
if (unlinking) {
|
||||
return (
|
||||
<>
|
||||
{card.entries !== null && card.entries > 0 && (
|
||||
<p className={`${NOTE} text-content-muted`}>
|
||||
{t('assistant.card.journeyHeader.entriesStay', { count: card.entries })}
|
||||
</p>
|
||||
)}
|
||||
{card.suggestions !== null && card.suggestions > 0 && (
|
||||
<p className={`${NOTE} text-content-muted`}>
|
||||
{t('assistant.card.journeyHeader.suggestionsGo', { count: card.suggestions })}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (card.suggestions === null || card.suggestions === 0) return null;
|
||||
return (
|
||||
<p className={`${NOTE} text-content-muted`}>
|
||||
{t('assistant.card.journeyHeader.suggestionsCome', { count: card.suggestions })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/** The journal's trips and what the change brings or takes, under the row. */
|
||||
export function JourneyHeaderBody({ card, item }: { card: JourneyHeaderCard; item: ChangeItem }) {
|
||||
if (GONE.has(item.state)) return null;
|
||||
return (
|
||||
<div>
|
||||
{card.subtitle && card.change === 'new' && (
|
||||
<p className="m-0 text-caption text-content-muted">
|
||||
<bdi>{card.subtitle}</bdi>
|
||||
</p>
|
||||
)}
|
||||
{card.trips.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1">
|
||||
{card.trips.map((trip, index) => (
|
||||
<TripChip key={`${trip.id ?? 'x'}-${index}`} trip={trip} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<HeaderNotes card={card} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { AssistantChangeRow } from '@trek/shared';
|
||||
import {
|
||||
CircleDot,
|
||||
CloudSun,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Footprints,
|
||||
Luggage,
|
||||
MapPin,
|
||||
Palette,
|
||||
Route,
|
||||
Smile,
|
||||
Sparkles,
|
||||
ThumbsUp,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
/**
|
||||
* The pure side of a change of a journal itself: the glyph and name of each
|
||||
* setting a change row names (under the tool's own field names), and how its
|
||||
* value reads, a status by its name, a switch as on or off, a cleared status
|
||||
* override as following the trip dates. The names are the journal's own,
|
||||
* as its settings dialog writes them.
|
||||
*/
|
||||
|
||||
interface FieldLook {
|
||||
Icon: LucideIcon;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
const FIELDS: Readonly<Record<string, FieldLook>> = {
|
||||
status: { Icon: CircleDot, labelKey: 'journey.settings.status' },
|
||||
status_override: { Icon: Eye, labelKey: 'assistant.card.journeyHeader.field.statusOverride' },
|
||||
show_verdict: { Icon: ThumbsUp, labelKey: 'journey.settings.showVerdict' },
|
||||
show_mood: { Icon: Smile, labelKey: 'journey.settings.showMood' },
|
||||
show_weather: { Icon: CloudSun, labelKey: 'journey.settings.showWeather' },
|
||||
photo_location: { Icon: MapPin, labelKey: 'journey.settings.photoLocation' },
|
||||
show_trip_tracks: { Icon: Route, labelKey: 'journey.settings.showTripTracks' },
|
||||
auto_prepare_recordings: { Icon: Footprints, labelKey: 'journey.settings.autoPrepare' },
|
||||
cover_gradient: { Icon: Palette, labelKey: 'assistant.card.journeyHeader.field.coverGradient' },
|
||||
trip: { Icon: Luggage, labelKey: 'assistant.card.journeyHeader.field.trip' },
|
||||
suggestions: { Icon: Sparkles, labelKey: 'assistant.card.journeyHeader.field.suggestions' },
|
||||
hide_skeletons: { Icon: EyeOff, labelKey: 'journey.skeletons.hide' },
|
||||
};
|
||||
|
||||
const STATUSES: Readonly<Record<string, string>> = {
|
||||
draft: 'journey.status.draft',
|
||||
active: 'journey.status.active',
|
||||
completed: 'journey.status.completed',
|
||||
archived: 'journey.status.archived',
|
||||
live: 'journey.frontpage.live',
|
||||
};
|
||||
|
||||
const FLAG_ON = '✓';
|
||||
const FLAG_OFF = '✗';
|
||||
|
||||
function own<T>(table: Readonly<Record<string, T>>, key: string | null | undefined): T | null {
|
||||
if (!key || !Object.prototype.hasOwnProperty.call(table, key)) return null;
|
||||
return table[key] ?? null;
|
||||
}
|
||||
|
||||
/** The look of a setting a row names; null for a field the card has no name for. */
|
||||
export function headerFieldOf(field: string | undefined): FieldLook | null {
|
||||
return own(FIELDS, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* The key a value of a setting reads by, or null for one that reads as it is
|
||||
* (a name, a count). A cleared status override follows the trip dates again.
|
||||
*/
|
||||
export function headerValueKey(field: string | undefined, value: string | null): string | null {
|
||||
if (value === FLAG_ON) return 'settings.on';
|
||||
if (value === FLAG_OFF) return 'settings.off';
|
||||
if (field === 'status_override' && !value) return 'journey.settings.statusAuto';
|
||||
if (field === 'status' || field === 'status_override') return own(STATUSES, value);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The setting rows of a change, the trips aside: the card draws those as its own chips. */
|
||||
export function settingRows(rows: readonly AssistantChangeRow[]): AssistantChangeRow[] {
|
||||
return rows.filter((row) => row.labelKey === 'field' && row.field !== 'trip' && headerFieldOf(row.field) !== null);
|
||||
}
|
||||
@@ -1,4 +1,16 @@
|
||||
import { BookOpen, BookOpenText, BookPlus, BookX } from 'lucide-react';
|
||||
import {
|
||||
BookCheck,
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
BookPlus,
|
||||
BookX,
|
||||
Eye,
|
||||
Link,
|
||||
RotateCcw,
|
||||
Settings2,
|
||||
Type,
|
||||
Unlink,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { PackLook } from '../cardTypes';
|
||||
import { journalTarget } from './journeyCardModel';
|
||||
@@ -17,6 +29,19 @@ export const JOURNEY_LOOK = {
|
||||
addJournalEntry: { Icon: BookPlus, tone: 'success', labelKey: 'assistant.change.verb.addJournalEntry' },
|
||||
updateJournalEntry: { Icon: BookOpenText, tone: 'info', labelKey: 'assistant.change.verb.updateJournalEntry' },
|
||||
removeJournalEntry: { Icon: BookX, tone: 'danger', labelKey: 'assistant.change.verb.removeJournalEntry' },
|
||||
createJournal: { Icon: BookPlus, tone: 'success', labelKey: 'assistant.change.verb.createJournal' },
|
||||
renameJournal: { Icon: Type, tone: 'info', labelKey: 'assistant.change.verb.renameJournal' },
|
||||
setJournalStatus: { Icon: BookCheck, tone: 'info', labelKey: 'assistant.change.verb.setJournalStatus' },
|
||||
updateJournalSettings: { Icon: Settings2, tone: 'info', labelKey: 'assistant.change.verb.updateJournalSettings' },
|
||||
deleteJournal: { Icon: BookX, tone: 'danger', labelKey: 'assistant.change.verb.deleteJournal' },
|
||||
linkJournalTrip: { Icon: Link, tone: 'success', labelKey: 'assistant.change.verb.linkJournalTrip' },
|
||||
unlinkJournalTrip: { Icon: Unlink, tone: 'warning', labelKey: 'assistant.change.verb.unlinkJournalTrip' },
|
||||
restoreJournalSuggestions: {
|
||||
Icon: RotateCcw,
|
||||
tone: 'info',
|
||||
labelKey: 'assistant.change.verb.restoreJournalSuggestions',
|
||||
},
|
||||
setJournalPreferences: { Icon: Eye, tone: 'info', labelKey: 'assistant.change.verb.setJournalPreferences' },
|
||||
},
|
||||
tools: {
|
||||
create_journey: { labelKey: 'assistant.tool.create_journey' },
|
||||
@@ -40,6 +65,14 @@ export const JOURNEY_LOOK = {
|
||||
? journalTarget(card.journeyId, applied.ids[0] ?? card.entries[0]?.entryId ?? null, card.tripJournal)
|
||||
: null,
|
||||
},
|
||||
journey: {
|
||||
Icon: BookOpen,
|
||||
openLabelKey: 'assistant.card.journeyHeader.open',
|
||||
open: (applied, card) => {
|
||||
const journeyId = applied.ids[0] ?? (card?.kind === 'journeyHeader' ? card.journeyId : null);
|
||||
return journeyId ? journalTarget(journeyId, null, false) : null;
|
||||
},
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
journeyEntries: {
|
||||
@@ -59,5 +92,8 @@ export const JOURNEY_LOOK = {
|
||||
return readIds.length > 0 && readIds.every((id) => id !== null && touched.has(id));
|
||||
},
|
||||
},
|
||||
journeyHeader: {
|
||||
entity: () => 'journey',
|
||||
},
|
||||
},
|
||||
} satisfies PackLook<'journeyEntries'>;
|
||||
} satisfies PackLook<'journeyEntries' | 'journeyHeader'>;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { CardView } from '../cardTypes';
|
||||
import { JourneyEntryAside, JourneyEntryBody, JourneyEntryFacts } from './JourneyChangeSlots';
|
||||
import JourneyEntriesBlock from './JourneyEntriesBlock';
|
||||
import { JourneyHeaderBody, JourneyHeaderFacts } from './JourneyHeaderSlots';
|
||||
|
||||
/**
|
||||
* How the kinds of the journeys draw in the chat: the entries a read found
|
||||
* as a block of their own, and a change row of an entry with its photo beside
|
||||
* the subject, its day, time and place and whatever an update changes on the
|
||||
* faint line, and the start of its text under it. This file only maps each
|
||||
* faint line, and the start of its text under it; a change of the journal
|
||||
* itself with its settings on the faint line and its trips under it. This file only maps each
|
||||
* kind to its view: the components live in sibling files, since a file that
|
||||
* exports anything besides components breaks fast refresh, and lint:warnings holds
|
||||
* react-refresh/only-export-components at zero for a new file.
|
||||
@@ -18,4 +20,8 @@ export const JOURNEY_VIEWS = {
|
||||
Facts: JourneyEntryFacts,
|
||||
Body: JourneyEntryBody,
|
||||
},
|
||||
} satisfies { [K in 'journeyEntries']: CardView<K> };
|
||||
journeyHeader: {
|
||||
Facts: JourneyHeaderFacts,
|
||||
Body: JourneyHeaderBody,
|
||||
},
|
||||
} satisfies { [K in 'journeyEntries' | 'journeyHeader']: CardView<K> };
|
||||
|
||||
@@ -7,16 +7,32 @@ import {
|
||||
loadJournal,
|
||||
type JournalLoaded,
|
||||
} from './journey-change';
|
||||
import {
|
||||
JOURNAL_HEADER_DESCRIBERS,
|
||||
journalHeaderApplied,
|
||||
journalHeaderCard,
|
||||
loadJournalHeader,
|
||||
type JournalHeaderLoaded,
|
||||
} from './journey-header';
|
||||
import {
|
||||
DELETE_CREATED_JOURNEY,
|
||||
FLIP_JOURNEY_PREFERENCE,
|
||||
RESTORE_JOURNEY_SETTINGS,
|
||||
UNLINK_LINKED_TRIP,
|
||||
} from './journey-header-undo';
|
||||
import { readCard } from './journey-look';
|
||||
import { DELETE_CREATED_ENTRY } from './journey-undo';
|
||||
import type { JourneyEntriesCard } from '@trek/shared';
|
||||
import type { AssistantChangeVerbKey, JourneyEntriesCard } from '@trek/shared';
|
||||
|
||||
/**
|
||||
* The travel journal in the assistant chat. A read of a journal's entries
|
||||
* shows them as a card beside the answer; a held write of an entry shows the
|
||||
* entry it will write, change or delete, and once applied the entry as it
|
||||
* was stored, its photo and country included. A created entry can be taken
|
||||
* back while nothing was written to it since.
|
||||
* back while nothing was written to it since. A change of a journal itself
|
||||
* (a new one, its settings, its trips, its suggestions, the user's own view
|
||||
* of it, its deletion) shows the journal with its trips (journey-header.ts),
|
||||
* and most of them can be taken back (journey-header-undo.ts).
|
||||
*/
|
||||
|
||||
/** The entry a create or a change answered with, else the one a delete named. */
|
||||
@@ -46,6 +62,29 @@ function journalWrite(tool: string): ToolCards<JournalLoaded> {
|
||||
};
|
||||
}
|
||||
|
||||
/** The journal a change of the journal itself touched: the one its answer names, else the one it was called on. */
|
||||
function journalIds(args: ToolArgs, result: unknown): number[] {
|
||||
const id = idIn(result, 'journey') ?? argId(args, 'journeyId');
|
||||
return id === null ? [] : [id];
|
||||
}
|
||||
|
||||
/** A change of a journal itself: its verb, its words, its lookups, its card while held and once applied. */
|
||||
function journalHeader(
|
||||
tool: keyof typeof JOURNAL_HEADER_DESCRIBERS,
|
||||
verbKey: AssistantChangeVerbKey,
|
||||
undo?: NonNullable<ToolCards['undo']>,
|
||||
): ToolCards<JournalHeaderLoaded> {
|
||||
return {
|
||||
write: { verbKey, entity: 'journey' },
|
||||
describe: JOURNAL_HEADER_DESCRIBERS[tool],
|
||||
load: (args, read) => loadJournalHeader(tool, args, read),
|
||||
change: (_ctx, args, loaded) => journalHeaderCard(tool, args, loaded),
|
||||
applied: ({ result, preview }) => journalHeaderApplied(tool, result, preview),
|
||||
appliedIds: journalIds,
|
||||
...(undo ? { undo } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const JOURNEY_CARDS: DomainCards = {
|
||||
id: 'journey',
|
||||
tools: {
|
||||
@@ -76,6 +115,17 @@ export const JOURNEY_CARDS: DomainCards = {
|
||||
describe: JOURNEY_DESCRIBERS.delete_journey_entry,
|
||||
...journalWrite('delete_journey_entry'),
|
||||
},
|
||||
create_journey: journalHeader('create_journey', 'createJournal', DELETE_CREATED_JOURNEY),
|
||||
update_journey: journalHeader('update_journey', 'updateJournalSettings', RESTORE_JOURNEY_SETTINGS),
|
||||
delete_journey: journalHeader('delete_journey', 'deleteJournal'),
|
||||
add_journey_trip: journalHeader('add_journey_trip', 'linkJournalTrip', UNLINK_LINKED_TRIP),
|
||||
remove_journey_trip: journalHeader('remove_journey_trip', 'unlinkJournalTrip'),
|
||||
restore_journey_suggestions: journalHeader('restore_journey_suggestions', 'restoreJournalSuggestions'),
|
||||
update_journey_preferences: journalHeader(
|
||||
'update_journey_preferences',
|
||||
'setJournalPreferences',
|
||||
FLIP_JOURNEY_PREFERENCE,
|
||||
),
|
||||
},
|
||||
prompt: [
|
||||
'Journal: get_trip_journal finds the journal this trip shows. To show entries, call list_journey_entries with from/to for the days asked: the app draws them as cards, so do not list them again in your answer.',
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ToolArgs, ToolCards, UndoSnapshot } from '../card-types';
|
||||
import { argId, idIn, readKey, recordOf, type Row } from '../card-values';
|
||||
import { rowsOf } from '../undo-snapshot';
|
||||
import { journeyArgs } from './journey-header';
|
||||
|
||||
/**
|
||||
* The ways back from a change of a journal itself, each checked against the
|
||||
* journal as `get_journey` reads it now:
|
||||
* - a new journal is deleted while nothing was written, added or laid out in
|
||||
* it since (its trips' suggestions are what it was made with);
|
||||
* - changed settings are written back as they were, while the journal was not
|
||||
* changed again since;
|
||||
* - a linked trip is unlinked again, by its owner and while none of the
|
||||
* trip's suggestions was written up since;
|
||||
* - the user's own switch is flipped back while it still stands as set.
|
||||
*/
|
||||
|
||||
type Undo = NonNullable<ToolCards['undo']>;
|
||||
|
||||
/** The settings `update_journey` takes no null for. */
|
||||
const NEVER_NULL: ReadonlySet<string> = new Set(['title', 'status']);
|
||||
|
||||
function journeyRead(args: ToolArgs) {
|
||||
return { tool: 'get_journey', args };
|
||||
}
|
||||
|
||||
/** The journal a snapshot read, as `get_journey` answered. */
|
||||
function journeyIn(snapshot: UndoSnapshot, args: ToolArgs): Row {
|
||||
const read = journeyRead(args);
|
||||
return recordOf(recordOf(snapshot.reads.get(readKey(read.tool, read.args))).journey);
|
||||
}
|
||||
|
||||
/** The new journal by the id its answer names; the reads may not depend on it at boot, so they take it as given. */
|
||||
function createdArgs(result: unknown): ToolArgs {
|
||||
const journeyId = idIn(result, 'journey');
|
||||
return journeyId === null ? {} : { journeyId };
|
||||
}
|
||||
|
||||
export const DELETE_CREATED_JOURNEY: Undo = {
|
||||
tool: 'delete_journey',
|
||||
args: (_args, result) => {
|
||||
const journeyId = idIn(result, 'journey');
|
||||
return journeyId === null ? null : { journeyId };
|
||||
},
|
||||
reads: (_args, result) => [journeyRead(createdArgs(result)), { tool: 'get_book', args: createdArgs(result) }],
|
||||
unchanged: (result, snapshot) => {
|
||||
const created = recordOf(recordOf(result).journey);
|
||||
const args = createdArgs(result);
|
||||
if (args.journeyId === undefined) return false;
|
||||
const current = journeyIn(snapshot, args);
|
||||
const book = recordOf(snapshot.reads.get(readKey('get_book', args)));
|
||||
return (
|
||||
current.id === created.id &&
|
||||
current.updated_at !== undefined &&
|
||||
current.updated_at === created.updated_at &&
|
||||
rowsOf(current.entries).every((entry) => entry.type === 'skeleton') &&
|
||||
rowsOf(current.gallery).length === 0 &&
|
||||
rowsOf(current.contributors).length <= 1 &&
|
||||
book.exists === false
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const RESTORE_JOURNEY_SETTINGS: Undo = {
|
||||
tool: 'update_journey',
|
||||
args: (_args, result) => {
|
||||
const journeyId = idIn(result, 'journey');
|
||||
// The name and the status take no null; one stored empty has nothing to write back.
|
||||
const previous = Object.fromEntries(
|
||||
Object.entries(recordOf(recordOf(result).previous)).filter(
|
||||
([key, value]) => value !== null || !NEVER_NULL.has(key),
|
||||
),
|
||||
);
|
||||
return journeyId === null || Object.keys(previous).length === 0 ? null : { journeyId, ...previous };
|
||||
},
|
||||
reads: (_args, result) => [journeyRead(createdArgs(result))],
|
||||
unchanged: (result, snapshot) => {
|
||||
const changed = recordOf(recordOf(result).journey);
|
||||
const current = journeyIn(snapshot, createdArgs(result));
|
||||
return current.id === changed.id && current.updated_at !== undefined && current.updated_at === changed.updated_at;
|
||||
},
|
||||
};
|
||||
|
||||
export const UNLINK_LINKED_TRIP: Undo = {
|
||||
tool: 'remove_journey_trip',
|
||||
args: (args, result, preview) => {
|
||||
const tripId = argId(args, 'tripId');
|
||||
if (preview?.undoable === false || tripId === null || recordOf(result).success !== true) return null;
|
||||
return { ...journeyArgs(args), tripId };
|
||||
},
|
||||
reads: (args) => [journeyRead(journeyArgs(args))],
|
||||
unchanged: (_result, snapshot, call) => {
|
||||
const tripId = argId(call?.args ?? {}, 'tripId');
|
||||
if (tripId === null) return false;
|
||||
const current = journeyIn(snapshot, journeyArgs(call?.args ?? {}));
|
||||
const stillLinked = rowsOf(current.trips).some((trip) => trip.trip_id === tripId);
|
||||
const written = rowsOf(current.entries).some(
|
||||
(entry) => entry.source_trip_id === tripId && entry.type !== 'skeleton',
|
||||
);
|
||||
return stillLinked && !written;
|
||||
},
|
||||
};
|
||||
|
||||
export const FLIP_JOURNEY_PREFERENCE: Undo = {
|
||||
tool: 'update_journey_preferences',
|
||||
args: (args, result, preview) => {
|
||||
if (preview?.undoable === false || !('hide_skeletons' in args) || !('hide_skeletons' in recordOf(result))) {
|
||||
return null;
|
||||
}
|
||||
return { ...journeyArgs(args), hide_skeletons: !args.hide_skeletons };
|
||||
},
|
||||
reads: (args) => [journeyRead(journeyArgs(args))],
|
||||
unchanged: (result, snapshot, call) => {
|
||||
const current = journeyIn(snapshot, journeyArgs(call?.args ?? {}));
|
||||
return 'hide_skeletons' in current && !!current.hide_skeletons === !!recordOf(result).hide_skeletons;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,327 @@
|
||||
import type { CardReader, Described, PreviewContext, ToolArgs } from '../card-types';
|
||||
import { argId, clipped, recordOf, type Row } from '../card-values';
|
||||
import { Rows, text } from '../preview-format';
|
||||
import { rowsOf } from '../undo-snapshot';
|
||||
import {
|
||||
JOURNEY_HEADER_LIMITS,
|
||||
type AssistantChangePreview,
|
||||
type AssistantChangeRow,
|
||||
type AssistantChangeVerbKey,
|
||||
type JourneyHeaderCard,
|
||||
type JourneyHeaderTrip,
|
||||
} from '@trek/shared';
|
||||
|
||||
/**
|
||||
* The change cards of a journal itself: a new one, its settings changed, a
|
||||
* trip linked or unlinked, its suggestions brought back, the user's own view
|
||||
* of it, the whole journal deleted. The rows name what changes, old to new;
|
||||
* the card under them is the journal with its trips and what the change takes
|
||||
* along. Everything is read with the user's rights: the journal as
|
||||
* `get_journey` answers it, the trips it could link as
|
||||
* `list_journey_available_trips` does.
|
||||
*/
|
||||
|
||||
/** What a held journal change looked up before its card is drawn. */
|
||||
export interface JournalHeaderLoaded {
|
||||
/** The journal as `get_journey` answers it; null for a new one or a read that failed. */
|
||||
journey: Row | null;
|
||||
/** The user's trips a journal could link. */
|
||||
available: Row[];
|
||||
}
|
||||
|
||||
/** The tools these cards are for. */
|
||||
export const JOURNAL_HEADER_TOOLS = [
|
||||
'create_journey',
|
||||
'update_journey',
|
||||
'delete_journey',
|
||||
'add_journey_trip',
|
||||
'remove_journey_trip',
|
||||
'restore_journey_suggestions',
|
||||
'update_journey_preferences',
|
||||
] as const;
|
||||
type HeaderTool = (typeof JOURNAL_HEADER_TOOLS)[number];
|
||||
|
||||
/** The journal a call names, or none in a journey's own conversation, where the session pins it. */
|
||||
export function journeyArgs(args: ToolArgs): ToolArgs {
|
||||
const journeyId = argId(args, 'journeyId');
|
||||
return journeyId === null ? {} : { journeyId };
|
||||
}
|
||||
|
||||
/** At most two reads: the journal (all but a new one), the user's trips (a new journal, a trip linked). */
|
||||
export async function loadJournalHeader(tool: string, args: ToolArgs, read: CardReader): Promise<JournalHeaderLoaded> {
|
||||
const withTrips = tool === 'create_journey' || tool === 'add_journey_trip';
|
||||
const available = withTrips ? rowsOf(recordOf(await read('list_journey_available_trips')).trips) : [];
|
||||
const found = tool === 'create_journey' ? null : recordOf(await read('get_journey', journeyArgs(args))).journey;
|
||||
return { journey: found ? recordOf(found) : null, available };
|
||||
}
|
||||
|
||||
/** The settings an update can change beyond the name and subtitle, each a `field` row under the tool's own name. */
|
||||
const SETTING_FIELDS = [
|
||||
'status',
|
||||
'status_override',
|
||||
'show_verdict',
|
||||
'show_mood',
|
||||
'show_weather',
|
||||
'photo_location',
|
||||
'show_trip_tracks',
|
||||
'auto_prepare_recordings',
|
||||
'cover_gradient',
|
||||
] as const;
|
||||
const FLAG_FIELDS: ReadonlySet<string> = new Set([
|
||||
'show_verdict',
|
||||
'show_mood',
|
||||
'show_weather',
|
||||
'photo_location',
|
||||
'show_trip_tracks',
|
||||
'auto_prepare_recordings',
|
||||
'hide_skeletons',
|
||||
]);
|
||||
const STATUS_FIELDS: ReadonlySet<string> = new Set(['status', 'status_override']);
|
||||
|
||||
/** A setting as its row writes it: a flag (stored 0 or 1, sent as a boolean) as ✓ or ✗, anything else as it is. */
|
||||
function settingText(field: string, value: unknown): string {
|
||||
return FLAG_FIELDS.has(field) ? text(value === true || value === 1) : text(value);
|
||||
}
|
||||
|
||||
function fieldRow(field: string, before: string | null, after: string): AssistantChangeRow {
|
||||
return { op: before === null ? 'add' : 'change', labelKey: 'field', field, before, after };
|
||||
}
|
||||
|
||||
function described(subject: string, rows: Rows, extra: Partial<Described> = {}): Described {
|
||||
return { subject, rows, dayId: null, placeIds: [], domain: 'journey', ...extra };
|
||||
}
|
||||
|
||||
/** Which verb an update reads as: a new name alone, a new status alone, else the settings. */
|
||||
function updateVerb(args: ToolArgs): AssistantChangeVerbKey {
|
||||
const keys = Object.keys(args).filter((key) => key !== 'journeyId');
|
||||
if (keys.length > 0 && keys.every((key) => key === 'title')) return 'renameJournal';
|
||||
if (keys.length > 0 && keys.every((key) => STATUS_FIELDS.has(key))) return 'setJournalStatus';
|
||||
return 'updateJournalSettings';
|
||||
}
|
||||
|
||||
/** The trip a call names, by the user's trips or the journal's own. */
|
||||
function tripNamed(loaded: JournalHeaderLoaded | null, tripId: number | null): Row | null {
|
||||
if (tripId === null) return null;
|
||||
const own = rowsOf(loaded?.journey?.trips).find((trip) => trip.trip_id === tripId);
|
||||
if (own) return { id: own.trip_id, title: own.title, start_date: own.start_date, end_date: own.end_date };
|
||||
return loaded?.available.find((trip) => trip.id === tripId) ?? null;
|
||||
}
|
||||
|
||||
function linked(loaded: JournalHeaderLoaded | null, tripId: number | null): boolean {
|
||||
return tripId !== null && rowsOf(loaded?.journey?.trips).some((trip) => trip.trip_id === tripId);
|
||||
}
|
||||
|
||||
function updateRows(before: Row, args: ToolArgs): Rows {
|
||||
const rows = new Rows();
|
||||
if ('title' in args && text(args.title) !== text(before.title))
|
||||
rows.change('name', text(before.title), text(args.title));
|
||||
if ('subtitle' in args && text(args.subtitle) !== text(before.subtitle)) {
|
||||
rows.change('description', text(before.subtitle), text(args.subtitle));
|
||||
}
|
||||
for (const field of SETTING_FIELDS) {
|
||||
if (!(field in args)) continue;
|
||||
const was = field in before ? settingText(field, before[field]) : null;
|
||||
const now = settingText(field, args[field]);
|
||||
if (was !== now) rows.rows.push(fieldRow(field, was, now));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const TRIP_ROW = (verb: 'add' | 'remove', name: string): AssistantChangeRow =>
|
||||
verb === 'add'
|
||||
? { op: 'add', labelKey: 'field', field: 'trip', before: null, after: name }
|
||||
: { op: 'change', labelKey: 'field', field: 'trip', before: name, after: '' };
|
||||
|
||||
export const JOURNAL_HEADER_DESCRIBERS: Readonly<
|
||||
Record<HeaderTool, (ctx: PreviewContext, args: ToolArgs, loaded: JournalHeaderLoaded | null) => Described>
|
||||
> = {
|
||||
create_journey: (_ctx, args) =>
|
||||
described(text(args.title), new Rows().add('name', text(args.title)).add('description', text(args.subtitle))),
|
||||
update_journey: (_ctx, args, loaded) => {
|
||||
const before = loaded?.journey ?? {};
|
||||
return described(text(before.title) || text(args.title), updateRows(before, args), { verbKey: updateVerb(args) });
|
||||
},
|
||||
delete_journey: (_ctx, _args, loaded) => described(text(loaded?.journey?.title), new Rows()),
|
||||
add_journey_trip: (_ctx, args, loaded) => {
|
||||
const tripId = argId(args, 'tripId');
|
||||
const rows = new Rows();
|
||||
rows.rows.push(TRIP_ROW('add', text(tripNamed(loaded, tripId)?.title)));
|
||||
// Unlinking takes the owner; a trip linked already would be unlinked by a way back that never linked it.
|
||||
const owner = loaded?.journey?.my_role === 'owner';
|
||||
return described(text(loaded?.journey?.title), rows, { undoable: owner && !linked(loaded, tripId) });
|
||||
},
|
||||
remove_journey_trip: (_ctx, args, loaded) => {
|
||||
const rows = new Rows();
|
||||
rows.rows.push(TRIP_ROW('remove', text(tripNamed(loaded, argId(args, 'tripId'))?.title)));
|
||||
return described(text(loaded?.journey?.title), rows);
|
||||
},
|
||||
restore_journey_suggestions: (_ctx, _args, loaded) => {
|
||||
const rows = new Rows();
|
||||
const dismissed = Number(loaded?.journey?.dismissed_count);
|
||||
if (Number.isInteger(dismissed) && dismissed > 0) rows.rows.push(fieldRow('suggestions', null, String(dismissed)));
|
||||
return described(text(loaded?.journey?.title), rows);
|
||||
},
|
||||
update_journey_preferences: (_ctx, args, loaded) => {
|
||||
const rows = new Rows();
|
||||
const known = loaded?.journey && 'hide_skeletons' in loaded.journey;
|
||||
const was = known ? settingText('hide_skeletons', loaded?.journey?.hide_skeletons) : null;
|
||||
const now = 'hide_skeletons' in args ? settingText('hide_skeletons', args.hide_skeletons) : null;
|
||||
if (now !== null && now !== was) rows.rows.push(fieldRow('hide_skeletons', was, now));
|
||||
// A way back that flips the switch is a way back only where the switch was known to change.
|
||||
return described(text(loaded?.journey?.title), rows, { undoable: was !== null && now !== null && was !== now });
|
||||
},
|
||||
};
|
||||
|
||||
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
function dateOf(value: unknown): string | null {
|
||||
return typeof value === 'string' && DATE.test(value.slice(0, 10)) ? value.slice(0, 10) : null;
|
||||
}
|
||||
|
||||
function tripLook(row: Row, change: JourneyHeaderTrip['change'], idKey: 'id' | 'trip_id'): JourneyHeaderTrip | null {
|
||||
const title = clipped(row.title, JOURNEY_HEADER_LIMITS.tripTitle);
|
||||
if (!title) return null;
|
||||
const id = argId(row, idKey);
|
||||
return { id, title, startDate: dateOf(row.start_date), endDate: dateOf(row.end_date), change };
|
||||
}
|
||||
|
||||
function count(value: unknown): number | null {
|
||||
const n = Number(value);
|
||||
return Number.isInteger(n) && n >= 0 ? Math.min(n, 1_000_000) : null;
|
||||
}
|
||||
|
||||
/** The journal's own trips, with what the change does to the one it names; that one first, so a cut keeps it. */
|
||||
function journalTrips(tool: string, args: ToolArgs, loaded: JournalHeaderLoaded): JourneyHeaderTrip[] {
|
||||
const tripId = argId(args, 'tripId');
|
||||
const own = rowsOf(loaded.journey?.trips).flatMap((row) => {
|
||||
const change = tool === 'remove_journey_trip' && row.trip_id === tripId ? 'unlink' : 'kept';
|
||||
const look = tripLook(row, change, 'trip_id');
|
||||
return look ? [look] : [];
|
||||
});
|
||||
const changedFirst = [
|
||||
...own.filter((trip) => trip.change !== 'kept'),
|
||||
...own.filter((trip) => trip.change === 'kept'),
|
||||
];
|
||||
if (tool !== 'add_journey_trip' || linked(loaded, tripId)) return changedFirst;
|
||||
const added = loaded.available.find((row) => row.id === tripId);
|
||||
const look = added ? tripLook(added, 'link', 'id') : null;
|
||||
return look ? [look, ...changedFirst] : changedFirst;
|
||||
}
|
||||
|
||||
/** The trips a new journal links, by the user's trips; an id the user's trips do not list is left out. */
|
||||
function newTrips(args: ToolArgs, loaded: JournalHeaderLoaded): JourneyHeaderTrip[] {
|
||||
const ids = Array.isArray(args.trip_ids) ? args.trip_ids : [];
|
||||
return ids.flatMap((id) => {
|
||||
const row = loaded.available.find((trip) => trip.id === id);
|
||||
const look = row ? tripLook(row, 'link', 'id') : null;
|
||||
return look ? [look] : [];
|
||||
});
|
||||
}
|
||||
|
||||
/** The places of the trips linked by the change, which come in as suggestions. */
|
||||
function suggestionsOf(trips: readonly JourneyHeaderTrip[], loaded: JournalHeaderLoaded): number | null {
|
||||
const linking = trips.filter((trip) => trip.change === 'link' && trip.id !== null);
|
||||
if (linking.length === 0) return null;
|
||||
const places = linking.map((trip) => count(loaded.available.find((row) => row.id === trip.id)?.place_count) ?? 0);
|
||||
return places.reduce((sum, n) => sum + n, 0);
|
||||
}
|
||||
|
||||
/** What the journal holds that the change concerns: all of it, or what one unlinked trip leaves behind. */
|
||||
function heldBy(tool: string, args: ToolArgs, journey: Row): { entries: number | null; photos: number | null } {
|
||||
const entries = rowsOf(journey.entries).filter((entry) => !entry.dismissed);
|
||||
if (tool === 'remove_journey_trip') {
|
||||
const tripId = argId(args, 'tripId');
|
||||
return {
|
||||
entries: entries.filter((e) => e.source_trip_id === tripId && e.type !== 'skeleton').length,
|
||||
photos: null,
|
||||
};
|
||||
}
|
||||
const written = entries.filter((entry) => entry.type !== 'skeleton').length;
|
||||
return { entries: written, photos: Array.isArray(journey.gallery) ? journey.gallery.length : null };
|
||||
}
|
||||
|
||||
/** The suggestions an unlinked trip takes with it: its places nobody wrote up. */
|
||||
function unlinkedSuggestions(args: ToolArgs, journey: Row): number {
|
||||
const tripId = argId(args, 'tripId');
|
||||
return rowsOf(journey.entries).filter((e) => e.source_trip_id === tripId && e.type === 'skeleton').length;
|
||||
}
|
||||
|
||||
/** The card under a held journal change; null when it has no journal to name. */
|
||||
export function journalHeaderCard(
|
||||
tool: string,
|
||||
args: ToolArgs,
|
||||
loaded: JournalHeaderLoaded | null,
|
||||
): JourneyHeaderCard | null {
|
||||
if (!loaded) return null;
|
||||
if (tool === 'create_journey') {
|
||||
const title = clipped(args.title, JOURNEY_HEADER_LIMITS.title);
|
||||
if (!title) return null;
|
||||
const trips = newTrips(args, loaded).slice(0, JOURNEY_HEADER_LIMITS.trips);
|
||||
const subtitle = clipped(args.subtitle, JOURNEY_HEADER_LIMITS.subtitle);
|
||||
const suggestions = suggestionsOf(trips, loaded);
|
||||
return {
|
||||
kind: 'journeyHeader',
|
||||
journeyId: null,
|
||||
title,
|
||||
subtitle,
|
||||
change: 'new',
|
||||
trips,
|
||||
entries: null,
|
||||
photos: null,
|
||||
suggestions,
|
||||
};
|
||||
}
|
||||
const journey = loaded.journey;
|
||||
const title = clipped(journey?.title, JOURNEY_HEADER_LIMITS.title);
|
||||
if (!journey || !title) return null;
|
||||
const trips = journalTrips(tool, args, loaded).slice(0, JOURNEY_HEADER_LIMITS.trips);
|
||||
const held = heldBy(tool, args, journey);
|
||||
const suggestions =
|
||||
tool === 'remove_journey_trip' ? unlinkedSuggestions(args, journey) : suggestionsOf(trips, loaded);
|
||||
return {
|
||||
kind: 'journeyHeader',
|
||||
journeyId: argId(journey, 'id'),
|
||||
title,
|
||||
subtitle: clipped(journey.subtitle, JOURNEY_HEADER_LIMITS.subtitle),
|
||||
change: tool === 'delete_journey' ? 'willDelete' : 'changed',
|
||||
trips,
|
||||
entries: tool === 'add_journey_trip' ? null : held.entries,
|
||||
photos: tool === 'delete_journey' ? held.photos : null,
|
||||
suggestions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The card of an applied journal change: a new journal as the tool answered
|
||||
* it (its id, its trips), a changed one with its new name and subtitle over
|
||||
* the card it was held with. Every other change keeps its held card.
|
||||
*/
|
||||
export function journalHeaderApplied(
|
||||
tool: string,
|
||||
result: unknown,
|
||||
preview: AssistantChangePreview | null,
|
||||
): JourneyHeaderCard | null {
|
||||
const journey = recordOf(recordOf(result).journey);
|
||||
const held = preview?.card?.kind === 'journeyHeader' ? preview.card : null;
|
||||
const title = clipped(journey.title, JOURNEY_HEADER_LIMITS.title);
|
||||
if (!title) return null;
|
||||
const subtitle = clipped(journey.subtitle, JOURNEY_HEADER_LIMITS.subtitle);
|
||||
if (tool === 'update_journey') return held ? { ...held, journeyId: argId(journey, 'id'), title, subtitle } : null;
|
||||
if (tool !== 'create_journey') return null;
|
||||
const trips = rowsOf(journey.trips).flatMap((row) => {
|
||||
const look = tripLook(row, 'link', 'trip_id');
|
||||
return look ? [look] : [];
|
||||
});
|
||||
const suggestions = rowsOf(journey.entries).filter((entry) => entry.type === 'skeleton').length;
|
||||
return {
|
||||
kind: 'journeyHeader',
|
||||
journeyId: argId(journey, 'id'),
|
||||
title,
|
||||
subtitle,
|
||||
change: 'new',
|
||||
trips: trips.slice(0, JOURNEY_HEADER_LIMITS.trips),
|
||||
entries: null,
|
||||
photos: null,
|
||||
suggestions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* ASTCARD-JHD-001 to ASTCARD-JHD-012
|
||||
*
|
||||
* A change of a journal itself in the assistant chat: what a held change
|
||||
* reads first, the words of its row (the verb an update reads as, each
|
||||
* setting old to new), the journal card under it with its trips and what the
|
||||
* change brings or takes, the receipt, and the ways back with their checks.
|
||||
* Every card is checked against the shared union through the registry's guard.
|
||||
*/
|
||||
import { buildTripCard } from '../../../../../../src/nest/assistant/assistant-context';
|
||||
import type { CardReader, PreviewContext, UndoSnapshot } from '../../../../../../src/nest/assistant/cards/card-types';
|
||||
import { readKey } from '../../../../../../src/nest/assistant/cards/card-values';
|
||||
import { JOURNEY_CARDS } from '../../../../../../src/nest/assistant/cards/journey';
|
||||
import {
|
||||
loadJournalHeader,
|
||||
type JournalHeaderLoaded,
|
||||
} from '../../../../../../src/nest/assistant/cards/journey/journey-header';
|
||||
import {
|
||||
appliedCardOf,
|
||||
ASSISTANT_UNDO_MAP,
|
||||
ASSISTANT_WRITE_TOOLS,
|
||||
changeCardOf,
|
||||
undoReadsOf,
|
||||
} from '../../../../../../src/nest/assistant/cards/registry';
|
||||
import { appliedChange, describeToolCall } from '../../../../../../src/nest/assistant/tool-call-preview.helpers';
|
||||
import { journeyHeaderCardSchema, type AssistantChangePreview, type JourneyHeaderCard } from '@trek/shared';
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../../../../../src/nest/audit/audit-log.logger', () => ({
|
||||
logInfo: vi.fn(),
|
||||
logError: vi.fn(),
|
||||
logWarn: vi.fn(),
|
||||
logDebug: vi.fn(),
|
||||
}));
|
||||
|
||||
const ctx: PreviewContext = {
|
||||
card: buildTripCard(1, 7, { trip: { title: 'Berlin' } }, null, null),
|
||||
categories: new Map(),
|
||||
locale: 'en',
|
||||
today: '2026-10-24',
|
||||
airport: () => null,
|
||||
};
|
||||
|
||||
const AVAILABLE = [
|
||||
{ id: 5, title: 'Tokyo days', start_date: '2026-11-02', end_date: '2026-11-08', place_count: 9 },
|
||||
{ id: 6, title: 'Kyoto days', start_date: '2026-11-09', end_date: '2026-11-12', place_count: 4 },
|
||||
];
|
||||
|
||||
function journey(over: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
id: 12,
|
||||
title: 'Japan 2026',
|
||||
subtitle: null,
|
||||
status: 'active',
|
||||
status_override: null,
|
||||
show_mood: 1,
|
||||
show_verdict: 0,
|
||||
hide_skeletons: 0,
|
||||
my_role: 'owner',
|
||||
updated_at: 100,
|
||||
dismissed_count: 3,
|
||||
trips: [{ trip_id: 5, title: 'Tokyo days', start_date: '2026-11-02', end_date: '2026-11-08' }],
|
||||
entries: [
|
||||
{ id: 1, type: 'entry', source_trip_id: 5, dismissed: false },
|
||||
{ id: 2, type: 'skeleton', source_trip_id: 5, dismissed: false },
|
||||
{ id: 3, type: 'skeleton', source_trip_id: 5, dismissed: false },
|
||||
],
|
||||
gallery: [{ id: 1 }, { id: 2 }],
|
||||
contributors: [{ user_id: 7 }],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function readerOf(answers: Record<string, unknown>): { read: CardReader; asked: string[] } {
|
||||
const asked: string[] = [];
|
||||
const read: CardReader = (tool, args = {}) => {
|
||||
asked.push(readKey(tool, args));
|
||||
return Promise.resolve(answers[readKey(tool, args)] ?? answers[tool] ?? null);
|
||||
};
|
||||
return { read, asked };
|
||||
}
|
||||
|
||||
const loadedWith = (over: Record<string, unknown> = {}): JournalHeaderLoaded => ({
|
||||
journey: journey(over),
|
||||
available: AVAILABLE,
|
||||
});
|
||||
|
||||
function cardOf(value: unknown): JourneyHeaderCard {
|
||||
return journeyHeaderCardSchema.parse(value);
|
||||
}
|
||||
|
||||
describe('the lookups of a journal change', () => {
|
||||
it('ASTCARD-JHD-001: a new journal reads the trips it could link, a change the journal it is on', async () => {
|
||||
const created = readerOf({ list_journey_available_trips: { trips: AVAILABLE } });
|
||||
expect(await loadJournalHeader('create_journey', { title: 'Japan' }, created.read)).toEqual({
|
||||
journey: null,
|
||||
available: AVAILABLE,
|
||||
});
|
||||
expect(created.asked).toEqual([readKey('list_journey_available_trips')]);
|
||||
|
||||
const named = readerOf({ get_journey: { journey: journey() } });
|
||||
const loaded = await loadJournalHeader('update_journey', { journeyId: 12, title: 'X' }, named.read);
|
||||
expect(loaded.journey).toMatchObject({ id: 12 });
|
||||
expect(named.asked).toEqual([readKey('get_journey', { journeyId: 12 })]);
|
||||
// A journey's own conversation pins the journal, so the read names none.
|
||||
const pinned = readerOf({});
|
||||
expect(await loadJournalHeader('delete_journey', {}, pinned.read)).toEqual({ journey: null, available: [] });
|
||||
expect(pinned.asked).toEqual([readKey('get_journey', {})]);
|
||||
const linking = readerOf({});
|
||||
await loadJournalHeader('add_journey_trip', { journeyId: 12, tripId: 6 }, linking.read);
|
||||
expect(linking.asked).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the words of a journal change', () => {
|
||||
it('ASTCARD-JHD-002: an update reads as a rename, a status change or a change of settings', () => {
|
||||
const loaded = loadedWith();
|
||||
const rename = describeToolCall('update_journey', 'c1', { journeyId: 12, title: 'Nippon' }, ctx, loaded);
|
||||
expect(rename).toMatchObject({ verbKey: 'renameJournal', subject: 'Japan 2026', undoable: true });
|
||||
expect(rename.rows).toEqual([{ op: 'change', labelKey: 'name', before: 'Japan 2026', after: 'Nippon' }]);
|
||||
const status = describeToolCall('update_journey', 'c2', { status_override: 'live' }, ctx, loaded);
|
||||
expect(status.verbKey).toBe('setJournalStatus');
|
||||
expect(status.rows).toEqual([
|
||||
{ op: 'change', labelKey: 'field', field: 'status_override', before: '', after: 'live' },
|
||||
]);
|
||||
const settings = describeToolCall(
|
||||
'update_journey',
|
||||
'c3',
|
||||
{ show_mood: false, show_verdict: false, subtitle: 'Autumn', show_trip_tracks: true },
|
||||
ctx,
|
||||
loaded,
|
||||
);
|
||||
expect(settings.verbKey).toBe('updateJournalSettings');
|
||||
// A setting set to what it holds gets no row; one the journal does not name has no before.
|
||||
expect(settings.rows).toEqual([
|
||||
{ op: 'change', labelKey: 'description', before: null, after: 'Autumn' },
|
||||
{ op: 'change', labelKey: 'field', field: 'show_mood', before: '✓', after: '✗' },
|
||||
{ op: 'add', labelKey: 'field', field: 'show_trip_tracks', before: null, after: '✓' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-003: a trip linked names it, and is undoable only for the owner and a trip not linked yet', () => {
|
||||
const add = describeToolCall('add_journey_trip', 'c1', { journeyId: 12, tripId: 6 }, ctx, loadedWith());
|
||||
expect(add).toMatchObject({ verbKey: 'linkJournalTrip', subject: 'Japan 2026', undoable: true });
|
||||
expect(add.rows).toEqual([{ op: 'add', labelKey: 'field', field: 'trip', before: null, after: 'Kyoto days' }]);
|
||||
const again = describeToolCall('add_journey_trip', 'c2', { journeyId: 12, tripId: 5 }, ctx, loadedWith());
|
||||
expect(again.undoable).toBe(false);
|
||||
const editor = describeToolCall('add_journey_trip', 'c3', { tripId: 6 }, ctx, loadedWith({ my_role: 'editor' }));
|
||||
expect(editor.undoable).toBe(false);
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-004: the other changes name the journal, and a deletion or an unlink has no way back', () => {
|
||||
const loaded = loadedWith();
|
||||
expect(describeToolCall('delete_journey', 'c1', { journeyId: 12 }, ctx, loaded)).toMatchObject({
|
||||
verbKey: 'deleteJournal',
|
||||
subject: 'Japan 2026',
|
||||
undoable: false,
|
||||
});
|
||||
const unlink = describeToolCall('remove_journey_trip', 'c2', { tripId: 5 }, ctx, loaded);
|
||||
expect(unlink).toMatchObject({ verbKey: 'unlinkJournalTrip', undoable: false });
|
||||
expect(unlink.rows).toEqual([{ op: 'change', labelKey: 'field', field: 'trip', before: 'Tokyo days', after: '' }]);
|
||||
const restore = describeToolCall('restore_journey_suggestions', 'c3', {}, ctx, loaded);
|
||||
expect(restore).toMatchObject({ verbKey: 'restoreJournalSuggestions', undoable: false });
|
||||
expect(restore.rows).toEqual([{ op: 'add', labelKey: 'field', field: 'suggestions', before: null, after: '3' }]);
|
||||
const prefs = describeToolCall('update_journey_preferences', 'c4', { hide_skeletons: true }, ctx, loaded);
|
||||
expect(prefs).toMatchObject({ verbKey: 'setJournalPreferences', undoable: true });
|
||||
expect(prefs.rows).toEqual([{ op: 'change', labelKey: 'field', field: 'hide_skeletons', before: '✗', after: '✓' }]);
|
||||
// Set to what it holds, the switch has no way back that would mean anything.
|
||||
expect(describeToolCall('update_journey_preferences', 'c5', { hide_skeletons: false }, ctx, loaded).undoable).toBe(
|
||||
false,
|
||||
);
|
||||
expect(describeToolCall('create_journey', 'c6', { title: 'Japan' }, ctx, null)).toMatchObject({
|
||||
verbKey: 'createJournal',
|
||||
subject: 'Japan',
|
||||
undoable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('the journal card under a change', () => {
|
||||
it('ASTCARD-JHD-005: a new journal shows the trips it links and the places that come in as suggestions', () => {
|
||||
const loaded: JournalHeaderLoaded = { journey: null, available: AVAILABLE };
|
||||
const card = cardOf(
|
||||
changeCardOf('create_journey', ctx, { title: 'Japan 2026', subtitle: 'Autumn', trip_ids: [5, 6, 99] }, loaded),
|
||||
);
|
||||
expect(card).toMatchObject({ journeyId: null, title: 'Japan 2026', subtitle: 'Autumn', change: 'new' });
|
||||
expect(card.trips.map((trip) => [trip.id, trip.change])).toEqual([
|
||||
[5, 'link'],
|
||||
[6, 'link'],
|
||||
]);
|
||||
expect(card.suggestions).toBe(13);
|
||||
expect(changeCardOf('create_journey', ctx, {}, loaded)).toBeNull();
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-006: a deletion counts what goes, an unlink what stays and what goes, a link comes first', () => {
|
||||
const loaded = loadedWith();
|
||||
const deleting = cardOf(changeCardOf('delete_journey', ctx, {}, loaded));
|
||||
expect(deleting).toMatchObject({ change: 'willDelete', journeyId: 12, entries: 1, photos: 2 });
|
||||
const unlinking = cardOf(changeCardOf('remove_journey_trip', ctx, { tripId: 5 }, loaded));
|
||||
expect(unlinking).toMatchObject({ change: 'changed', entries: 1, suggestions: 2, photos: null });
|
||||
expect(unlinking.trips).toEqual([
|
||||
{ id: 5, title: 'Tokyo days', startDate: '2026-11-02', endDate: '2026-11-08', change: 'unlink' },
|
||||
]);
|
||||
const linking = cardOf(changeCardOf('add_journey_trip', ctx, { tripId: 6 }, loaded));
|
||||
expect(linking.trips.map((trip) => [trip.id, trip.change])).toEqual([
|
||||
[6, 'link'],
|
||||
[5, 'kept'],
|
||||
]);
|
||||
expect(linking).toMatchObject({ suggestions: 4, entries: null });
|
||||
expect(changeCardOf('update_journey', ctx, { title: 'X' }, { journey: null, available: [] })).toBeNull();
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-007: the receipt of a new journal names it with its id, a change takes its new name', () => {
|
||||
const result = { journey: journey({ entries: [{ type: 'skeleton' }, { type: 'skeleton' }] }) };
|
||||
const created = cardOf(appliedCardOf('create_journey', { args: {}, result, preview: null }));
|
||||
expect(created).toMatchObject({ journeyId: 12, change: 'new', suggestions: 2 });
|
||||
expect(created.trips[0]).toMatchObject({ id: 5, change: 'link' });
|
||||
const held = changeCardOf('update_journey', ctx, { title: 'Nippon' }, loadedWith());
|
||||
const preview = { card: held } as unknown as AssistantChangePreview;
|
||||
const renamed = { journey: journey({ title: 'Nippon' }), previous: { title: 'Japan 2026' } };
|
||||
expect(appliedCardOf('update_journey', { args: {}, result: renamed, preview })).toMatchObject({
|
||||
title: 'Nippon',
|
||||
journeyId: 12,
|
||||
});
|
||||
expect(appliedCardOf('delete_journey', { args: {}, result: { success: true }, preview })).toBeNull();
|
||||
expect(appliedChange('create_journey', 'c1', {}, result, null, null)).toMatchObject({
|
||||
entity: 'journey',
|
||||
ids: [12],
|
||||
});
|
||||
expect(appliedChange('delete_journey', 'c2', { journeyId: 12 }, { success: true }, null, null)?.ids).toEqual([12]);
|
||||
});
|
||||
});
|
||||
|
||||
function snapshotOf(entries: Array<[string, Record<string, unknown>, unknown]>): UndoSnapshot {
|
||||
return {
|
||||
summary: null,
|
||||
places: null,
|
||||
reads: new Map(entries.map(([tool, args, value]) => [readKey(tool, args), value])),
|
||||
};
|
||||
}
|
||||
|
||||
function undoOf(tool: string) {
|
||||
const undo = JOURNEY_CARDS.tools[tool]?.undo;
|
||||
if (!undo) throw new Error(`no undo for ${tool}`);
|
||||
return undo;
|
||||
}
|
||||
|
||||
describe('the ways back from a journal change', () => {
|
||||
it('ASTCARD-JHD-008: a new journal is deleted while nothing was written, added or laid out in it', () => {
|
||||
const undo = undoOf('create_journey');
|
||||
const created = journey({ entries: [{ type: 'skeleton' }], gallery: [] });
|
||||
const result = { journey: created };
|
||||
expect(ASSISTANT_UNDO_MAP.create_journey?.tool).toBe('delete_journey');
|
||||
expect(undo.args({}, result, null)).toEqual({ journeyId: 12 });
|
||||
expect(undo.args({}, {}, null)).toBeNull();
|
||||
expect(undoReadsOf('create_journey', {}, result)).toEqual([
|
||||
{ tool: 'get_journey', args: { journeyId: 12 } },
|
||||
{ tool: 'get_book', args: { journeyId: 12 } },
|
||||
]);
|
||||
const check = (current: Record<string, unknown>, book: unknown = { exists: false }) =>
|
||||
undo.unchanged(
|
||||
result,
|
||||
snapshotOf([
|
||||
['get_journey', { journeyId: 12 }, { journey: current }],
|
||||
['get_book', { journeyId: 12 }, book],
|
||||
]),
|
||||
);
|
||||
expect(check(created)).toBe(true);
|
||||
expect(check({ ...created, updated_at: 101 })).toBe(false);
|
||||
expect(check({ ...created, entries: [{ type: 'entry' }] })).toBe(false);
|
||||
expect(check({ ...created, gallery: [{ id: 1 }] })).toBe(false);
|
||||
expect(check({ ...created, contributors: [{ user_id: 7 }, { user_id: 8 }] })).toBe(false);
|
||||
expect(check(created, { exists: true })).toBe(false);
|
||||
expect(check(created, null)).toBe(false);
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-009: changed settings are written back as they were, while the journal was not changed since', () => {
|
||||
const undo = undoOf('update_journey');
|
||||
const result = { journey: journey({ title: 'Nippon' }), previous: { title: 'Japan 2026', show_mood: true } };
|
||||
expect(undo.args({ title: 'Nippon' }, result, null)).toEqual({
|
||||
journeyId: 12,
|
||||
title: 'Japan 2026',
|
||||
show_mood: true,
|
||||
});
|
||||
expect(undo.args({}, { journey: journey(), previous: {} }, null)).toBeNull();
|
||||
// A name or a status stored empty has nothing the tool could take back.
|
||||
expect(undo.args({}, { journey: journey(), previous: { title: null, subtitle: null } }, null)).toEqual({
|
||||
journeyId: 12,
|
||||
subtitle: null,
|
||||
});
|
||||
const at = (updated: number) =>
|
||||
undo.unchanged(
|
||||
result,
|
||||
snapshotOf([['get_journey', { journeyId: 12 }, { journey: journey({ updated_at: updated }) }]]),
|
||||
);
|
||||
expect(at(100)).toBe(true);
|
||||
expect(at(150)).toBe(false);
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-010: a linked trip is unlinked by its owner while none of its suggestions was written up', () => {
|
||||
const undo = undoOf('add_journey_trip');
|
||||
const args = { journeyId: 12, tripId: 6 };
|
||||
expect(undo.args(args, { success: true }, null)).toEqual({ journeyId: 12, tripId: 6 });
|
||||
expect(undo.args({ tripId: 6 }, { success: true }, null)).toEqual({ tripId: 6 });
|
||||
expect(undo.args(args, { success: false }, null)).toBeNull();
|
||||
const notOwner = { undoable: false } as unknown as AssistantChangePreview;
|
||||
expect(undo.args(args, { success: true }, notOwner)).toBeNull();
|
||||
const linked = { trips: [{ trip_id: 6 }], entries: [{ type: 'skeleton', source_trip_id: 6 }] };
|
||||
const check = (current: Record<string, unknown>) =>
|
||||
undo.unchanged({ success: true }, snapshotOf([['get_journey', { journeyId: 12 }, { journey: current }]]), {
|
||||
args,
|
||||
preview: null,
|
||||
});
|
||||
expect(check(linked)).toBe(true);
|
||||
expect(check({ ...linked, entries: [{ type: 'entry', source_trip_id: 6 }] })).toBe(false);
|
||||
expect(check({ ...linked, trips: [] })).toBe(false);
|
||||
expect(undo.unchanged({ success: true }, snapshotOf([]), { args: {}, preview: null })).toBe(false);
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-011: the own switch flips back while it still stands as set', () => {
|
||||
const undo = undoOf('update_journey_preferences');
|
||||
const args = { journeyId: 12, hide_skeletons: true };
|
||||
expect(undo.args(args, { hide_skeletons: 1 }, null)).toEqual({ journeyId: 12, hide_skeletons: false });
|
||||
expect(undo.args({ journeyId: 12 }, { hide_skeletons: 1 }, null)).toBeNull();
|
||||
const same = { undoable: false } as unknown as AssistantChangePreview;
|
||||
expect(undo.args(args, { hide_skeletons: 1 }, same)).toBeNull();
|
||||
const check = (stored: unknown) =>
|
||||
undo.unchanged(
|
||||
{ hide_skeletons: 1 },
|
||||
snapshotOf([['get_journey', { journeyId: 12 }, { journey: { hide_skeletons: stored } }]]),
|
||||
{ args, preview: null },
|
||||
);
|
||||
expect(check(1)).toBe(true);
|
||||
expect(check(0)).toBe(false);
|
||||
});
|
||||
|
||||
it('ASTCARD-JHD-012: the journal changes bring their verbs, the journal as entity, and no way back for a deletion', () => {
|
||||
expect(ASSISTANT_WRITE_TOOLS.create_journey).toEqual({ verbKey: 'createJournal', entity: 'journey' });
|
||||
expect(ASSISTANT_WRITE_TOOLS.update_journey?.verbKey).toBe('updateJournalSettings');
|
||||
expect(ASSISTANT_WRITE_TOOLS.delete_journey?.verbKey).toBe('deleteJournal');
|
||||
expect(ASSISTANT_WRITE_TOOLS.add_journey_trip?.verbKey).toBe('linkJournalTrip');
|
||||
expect(ASSISTANT_WRITE_TOOLS.remove_journey_trip?.verbKey).toBe('unlinkJournalTrip');
|
||||
expect(ASSISTANT_WRITE_TOOLS.restore_journey_suggestions?.verbKey).toBe('restoreJournalSuggestions');
|
||||
expect(ASSISTANT_WRITE_TOOLS.update_journey_preferences?.verbKey).toBe('setJournalPreferences');
|
||||
for (const tool of ['delete_journey', 'remove_journey_trip', 'restore_journey_suggestions']) {
|
||||
expect(ASSISTANT_UNDO_MAP[tool], tool).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { storedTrekCardSchema, trekCardSchema } from './index';
|
||||
import { JOURNEY_HEADER_LIMITS, journeyHeaderCardSchema } from './journey-header.card';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const card = {
|
||||
kind: 'journeyHeader',
|
||||
journeyId: 12,
|
||||
title: 'Japan 2026',
|
||||
subtitle: null,
|
||||
change: 'changed',
|
||||
trips: [{ id: 5, title: 'Tokyo days', startDate: '2026-11-02', endDate: null, change: 'link' }],
|
||||
entries: 3,
|
||||
photos: null,
|
||||
suggestions: 9,
|
||||
} as const;
|
||||
|
||||
describe('journeyHeaderCardSchema', () => {
|
||||
it('JOURNEYHEADER-001: is part of the card union and reads as itself', () => {
|
||||
expect(trekCardSchema.parse(card)).toEqual(card);
|
||||
expect(storedTrekCardSchema.parse(card)).toEqual(card);
|
||||
});
|
||||
|
||||
it('JOURNEYHEADER-002: takes a new journal without an id, and a trip the user trips do not list', () => {
|
||||
const fresh = { ...card, journeyId: null, change: 'new', trips: [{ ...card.trips[0], id: null }] };
|
||||
expect(journeyHeaderCardSchema.parse(fresh)).toEqual(fresh);
|
||||
});
|
||||
|
||||
it('JOURNEYHEADER-003: refuses an unknown change, too many trips, an empty title or a negative count', () => {
|
||||
expect(journeyHeaderCardSchema.safeParse({ ...card, change: 'renamed' }).success).toBe(false);
|
||||
const many = Array.from({ length: JOURNEY_HEADER_LIMITS.trips + 1 }, () => card.trips[0]);
|
||||
expect(journeyHeaderCardSchema.safeParse({ ...card, trips: many }).success).toBe(false);
|
||||
expect(journeyHeaderCardSchema.safeParse({ ...card, title: '' }).success).toBe(false);
|
||||
expect(journeyHeaderCardSchema.safeParse({ ...card, entries: -1 }).success).toBe(false);
|
||||
expect(journeyHeaderCardSchema.safeParse({ ...card, trips: [{ ...card.trips[0], change: 'moved' }] }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { idSchema } from '../../common/primitives.schema';
|
||||
import { cardCountSchema, cardDateSchema, cardText } from './card-parts';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* A journal itself as the assistant chat shows a change of it: a new one,
|
||||
* its settings changed, a trip linked or unlinked, its suggestions brought
|
||||
* back, or the whole journal deleted. The rows of the change card say what
|
||||
* changes field by field; this card is the journal under them, its trips and
|
||||
* what a change takes with it.
|
||||
*/
|
||||
|
||||
/** How much of a journal the card carries; the server clips to the same numbers. */
|
||||
export const JOURNEY_HEADER_LIMITS = { title: 200, subtitle: 300, trips: 12, tripTitle: 120 } as const;
|
||||
|
||||
/** What the change does to the journal: makes it, changes it, deletes it. */
|
||||
export const JOURNEY_HEADER_CHANGES = ['new', 'changed', 'willDelete'] as const;
|
||||
|
||||
/** What the change does to one of its trips. */
|
||||
export const JOURNEY_HEADER_TRIP_CHANGES = ['kept', 'link', 'unlink'] as const;
|
||||
|
||||
export const journeyHeaderTripSchema = z.object({
|
||||
/** Null for a trip a new journal names that the user's trips do not list. */
|
||||
id: idSchema.nullable(),
|
||||
title: cardText(JOURNEY_HEADER_LIMITS.tripTitle),
|
||||
startDate: cardDateSchema.nullable(),
|
||||
endDate: cardDateSchema.nullable(),
|
||||
change: z.enum(JOURNEY_HEADER_TRIP_CHANGES),
|
||||
});
|
||||
export type JourneyHeaderTrip = z.infer<typeof journeyHeaderTripSchema>;
|
||||
|
||||
export const journeyHeaderCardSchema = z.object({
|
||||
kind: z.literal('journeyHeader'),
|
||||
/** Null for a journal a held change has not made yet. */
|
||||
journeyId: idSchema.nullable(),
|
||||
title: cardText(JOURNEY_HEADER_LIMITS.title),
|
||||
subtitle: cardText(JOURNEY_HEADER_LIMITS.subtitle).nullable(),
|
||||
change: z.enum(JOURNEY_HEADER_CHANGES),
|
||||
trips: z.array(journeyHeaderTripSchema).max(JOURNEY_HEADER_LIMITS.trips),
|
||||
/** What the journal holds: what a deletion takes with it, what an unlinked trip leaves behind. */
|
||||
entries: cardCountSchema.nullable(),
|
||||
photos: cardCountSchema.nullable(),
|
||||
/** Places of the linked trips that come in as suggestions; null when unknown. */
|
||||
suggestions: cardCountSchema.nullable(),
|
||||
});
|
||||
export type JourneyHeaderCard = z.infer<typeof journeyHeaderCardSchema>;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { journeyEntriesCardSchema } from './journey-entries.card';
|
||||
import { journeyHeaderCardSchema } from './journey-header.card';
|
||||
|
||||
export * from './journey-entries.card';
|
||||
export * from './journey-header.card';
|
||||
|
||||
/**
|
||||
* What the travel journal brings to the assistant chat: the kinds of card it draws,
|
||||
@@ -9,8 +11,25 @@ export * from './journey-entries.card';
|
||||
* spread these into the card union and the shared enums, so a new kind, verb
|
||||
* or entity is added here and nowhere else.
|
||||
*/
|
||||
export const JOURNEY_CARD_SCHEMAS = [journeyEntriesCardSchema] as const;
|
||||
/** An entry written, changed or deleted. */
|
||||
export const JOURNEY_VERB_KEYS = ['addJournalEntry', 'updateJournalEntry', 'removeJournalEntry'] as const;
|
||||
/** An entry of a journal; the receipt opens it there. */
|
||||
export const JOURNEY_APPLIED_ENTITIES = ['journey_entry'] as const;
|
||||
export const JOURNEY_CARD_SCHEMAS = [journeyEntriesCardSchema, journeyHeaderCardSchema] as const;
|
||||
/**
|
||||
* An entry written, changed or deleted; a journal started, renamed, given
|
||||
* another status or other settings, deleted; a trip linked or unlinked; its
|
||||
* suggestions brought back; the user's own view of it changed.
|
||||
*/
|
||||
export const JOURNEY_VERB_KEYS = [
|
||||
'addJournalEntry',
|
||||
'updateJournalEntry',
|
||||
'removeJournalEntry',
|
||||
'createJournal',
|
||||
'renameJournal',
|
||||
'setJournalStatus',
|
||||
'updateJournalSettings',
|
||||
'deleteJournal',
|
||||
'linkJournalTrip',
|
||||
'unlinkJournalTrip',
|
||||
'restoreJournalSuggestions',
|
||||
'setJournalPreferences',
|
||||
] as const;
|
||||
/** An entry of a journal, or the journal itself; the receipt opens it there. */
|
||||
export const JOURNEY_APPLIED_ENTITIES = ['journey_entry', 'journey'] as const;
|
||||
|
||||
@@ -41,5 +41,41 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'لخّص هذه الأيام',
|
||||
'assistant.journal.welcome': 'ماذا نكتب في اليوميات؟',
|
||||
'assistant.journal.readOnly': 'يمكنك قراءة هذه اليوميات فقط، لذا أستطيع الإجابة عن الأسئلة دون تغيير أي شيء.',
|
||||
'assistant.change.verb.createJournal': 'إنشاء يوميات',
|
||||
'assistant.change.verb.renameJournal': 'إعادة تسمية اليوميات',
|
||||
'assistant.change.verb.setJournalStatus': 'تغيير حالة اليوميات',
|
||||
'assistant.change.verb.updateJournalSettings': 'تغيير إعدادات اليوميات',
|
||||
'assistant.change.verb.deleteJournal': 'حذف اليوميات',
|
||||
'assistant.change.verb.linkJournalTrip': 'إضافة رحلة إلى اليوميات',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'إزالة رحلة من اليوميات',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'استعادة الاقتراحات',
|
||||
'assistant.change.verb.setJournalPreferences': 'تغيير طريقة عرضك لليوميات',
|
||||
'assistant.card.journeyHeader.open': 'فتح اليوميات',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'يظهر كـ',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'ألوان الغلاف',
|
||||
'assistant.card.journeyHeader.field.trip': 'الرحلة',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'اقتراحات مستعادة',
|
||||
'assistant.card.journeyHeader.tripLink': 'ستُضاف',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'ستُزال',
|
||||
'assistant.card.journeyHeader.deletes': 'تُحذف اليوميات مع كل مدخلاتها وصورها. لا يمكن التراجع عن ذلك.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'يبقى {count} مدخل مكتوب',
|
||||
'assistant.card.journeyHeader.entriesStay.zero': 'لا تبقى مدخلات مكتوبة {count}',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'يبقى {count} مدخل مكتوب',
|
||||
'assistant.card.journeyHeader.entriesStay.two': 'يبقى {count} مدخلان مكتوبان',
|
||||
'assistant.card.journeyHeader.entriesStay.few': 'تبقى {count} مدخلات مكتوبة',
|
||||
'assistant.card.journeyHeader.entriesStay.many': 'يبقى {count} مدخلًا مكتوبًا',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'يُزال {count} اقتراح مفتوح',
|
||||
'assistant.card.journeyHeader.suggestionsGo.zero': 'لا تُزال اقتراحات مفتوحة {count}',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'يُزال {count} اقتراح مفتوح',
|
||||
'assistant.card.journeyHeader.suggestionsGo.two': 'يُزال {count} اقتراحان مفتوحان',
|
||||
'assistant.card.journeyHeader.suggestionsGo.few': 'تُزال {count} اقتراحات مفتوحة',
|
||||
'assistant.card.journeyHeader.suggestionsGo.many': 'يُزال {count} اقتراحًا مفتوحًا',
|
||||
'assistant.card.journeyHeader.suggestionsCome': 'يُضاف {count} مكان كاقتراح',
|
||||
'assistant.card.journeyHeader.suggestionsCome.zero': 'لا تُضاف أماكن كاقتراحات {count}',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': 'يُضاف {count} مكان كاقتراح',
|
||||
'assistant.card.journeyHeader.suggestionsCome.two': 'يُضاف {count} مكانان كاقتراحين',
|
||||
'assistant.card.journeyHeader.suggestionsCome.few': 'تُضاف {count} أماكن كاقتراحات',
|
||||
'assistant.card.journeyHeader.suggestionsCome.many': 'يُضاف {count} مكانًا كاقتراحات',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,30 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Gündəliyə nə yazaq?',
|
||||
'assistant.journal.readOnly':
|
||||
'Bu gündəliyi yalnız oxuya bilərsən, ona görə suallara cavab verə bilərəm, amma heç nəyi dəyişə bilmərəm.',
|
||||
'assistant.change.verb.createJournal': 'Gündəlik başlat',
|
||||
'assistant.change.verb.renameJournal': 'Gündəliyin adını dəyiş',
|
||||
'assistant.change.verb.setJournalStatus': 'Gündəliyin vəziyyətini dəyiş',
|
||||
'assistant.change.verb.updateJournalSettings': 'Gündəliyin ayarlarını dəyiş',
|
||||
'assistant.change.verb.deleteJournal': 'Gündəliyi sil',
|
||||
'assistant.change.verb.linkJournalTrip': 'Gündəliyə səyahət əlavə et',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Səyahəti gündəlikdən çıxar',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Təklifləri geri qaytar',
|
||||
'assistant.change.verb.setJournalPreferences': 'Gündəliyin sənin üçün görünüşünü dəyiş',
|
||||
'assistant.card.journeyHeader.open': 'Gündəliyi aç',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Göstərilir',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Üz qabığının rəngləri',
|
||||
'assistant.card.journeyHeader.field.trip': 'Səyahət',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Geri qaytarılan təkliflər',
|
||||
'assistant.card.journeyHeader.tripLink': 'Əlavə olunacaq',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Çıxarılacaq',
|
||||
'assistant.card.journeyHeader.deletes':
|
||||
'Gündəlik bütün qeydləri və şəkilləri ilə birlikdə silinir. Bunu geri qaytarmaq olmur.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} yazılmış qeyd qalır',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} yazılmış qeyd qalır',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} açıq təklif silinir',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} açıq təklif silinir',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} yer təklif kimi əlavə olunur',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} yer təklif kimi əlavə olunur',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -29,5 +29,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Resumir estes dias',
|
||||
'assistant.journal.welcome': 'O que vamos colocar no diário?',
|
||||
'assistant.journal.readOnly': 'Você só pode ler este diário, então posso responder perguntas, mas não mudar nada.',
|
||||
'assistant.change.verb.createJournal': 'Começar um diário',
|
||||
'assistant.change.verb.renameJournal': 'Renomear o diário',
|
||||
'assistant.change.verb.setJournalStatus': 'Mudar o status do diário',
|
||||
'assistant.change.verb.updateJournalSettings': 'Mudar as configurações do diário',
|
||||
'assistant.change.verb.deleteJournal': 'Excluir o diário',
|
||||
'assistant.change.verb.linkJournalTrip': 'Adicionar uma viagem ao diário',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Tirar uma viagem do diário',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Trazer de volta as sugestões',
|
||||
'assistant.change.verb.setJournalPreferences': 'Mudar a sua visualização do diário',
|
||||
'assistant.card.journeyHeader.open': 'Abrir o diário',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Exibido como',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Cores da capa',
|
||||
'assistant.card.journeyHeader.field.trip': 'Viagem',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Sugestões de volta',
|
||||
'assistant.card.journeyHeader.tripLink': 'Será adicionada',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Será retirada',
|
||||
'assistant.card.journeyHeader.deletes': 'O diário sai com todas as entradas e fotos. Isso não pode ser desfeito.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} entradas escritas ficam',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} entrada escrita fica',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} sugestões em aberto saem',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} sugestão em aberto sai',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} lugares entram como sugestões',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} lugar entra como sugestão',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -29,5 +29,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Resumeix aquests dies',
|
||||
'assistant.journal.welcome': 'Què hi posem, al diari?',
|
||||
'assistant.journal.readOnly': 'Només pots llegir aquest diari, així que puc respondre preguntes però no canviar res.',
|
||||
'assistant.change.verb.createJournal': 'Començar un diari',
|
||||
'assistant.change.verb.renameJournal': 'Canviar el nom del diari',
|
||||
'assistant.change.verb.setJournalStatus': "Canviar l'estat del diari",
|
||||
'assistant.change.verb.updateJournalSettings': 'Canviar la configuració del diari',
|
||||
'assistant.change.verb.deleteJournal': 'Eliminar el diari',
|
||||
'assistant.change.verb.linkJournalTrip': 'Afegir un viatge al diari',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Treure un viatge del diari',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Recuperar els suggeriments',
|
||||
'assistant.change.verb.setJournalPreferences': 'Canviar la teva vista del diari',
|
||||
'assistant.card.journeyHeader.open': 'Obrir el diari',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Es mostra com a',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Colors de la portada',
|
||||
'assistant.card.journeyHeader.field.trip': 'Viatge',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Suggeriments recuperats',
|
||||
'assistant.card.journeyHeader.tripLink': "S'hi afegirà",
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Es traurà',
|
||||
'assistant.card.journeyHeader.deletes': "El diari se'n va amb totes les entrades i fotos. No es pot desfer.",
|
||||
'assistant.card.journeyHeader.entriesStay': 'Es queden {count} entrades escrites',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Es queda {count} entrada escrita',
|
||||
'assistant.card.journeyHeader.suggestionsGo': "Se'n van {count} suggeriments oberts",
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': "Se'n va {count} suggeriment obert",
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} llocs entren com a suggeriments',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} lloc entra com a suggeriment',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -32,5 +32,32 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Shrnout tyto dny',
|
||||
'assistant.journal.welcome': 'Co zapíšeme do deníku?',
|
||||
'assistant.journal.readOnly': 'Tento deník můžeš jen číst, takže můžu odpovídat na otázky, ale nic měnit.',
|
||||
'assistant.change.verb.createJournal': 'Založit deník',
|
||||
'assistant.change.verb.renameJournal': 'Přejmenovat deník',
|
||||
'assistant.change.verb.setJournalStatus': 'Změnit stav deníku',
|
||||
'assistant.change.verb.updateJournalSettings': 'Změnit nastavení deníku',
|
||||
'assistant.change.verb.deleteJournal': 'Smazat deník',
|
||||
'assistant.change.verb.linkJournalTrip': 'Přidat cestu do deníku',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Odebrat cestu z deníku',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Vrátit návrhy',
|
||||
'assistant.change.verb.setJournalPreferences': 'Změnit své zobrazení deníku',
|
||||
'assistant.card.journeyHeader.open': 'Otevřít deník',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Zobrazeno jako',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Barvy obálky',
|
||||
'assistant.card.journeyHeader.field.trip': 'Cesta',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Vrácené návrhy',
|
||||
'assistant.card.journeyHeader.tripLink': 'Bude přidána',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Bude odebrána',
|
||||
'assistant.card.journeyHeader.deletes': 'Deník zmizí se všemi záznamy a fotkami. Nelze to vrátit zpět.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} napsaných záznamů zůstane',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} napsaný záznam zůstane',
|
||||
'assistant.card.journeyHeader.entriesStay.few': '{count} napsané záznamy zůstanou',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} otevřených návrhů zmizí',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} otevřený návrh zmizí',
|
||||
'assistant.card.journeyHeader.suggestionsGo.few': '{count} otevřené návrhy zmizí',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} míst přibude jako návrhy',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} místo přibude jako návrh',
|
||||
'assistant.card.journeyHeader.suggestionsCome.few': '{count} místa přibudou jako návrhy',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,30 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Was soll ins Journal?',
|
||||
'assistant.journal.readOnly':
|
||||
'Du kannst dieses Journal nur lesen, darum kann ich Fragen beantworten, aber nichts ändern.',
|
||||
'assistant.change.verb.createJournal': 'Journal anlegen',
|
||||
'assistant.change.verb.renameJournal': 'Journal umbenennen',
|
||||
'assistant.change.verb.setJournalStatus': 'Stand des Journals ändern',
|
||||
'assistant.change.verb.updateJournalSettings': 'Einstellungen des Journals ändern',
|
||||
'assistant.change.verb.deleteJournal': 'Journal löschen',
|
||||
'assistant.change.verb.linkJournalTrip': 'Reise zum Journal hinzufügen',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Reise aus dem Journal nehmen',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Vorschläge zurückholen',
|
||||
'assistant.change.verb.setJournalPreferences': 'Deine Ansicht des Journals ändern',
|
||||
'assistant.card.journeyHeader.open': 'Journal öffnen',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Angezeigt als',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Farben des Covers',
|
||||
'assistant.card.journeyHeader.field.trip': 'Reise',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Zurückgeholte Vorschläge',
|
||||
'assistant.card.journeyHeader.tripLink': 'Wird hinzugefügt',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Wird herausgenommen',
|
||||
'assistant.card.journeyHeader.deletes':
|
||||
'Das Journal verschwindet mit allen Einträgen und Fotos. Das lässt sich nicht rückgängig machen.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} geschriebene Einträge bleiben',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} geschriebener Eintrag bleibt',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} offene Vorschläge fallen weg',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} offener Vorschlag fällt weg',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} Orte kommen als Vorschläge dazu',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} Ort kommt als Vorschlag dazu',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -29,5 +29,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Sum up these days',
|
||||
'assistant.journal.welcome': 'What should go into the journal?',
|
||||
'assistant.journal.readOnly': 'You can only read this journal, so I can answer questions but not change anything.',
|
||||
'assistant.change.verb.createJournal': 'Start a journal',
|
||||
'assistant.change.verb.renameJournal': 'Rename the journal',
|
||||
'assistant.change.verb.setJournalStatus': "Change the journal's status",
|
||||
'assistant.change.verb.updateJournalSettings': "Change the journal's settings",
|
||||
'assistant.change.verb.deleteJournal': 'Delete the journal',
|
||||
'assistant.change.verb.linkJournalTrip': 'Add a trip to the journal',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Take a trip out of the journal',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Bring back the suggestions',
|
||||
'assistant.change.verb.setJournalPreferences': 'Change your view of the journal',
|
||||
'assistant.card.journeyHeader.open': 'Open the journal',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Shown as',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Cover colours',
|
||||
'assistant.card.journeyHeader.field.trip': 'Trip',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Suggestions back',
|
||||
'assistant.card.journeyHeader.tripLink': 'Will be added',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Will be taken out',
|
||||
'assistant.card.journeyHeader.deletes': 'The journal goes with all its entries and photos. This cannot be undone.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} written entries stay',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} written entry stays',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} open suggestions go',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} open suggestion goes',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} places come in as suggestions',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} place comes in as a suggestion',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': '¿Qué ponemos en el diario?',
|
||||
'assistant.journal.readOnly':
|
||||
'Solo puedes leer este diario, así que puedo responder preguntas, pero no cambiar nada.',
|
||||
'assistant.change.verb.createJournal': 'Empezar un diario',
|
||||
'assistant.change.verb.renameJournal': 'Cambiar el nombre del diario',
|
||||
'assistant.change.verb.setJournalStatus': 'Cambiar el estado del diario',
|
||||
'assistant.change.verb.updateJournalSettings': 'Cambiar los ajustes del diario',
|
||||
'assistant.change.verb.deleteJournal': 'Eliminar el diario',
|
||||
'assistant.change.verb.linkJournalTrip': 'Añadir un viaje al diario',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Quitar un viaje del diario',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Recuperar las sugerencias',
|
||||
'assistant.change.verb.setJournalPreferences': 'Cambiar tu vista del diario',
|
||||
'assistant.card.journeyHeader.open': 'Abrir el diario',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Se muestra como',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Colores de la portada',
|
||||
'assistant.card.journeyHeader.field.trip': 'Viaje',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Sugerencias recuperadas',
|
||||
'assistant.card.journeyHeader.tripLink': 'Se añadirá',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Se quitará',
|
||||
'assistant.card.journeyHeader.deletes': 'El diario se va con todas sus entradas y fotos. No se puede deshacer.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'Se quedan {count} entradas escritas',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Se queda {count} entrada escrita',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'Se van {count} sugerencias abiertas',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'Se va {count} sugerencia abierta',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} lugares entran como sugerencias',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} lugar entra como sugerencia',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Mida päevikusse kirjutame?',
|
||||
'assistant.journal.readOnly':
|
||||
'Sa saad seda päevikut ainult lugeda, nii et vastan küsimustele, aga midagi muuta ei saa.',
|
||||
'assistant.change.verb.createJournal': 'Alusta päevikut',
|
||||
'assistant.change.verb.renameJournal': 'Nimeta päevik ümber',
|
||||
'assistant.change.verb.setJournalStatus': 'Muuda päeviku olekut',
|
||||
'assistant.change.verb.updateJournalSettings': 'Muuda päeviku seadeid',
|
||||
'assistant.change.verb.deleteJournal': 'Kustuta päevik',
|
||||
'assistant.change.verb.linkJournalTrip': 'Lisa reis päevikusse',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Eemalda reis päevikust',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Too soovitused tagasi',
|
||||
'assistant.change.verb.setJournalPreferences': 'Muuda oma päevikuvaadet',
|
||||
'assistant.card.journeyHeader.open': 'Ava päevik',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Kuvatakse kui',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Kaane värvid',
|
||||
'assistant.card.journeyHeader.field.trip': 'Reis',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Tagasi toodud soovitused',
|
||||
'assistant.card.journeyHeader.tripLink': 'Lisatakse',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Eemaldatakse',
|
||||
'assistant.card.journeyHeader.deletes': 'Päevik kaob koos kõigi sissekannete ja fotodega. Seda ei saa tagasi võtta.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} kirjutatud sissekannet jääb alles',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} kirjutatud sissekanne jääb alles',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} avatud soovitust kaob',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} avatud soovitus kaob',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} kohta tuleb soovitustena juurde',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} koht tuleb soovitusena juurde',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,30 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': "Qu'écrivons-nous dans le journal ?",
|
||||
'assistant.journal.readOnly':
|
||||
'Vous pouvez seulement lire ce journal : je peux répondre aux questions, sans rien modifier.',
|
||||
'assistant.change.verb.createJournal': 'Commencer un journal',
|
||||
'assistant.change.verb.renameJournal': 'Renommer le journal',
|
||||
'assistant.change.verb.setJournalStatus': 'Changer le statut du journal',
|
||||
'assistant.change.verb.updateJournalSettings': 'Modifier les réglages du journal',
|
||||
'assistant.change.verb.deleteJournal': 'Supprimer le journal',
|
||||
'assistant.change.verb.linkJournalTrip': 'Ajouter un voyage au journal',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Retirer un voyage du journal',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Faire revenir les suggestions',
|
||||
'assistant.change.verb.setJournalPreferences': 'Changer votre affichage du journal',
|
||||
'assistant.card.journeyHeader.open': 'Ouvrir le journal',
|
||||
'assistant.card.journeyHeader.setting': '{name} : {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Affiché comme',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Couleurs de la couverture',
|
||||
'assistant.card.journeyHeader.field.trip': 'Voyage',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Suggestions revenues',
|
||||
'assistant.card.journeyHeader.tripLink': 'Sera ajouté',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Sera retiré',
|
||||
'assistant.card.journeyHeader.deletes':
|
||||
"Le journal disparaît avec toutes ses entrées et ses photos. Impossible d'annuler.",
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} entrées écrites restent',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} entrée écrite reste',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} suggestions ouvertes disparaissent',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} suggestion ouverte disparaît',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} lieux arrivent comme suggestions',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} lieu arrive comme suggestion',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,30 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Τι να γράψουμε στο ημερολόγιο;',
|
||||
'assistant.journal.readOnly':
|
||||
'Μπορείς μόνο να διαβάσεις αυτό το ημερολόγιο, οπότε απαντώ σε ερωτήσεις αλλά δεν αλλάζω τίποτα.',
|
||||
'assistant.change.verb.createJournal': 'Δημιουργία ημερολογίου',
|
||||
'assistant.change.verb.renameJournal': 'Μετονομασία ημερολογίου',
|
||||
'assistant.change.verb.setJournalStatus': 'Αλλαγή κατάστασης ημερολογίου',
|
||||
'assistant.change.verb.updateJournalSettings': 'Αλλαγή ρυθμίσεων ημερολογίου',
|
||||
'assistant.change.verb.deleteJournal': 'Διαγραφή ημερολογίου',
|
||||
'assistant.change.verb.linkJournalTrip': 'Προσθήκη ταξιδιού στο ημερολόγιο',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Αφαίρεση ταξιδιού από το ημερολόγιο',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Επαναφορά προτάσεων',
|
||||
'assistant.change.verb.setJournalPreferences': 'Αλλαγή της δικής σου προβολής του ημερολογίου',
|
||||
'assistant.card.journeyHeader.open': 'Άνοιγμα ημερολογίου',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Εμφανίζεται ως',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Χρώματα εξωφύλλου',
|
||||
'assistant.card.journeyHeader.field.trip': 'Ταξίδι',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Προτάσεις που επανέρχονται',
|
||||
'assistant.card.journeyHeader.tripLink': 'Θα προστεθεί',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Θα αφαιρεθεί',
|
||||
'assistant.card.journeyHeader.deletes':
|
||||
'Το ημερολόγιο διαγράφεται με όλες τις καταχωρίσεις και τις φωτογραφίες του. Δεν αναιρείται.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'Μένουν {count} γραμμένες καταχωρίσεις',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Μένει {count} γραμμένη καταχώριση',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'Φεύγουν {count} ανοιχτές προτάσεις',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'Φεύγει {count} ανοιχτή πρόταση',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} μέρη μπαίνουν ως προτάσεις',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} μέρος μπαίνει ως πρόταση',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,30 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Mi kerüljön a naplóba?',
|
||||
'assistant.journal.readOnly':
|
||||
'Ezt a naplót csak olvasni tudod, így kérdésekre válaszolhatok, de semmit sem módosíthatok.',
|
||||
'assistant.change.verb.createJournal': 'Napló indítása',
|
||||
'assistant.change.verb.renameJournal': 'Napló átnevezése',
|
||||
'assistant.change.verb.setJournalStatus': 'Napló állapotának módosítása',
|
||||
'assistant.change.verb.updateJournalSettings': 'Napló beállításainak módosítása',
|
||||
'assistant.change.verb.deleteJournal': 'Napló törlése',
|
||||
'assistant.change.verb.linkJournalTrip': 'Utazás hozzáadása a naplóhoz',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Utazás eltávolítása a naplóból',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Javaslatok visszahozása',
|
||||
'assistant.change.verb.setJournalPreferences': 'A napló saját nézetének módosítása',
|
||||
'assistant.card.journeyHeader.open': 'Napló megnyitása',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Így látszik',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Borító színei',
|
||||
'assistant.card.journeyHeader.field.trip': 'Utazás',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Visszahozott javaslatok',
|
||||
'assistant.card.journeyHeader.tripLink': 'Hozzá lesz adva',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'El lesz távolítva',
|
||||
'assistant.card.journeyHeader.deletes':
|
||||
'A napló az összes bejegyzésével és fotójával együtt törlődik. Ez nem vonható vissza.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} megírt bejegyzés marad',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} megírt bejegyzés marad',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} nyitott javaslat eltűnik',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} nyitott javaslat eltűnik',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} hely javaslatként érkezik',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} hely javaslatként érkezik',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -27,5 +27,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Apa yang mau ditulis di jurnal?',
|
||||
'assistant.journal.readOnly':
|
||||
'Kamu hanya bisa membaca jurnal ini, jadi aku bisa menjawab pertanyaan tetapi tidak bisa mengubah apa pun.',
|
||||
'assistant.change.verb.createJournal': 'Mulai jurnal',
|
||||
'assistant.change.verb.renameJournal': 'Ganti nama jurnal',
|
||||
'assistant.change.verb.setJournalStatus': 'Ubah status jurnal',
|
||||
'assistant.change.verb.updateJournalSettings': 'Ubah pengaturan jurnal',
|
||||
'assistant.change.verb.deleteJournal': 'Hapus jurnal',
|
||||
'assistant.change.verb.linkJournalTrip': 'Tambahkan perjalanan ke jurnal',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Keluarkan perjalanan dari jurnal',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Kembalikan saran',
|
||||
'assistant.change.verb.setJournalPreferences': 'Ubah tampilan jurnal untukmu',
|
||||
'assistant.card.journeyHeader.open': 'Buka jurnal',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Ditampilkan sebagai',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Warna sampul',
|
||||
'assistant.card.journeyHeader.field.trip': 'Perjalanan',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Saran yang kembali',
|
||||
'assistant.card.journeyHeader.tripLink': 'Akan ditambahkan',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Akan dikeluarkan',
|
||||
'assistant.card.journeyHeader.deletes': 'Jurnal terhapus beserta semua entri dan fotonya. Ini tidak bisa dibatalkan.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} entri tertulis tetap ada',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} saran terbuka hilang',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} tempat masuk sebagai saran',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Cosa scriviamo nel diario?',
|
||||
'assistant.journal.readOnly':
|
||||
'Puoi solo leggere questo diario, quindi posso rispondere alle domande ma non cambiare nulla.',
|
||||
'assistant.change.verb.createJournal': 'Inizia un diario',
|
||||
'assistant.change.verb.renameJournal': 'Rinomina il diario',
|
||||
'assistant.change.verb.setJournalStatus': 'Cambia lo stato del diario',
|
||||
'assistant.change.verb.updateJournalSettings': 'Cambia le impostazioni del diario',
|
||||
'assistant.change.verb.deleteJournal': 'Elimina il diario',
|
||||
'assistant.change.verb.linkJournalTrip': 'Aggiungi un viaggio al diario',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Togli un viaggio dal diario',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Recupera i suggerimenti',
|
||||
'assistant.change.verb.setJournalPreferences': 'Cambia la tua vista del diario',
|
||||
'assistant.card.journeyHeader.open': 'Apri il diario',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Mostrato come',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Colori della copertina',
|
||||
'assistant.card.journeyHeader.field.trip': 'Viaggio',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Suggerimenti recuperati',
|
||||
'assistant.card.journeyHeader.tripLink': 'Verrà aggiunto',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Verrà tolto',
|
||||
'assistant.card.journeyHeader.deletes': 'Il diario se ne va con tutte le voci e le foto. Non si può annullare.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'Restano {count} voci scritte',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Resta {count} voce scritta',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'Spariscono {count} suggerimenti aperti',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'Sparisce {count} suggerimento aperto',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} luoghi arrivano come suggerimenti',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} luogo arriva come suggerimento',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -26,5 +26,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'この数日をまとめる',
|
||||
'assistant.journal.welcome': 'ジャーナルに何を書きましょうか?',
|
||||
'assistant.journal.readOnly': 'このジャーナルは閲覧のみなので、質問には答えられますが変更はできません。',
|
||||
'assistant.change.verb.createJournal': 'ジャーナルを始める',
|
||||
'assistant.change.verb.renameJournal': 'ジャーナルの名前を変更',
|
||||
'assistant.change.verb.setJournalStatus': 'ジャーナルの状態を変更',
|
||||
'assistant.change.verb.updateJournalSettings': 'ジャーナルの設定を変更',
|
||||
'assistant.change.verb.deleteJournal': 'ジャーナルを削除',
|
||||
'assistant.change.verb.linkJournalTrip': 'ジャーナルに旅を追加',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'ジャーナルから旅を外す',
|
||||
'assistant.change.verb.restoreJournalSuggestions': '提案を元に戻す',
|
||||
'assistant.change.verb.setJournalPreferences': '自分のジャーナル表示を変更',
|
||||
'assistant.card.journeyHeader.open': 'ジャーナルを開く',
|
||||
'assistant.card.journeyHeader.setting': '{name}:{value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': '表示',
|
||||
'assistant.card.journeyHeader.field.coverGradient': '表紙の色',
|
||||
'assistant.card.journeyHeader.field.trip': '旅',
|
||||
'assistant.card.journeyHeader.field.suggestions': '戻る提案',
|
||||
'assistant.card.journeyHeader.tripLink': '追加されます',
|
||||
'assistant.card.journeyHeader.tripUnlink': '外されます',
|
||||
'assistant.card.journeyHeader.deletes': 'ジャーナルはすべてのエントリーと写真ごと削除されます。元に戻せません。',
|
||||
'assistant.card.journeyHeader.entriesStay': '書かれたエントリー{count}件は残ります',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '未記入の提案{count}件はなくなります',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count}か所が提案として加わります',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -26,5 +26,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': '이 며칠 요약하기',
|
||||
'assistant.journal.welcome': '저널에 무엇을 적을까요?',
|
||||
'assistant.journal.readOnly': '이 저널은 읽기만 할 수 있어서 질문에는 답할 수 있지만 아무것도 바꿀 수 없어요.',
|
||||
'assistant.change.verb.createJournal': '저널 시작하기',
|
||||
'assistant.change.verb.renameJournal': '저널 이름 바꾸기',
|
||||
'assistant.change.verb.setJournalStatus': '저널 상태 바꾸기',
|
||||
'assistant.change.verb.updateJournalSettings': '저널 설정 바꾸기',
|
||||
'assistant.change.verb.deleteJournal': '저널 삭제',
|
||||
'assistant.change.verb.linkJournalTrip': '저널에 여행 추가',
|
||||
'assistant.change.verb.unlinkJournalTrip': '저널에서 여행 빼기',
|
||||
'assistant.change.verb.restoreJournalSuggestions': '제안 되돌리기',
|
||||
'assistant.change.verb.setJournalPreferences': '내 저널 보기 바꾸기',
|
||||
'assistant.card.journeyHeader.open': '저널 열기',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': '표시',
|
||||
'assistant.card.journeyHeader.field.coverGradient': '표지 색상',
|
||||
'assistant.card.journeyHeader.field.trip': '여행',
|
||||
'assistant.card.journeyHeader.field.suggestions': '되돌아오는 제안',
|
||||
'assistant.card.journeyHeader.tripLink': '추가됩니다',
|
||||
'assistant.card.journeyHeader.tripUnlink': '빠집니다',
|
||||
'assistant.card.journeyHeader.deletes': '저널이 모든 항목과 사진과 함께 삭제됩니다. 되돌릴 수 없어요.',
|
||||
'assistant.card.journeyHeader.entriesStay': '작성된 항목 {count}개는 남아요',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '열린 제안 {count}개는 사라져요',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '장소 {count}곳이 제안으로 들어와요',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -29,5 +29,30 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Vat deze dagen samen',
|
||||
'assistant.journal.welcome': 'Wat komt er in het dagboek?',
|
||||
'assistant.journal.readOnly': 'Je kunt dit dagboek alleen lezen, dus ik kan vragen beantwoorden maar niets wijzigen.',
|
||||
'assistant.change.verb.createJournal': 'Dagboek beginnen',
|
||||
'assistant.change.verb.renameJournal': 'Dagboek hernoemen',
|
||||
'assistant.change.verb.setJournalStatus': 'Status van het dagboek wijzigen',
|
||||
'assistant.change.verb.updateJournalSettings': 'Instellingen van het dagboek wijzigen',
|
||||
'assistant.change.verb.deleteJournal': 'Dagboek verwijderen',
|
||||
'assistant.change.verb.linkJournalTrip': 'Reis aan het dagboek toevoegen',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Reis uit het dagboek halen',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Suggesties terughalen',
|
||||
'assistant.change.verb.setJournalPreferences': 'Je weergave van het dagboek wijzigen',
|
||||
'assistant.card.journeyHeader.open': 'Dagboek openen',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Getoond als',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Kleuren van de omslag',
|
||||
'assistant.card.journeyHeader.field.trip': 'Reis',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Teruggehaalde suggesties',
|
||||
'assistant.card.journeyHeader.tripLink': 'Wordt toegevoegd',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Wordt eruit gehaald',
|
||||
'assistant.card.journeyHeader.deletes':
|
||||
"Het dagboek verdwijnt met al zijn berichten en foto's. Dit kan niet ongedaan worden gemaakt.",
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} geschreven berichten blijven',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} geschreven bericht blijft',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} open suggesties verdwijnen',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} open suggestie verdwijnt',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} plekken komen erbij als suggestie',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} plek komt erbij als suggestie',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -36,5 +36,35 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Co wpiszemy do dziennika?',
|
||||
'assistant.journal.readOnly':
|
||||
'Możesz tylko czytać ten dziennik, więc mogę odpowiadać na pytania, ale niczego nie zmienię.',
|
||||
'assistant.change.verb.createJournal': 'Załóż dziennik',
|
||||
'assistant.change.verb.renameJournal': 'Zmień nazwę dziennika',
|
||||
'assistant.change.verb.setJournalStatus': 'Zmień stan dziennika',
|
||||
'assistant.change.verb.updateJournalSettings': 'Zmień ustawienia dziennika',
|
||||
'assistant.change.verb.deleteJournal': 'Usuń dziennik',
|
||||
'assistant.change.verb.linkJournalTrip': 'Dodaj podróż do dziennika',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Usuń podróż z dziennika',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Przywróć sugestie',
|
||||
'assistant.change.verb.setJournalPreferences': 'Zmień swój widok dziennika',
|
||||
'assistant.card.journeyHeader.open': 'Otwórz dziennik',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Wyświetlany jako',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Kolory okładki',
|
||||
'assistant.card.journeyHeader.field.trip': 'Podróż',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Przywrócone sugestie',
|
||||
'assistant.card.journeyHeader.tripLink': 'Zostanie dodana',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Zostanie usunięta',
|
||||
'assistant.card.journeyHeader.deletes': 'Dziennik zniknie ze wszystkimi wpisami i zdjęciami. Tego nie da się cofnąć.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'Zostaje {count} napisanych wpisów',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Zostaje {count} napisany wpis',
|
||||
'assistant.card.journeyHeader.entriesStay.few': 'Zostają {count} napisane wpisy',
|
||||
'assistant.card.journeyHeader.entriesStay.many': 'Zostaje {count} napisanych wpisów',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'Znika {count} otwartych sugestii',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'Znika {count} otwarta sugestia',
|
||||
'assistant.card.journeyHeader.suggestionsGo.few': 'Znikają {count} otwarte sugestie',
|
||||
'assistant.card.journeyHeader.suggestionsGo.many': 'Znika {count} otwartych sugestii',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} miejsc dojdzie jako sugestie',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} miejsce dojdzie jako sugestia',
|
||||
'assistant.card.journeyHeader.suggestionsCome.few': '{count} miejsca dojdą jako sugestie',
|
||||
'assistant.card.journeyHeader.suggestionsCome.many': '{count} miejsc dojdzie jako sugestie',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -36,5 +36,35 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Что запишем в дневник?',
|
||||
'assistant.journal.readOnly':
|
||||
'Этот дневник тебе доступен только для чтения, поэтому я могу отвечать на вопросы, но ничего не изменю.',
|
||||
'assistant.change.verb.createJournal': 'Начать дневник',
|
||||
'assistant.change.verb.renameJournal': 'Переименовать дневник',
|
||||
'assistant.change.verb.setJournalStatus': 'Изменить статус дневника',
|
||||
'assistant.change.verb.updateJournalSettings': 'Изменить настройки дневника',
|
||||
'assistant.change.verb.deleteJournal': 'Удалить дневник',
|
||||
'assistant.change.verb.linkJournalTrip': 'Добавить поездку в дневник',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Убрать поездку из дневника',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Вернуть предложения',
|
||||
'assistant.change.verb.setJournalPreferences': 'Изменить свой вид дневника',
|
||||
'assistant.card.journeyHeader.open': 'Открыть дневник',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Показывается как',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Цвета обложки',
|
||||
'assistant.card.journeyHeader.field.trip': 'Поездка',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Возвращённые предложения',
|
||||
'assistant.card.journeyHeader.tripLink': 'Будет добавлена',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Будет убрана',
|
||||
'assistant.card.journeyHeader.deletes': 'Дневник удалится со всеми записями и фото. Отменить это нельзя.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'Останутся {count} написанных записей',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Останется {count} написанная запись',
|
||||
'assistant.card.journeyHeader.entriesStay.few': 'Останутся {count} написанные записи',
|
||||
'assistant.card.journeyHeader.entriesStay.many': 'Останутся {count} написанных записей',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'Исчезнут {count} открытых предложений',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'Исчезнет {count} открытое предложение',
|
||||
'assistant.card.journeyHeader.suggestionsGo.few': 'Исчезнут {count} открытых предложения',
|
||||
'assistant.card.journeyHeader.suggestionsGo.many': 'Исчезнут {count} открытых предложений',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} мест появятся как предложения',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} место появится как предложение',
|
||||
'assistant.card.journeyHeader.suggestionsCome.few': '{count} места появятся как предложения',
|
||||
'assistant.card.journeyHeader.suggestionsCome.many': '{count} мест появятся как предложения',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -32,5 +32,32 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Zhrnúť tieto dni',
|
||||
'assistant.journal.welcome': 'Čo zapíšeme do denníka?',
|
||||
'assistant.journal.readOnly': 'Tento denník môžeš iba čítať, takže môžem odpovedať na otázky, ale nič meniť.',
|
||||
'assistant.change.verb.createJournal': 'Založiť denník',
|
||||
'assistant.change.verb.renameJournal': 'Premenovať denník',
|
||||
'assistant.change.verb.setJournalStatus': 'Zmeniť stav denníka',
|
||||
'assistant.change.verb.updateJournalSettings': 'Zmeniť nastavenia denníka',
|
||||
'assistant.change.verb.deleteJournal': 'Zmazať denník',
|
||||
'assistant.change.verb.linkJournalTrip': 'Pridať cestu do denníka',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Odobrať cestu z denníka',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Vrátiť návrhy',
|
||||
'assistant.change.verb.setJournalPreferences': 'Zmeniť svoje zobrazenie denníka',
|
||||
'assistant.card.journeyHeader.open': 'Otvoriť denník',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Zobrazené ako',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Farby obálky',
|
||||
'assistant.card.journeyHeader.field.trip': 'Cesta',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Vrátené návrhy',
|
||||
'assistant.card.journeyHeader.tripLink': 'Bude pridaná',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Bude odobraná',
|
||||
'assistant.card.journeyHeader.deletes': 'Denník zmizne so všetkými záznamami a fotkami. Nedá sa to vrátiť.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} napísaných záznamov zostane',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} napísaný záznam zostane',
|
||||
'assistant.card.journeyHeader.entriesStay.few': '{count} napísané záznamy zostanú',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} otvorených návrhov zmizne',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} otvorený návrh zmizne',
|
||||
'assistant.card.journeyHeader.suggestionsGo.few': '{count} otvorené návrhy zmiznú',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} miest pribudne ako návrhy',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} miesto pribudne ako návrh',
|
||||
'assistant.card.journeyHeader.suggestionsCome.few': '{count} miesta pribudnú ako návrhy',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -29,5 +29,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'Sammanfatta de här dagarna',
|
||||
'assistant.journal.welcome': 'Vad ska stå i dagboken?',
|
||||
'assistant.journal.readOnly': 'Du kan bara läsa den här dagboken, så jag kan svara på frågor men inte ändra något.',
|
||||
'assistant.change.verb.createJournal': 'Starta en dagbok',
|
||||
'assistant.change.verb.renameJournal': 'Byt namn på dagboken',
|
||||
'assistant.change.verb.setJournalStatus': 'Ändra dagbokens status',
|
||||
'assistant.change.verb.updateJournalSettings': 'Ändra dagbokens inställningar',
|
||||
'assistant.change.verb.deleteJournal': 'Radera dagboken',
|
||||
'assistant.change.verb.linkJournalTrip': 'Lägg till en resa i dagboken',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Ta bort en resa ur dagboken',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Ta tillbaka förslagen',
|
||||
'assistant.change.verb.setJournalPreferences': 'Ändra din vy av dagboken',
|
||||
'assistant.card.journeyHeader.open': 'Öppna dagboken',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Visas som',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Omslagets färger',
|
||||
'assistant.card.journeyHeader.field.trip': 'Resa',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Förslag tillbaka',
|
||||
'assistant.card.journeyHeader.tripLink': 'Läggs till',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Tas bort',
|
||||
'assistant.card.journeyHeader.deletes': 'Dagboken försvinner med alla inlägg och foton. Det går inte att ångra.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} skrivna inlägg finns kvar',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} skrivet inlägg finns kvar',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} öppna förslag försvinner',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} öppet förslag försvinner',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} platser kommer in som förslag',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} plats kommer in som förslag',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -26,5 +26,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': 'สรุปวันเหล่านี้',
|
||||
'assistant.journal.welcome': 'จะเขียนอะไรลงในบันทึกดี?',
|
||||
'assistant.journal.readOnly': 'คุณอ่านบันทึกนี้ได้อย่างเดียว ฉันจึงตอบคำถามได้แต่เปลี่ยนแปลงอะไรไม่ได้',
|
||||
'assistant.change.verb.createJournal': 'เริ่มบันทึกใหม่',
|
||||
'assistant.change.verb.renameJournal': 'เปลี่ยนชื่อบันทึก',
|
||||
'assistant.change.verb.setJournalStatus': 'เปลี่ยนสถานะบันทึก',
|
||||
'assistant.change.verb.updateJournalSettings': 'เปลี่ยนการตั้งค่าบันทึก',
|
||||
'assistant.change.verb.deleteJournal': 'ลบบันทึก',
|
||||
'assistant.change.verb.linkJournalTrip': 'เพิ่มทริปลงในบันทึก',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'เอาทริปออกจากบันทึก',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'นำคำแนะนำกลับมา',
|
||||
'assistant.change.verb.setJournalPreferences': 'เปลี่ยนมุมมองบันทึกของคุณ',
|
||||
'assistant.card.journeyHeader.open': 'เปิดบันทึก',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'แสดงเป็น',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'สีหน้าปก',
|
||||
'assistant.card.journeyHeader.field.trip': 'ทริป',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'คำแนะนำที่กลับมา',
|
||||
'assistant.card.journeyHeader.tripLink': 'จะถูกเพิ่ม',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'จะถูกนำออก',
|
||||
'assistant.card.journeyHeader.deletes': 'บันทึกจะถูกลบพร้อมรายการและรูปภาพทั้งหมด ย้อนกลับไม่ได้',
|
||||
'assistant.card.journeyHeader.entriesStay': 'รายการที่เขียนแล้ว {count} รายการยังอยู่',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'คำแนะนำที่ยังไม่เขียน {count} รายการจะหายไป',
|
||||
'assistant.card.journeyHeader.suggestionsCome': 'สถานที่ {count} แห่งจะเข้ามาเป็นคำแนะนำ',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -30,5 +30,29 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Günlüğe ne yazalım?',
|
||||
'assistant.journal.readOnly':
|
||||
'Bu günlüğü yalnızca okuyabilirsin, bu yüzden soruları yanıtlayabilirim ama hiçbir şeyi değiştiremem.',
|
||||
'assistant.change.verb.createJournal': 'Günlük başlat',
|
||||
'assistant.change.verb.renameJournal': 'Günlüğün adını değiştir',
|
||||
'assistant.change.verb.setJournalStatus': 'Günlüğün durumunu değiştir',
|
||||
'assistant.change.verb.updateJournalSettings': 'Günlüğün ayarlarını değiştir',
|
||||
'assistant.change.verb.deleteJournal': 'Günlüğü sil',
|
||||
'assistant.change.verb.linkJournalTrip': 'Günlüğe seyahat ekle',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Seyahati günlükten çıkar',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Önerileri geri getir',
|
||||
'assistant.change.verb.setJournalPreferences': 'Günlüğün senin için görünümünü değiştir',
|
||||
'assistant.card.journeyHeader.open': 'Günlüğü aç',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Görünen',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Kapak renkleri',
|
||||
'assistant.card.journeyHeader.field.trip': 'Seyahat',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Geri gelen öneriler',
|
||||
'assistant.card.journeyHeader.tripLink': 'Eklenecek',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Çıkarılacak',
|
||||
'assistant.card.journeyHeader.deletes': 'Günlük tüm kayıtları ve fotoğraflarıyla birlikte silinir. Bu geri alınamaz.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} yazılmış kayıt kalır',
|
||||
'assistant.card.journeyHeader.entriesStay.one': '{count} yazılmış kayıt kalır',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} açık öneri kalkar',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': '{count} açık öneri kalkar',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} yer öneri olarak eklenir',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} yer öneri olarak eklenir',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -36,5 +36,35 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Що запишемо до щоденника?',
|
||||
'assistant.journal.readOnly':
|
||||
'Цей щоденник ти можеш лише читати, тож я можу відповідати на запитання, але нічого не зміню.',
|
||||
'assistant.change.verb.createJournal': 'Почати щоденник',
|
||||
'assistant.change.verb.renameJournal': 'Перейменувати щоденник',
|
||||
'assistant.change.verb.setJournalStatus': 'Змінити статус щоденника',
|
||||
'assistant.change.verb.updateJournalSettings': 'Змінити налаштування щоденника',
|
||||
'assistant.change.verb.deleteJournal': 'Видалити щоденник',
|
||||
'assistant.change.verb.linkJournalTrip': 'Додати подорож до щоденника',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Прибрати подорож зі щоденника',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Повернути пропозиції',
|
||||
'assistant.change.verb.setJournalPreferences': 'Змінити свій вигляд щоденника',
|
||||
'assistant.card.journeyHeader.open': 'Відкрити щоденник',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Показується як',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Кольори обкладинки',
|
||||
'assistant.card.journeyHeader.field.trip': 'Подорож',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Повернені пропозиції',
|
||||
'assistant.card.journeyHeader.tripLink': 'Буде додана',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Буде прибрана',
|
||||
'assistant.card.journeyHeader.deletes': 'Щоденник зникне з усіма записами й фото. Це не можна скасувати.',
|
||||
'assistant.card.journeyHeader.entriesStay': 'Залишаться {count} написаних записів',
|
||||
'assistant.card.journeyHeader.entriesStay.one': 'Залишиться {count} написаний запис',
|
||||
'assistant.card.journeyHeader.entriesStay.few': 'Залишаться {count} написані записи',
|
||||
'assistant.card.journeyHeader.entriesStay.many': 'Залишаться {count} написаних записів',
|
||||
'assistant.card.journeyHeader.suggestionsGo': 'Зникнуть {count} відкритих пропозицій',
|
||||
'assistant.card.journeyHeader.suggestionsGo.one': 'Зникне {count} відкрита пропозиція',
|
||||
'assistant.card.journeyHeader.suggestionsGo.few': 'Зникнуть {count} відкриті пропозиції',
|
||||
'assistant.card.journeyHeader.suggestionsGo.many': 'Зникнуть {count} відкритих пропозицій',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} місць з’являться як пропозиції',
|
||||
'assistant.card.journeyHeader.suggestionsCome.one': '{count} місце з’явиться як пропозиція',
|
||||
'assistant.card.journeyHeader.suggestionsCome.few': '{count} місця з’являться як пропозиції',
|
||||
'assistant.card.journeyHeader.suggestionsCome.many': '{count} місць з’являться як пропозиції',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -27,5 +27,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.journal.welcome': 'Mình viết gì vào nhật ký nhé?',
|
||||
'assistant.journal.readOnly':
|
||||
'Bạn chỉ có thể đọc nhật ký này, nên mình có thể trả lời câu hỏi nhưng không thay đổi được gì.',
|
||||
'assistant.change.verb.createJournal': 'Bắt đầu nhật ký',
|
||||
'assistant.change.verb.renameJournal': 'Đổi tên nhật ký',
|
||||
'assistant.change.verb.setJournalStatus': 'Đổi trạng thái nhật ký',
|
||||
'assistant.change.verb.updateJournalSettings': 'Đổi cài đặt nhật ký',
|
||||
'assistant.change.verb.deleteJournal': 'Xóa nhật ký',
|
||||
'assistant.change.verb.linkJournalTrip': 'Thêm chuyến đi vào nhật ký',
|
||||
'assistant.change.verb.unlinkJournalTrip': 'Bỏ chuyến đi khỏi nhật ký',
|
||||
'assistant.change.verb.restoreJournalSuggestions': 'Khôi phục các gợi ý',
|
||||
'assistant.change.verb.setJournalPreferences': 'Đổi cách bạn xem nhật ký',
|
||||
'assistant.card.journeyHeader.open': 'Mở nhật ký',
|
||||
'assistant.card.journeyHeader.setting': '{name}: {value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': 'Hiển thị là',
|
||||
'assistant.card.journeyHeader.field.coverGradient': 'Màu bìa',
|
||||
'assistant.card.journeyHeader.field.trip': 'Chuyến đi',
|
||||
'assistant.card.journeyHeader.field.suggestions': 'Gợi ý được khôi phục',
|
||||
'assistant.card.journeyHeader.tripLink': 'Sẽ được thêm',
|
||||
'assistant.card.journeyHeader.tripUnlink': 'Sẽ bị bỏ ra',
|
||||
'assistant.card.journeyHeader.deletes': 'Nhật ký sẽ bị xóa cùng mọi mục và ảnh. Không thể hoàn tác.',
|
||||
'assistant.card.journeyHeader.entriesStay': '{count} mục đã viết vẫn được giữ',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} gợi ý chưa viết sẽ bị bỏ',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} địa điểm được thêm vào làm gợi ý',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -26,5 +26,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': '總結這幾天',
|
||||
'assistant.journal.welcome': '日誌裡要寫些什麼?',
|
||||
'assistant.journal.readOnly': '你只能查看這本日誌,所以我可以回答問題,但不能做任何更改。',
|
||||
'assistant.change.verb.createJournal': '建立日誌',
|
||||
'assistant.change.verb.renameJournal': '重新命名日誌',
|
||||
'assistant.change.verb.setJournalStatus': '變更日誌狀態',
|
||||
'assistant.change.verb.updateJournalSettings': '變更日誌設定',
|
||||
'assistant.change.verb.deleteJournal': '刪除日誌',
|
||||
'assistant.change.verb.linkJournalTrip': '將旅行加入日誌',
|
||||
'assistant.change.verb.unlinkJournalTrip': '從日誌移除旅行',
|
||||
'assistant.change.verb.restoreJournalSuggestions': '恢復建議',
|
||||
'assistant.change.verb.setJournalPreferences': '變更你的日誌檢視',
|
||||
'assistant.card.journeyHeader.open': '開啟日誌',
|
||||
'assistant.card.journeyHeader.setting': '{name}:{value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': '顯示為',
|
||||
'assistant.card.journeyHeader.field.coverGradient': '封面顏色',
|
||||
'assistant.card.journeyHeader.field.trip': '旅行',
|
||||
'assistant.card.journeyHeader.field.suggestions': '恢復的建議',
|
||||
'assistant.card.journeyHeader.tripLink': '將被加入',
|
||||
'assistant.card.journeyHeader.tripUnlink': '將被移除',
|
||||
'assistant.card.journeyHeader.deletes': '日誌及其所有條目和照片都會被刪除,且無法復原。',
|
||||
'assistant.card.journeyHeader.entriesStay': '已寫的 {count} 則條目會保留',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} 則未寫的建議會被移除',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} 個地點會作為建議加入',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
@@ -26,5 +26,26 @@ const assistantJourney: TranslationStrings = {
|
||||
'assistant.followUp.journalSummary': '总结这几天',
|
||||
'assistant.journal.welcome': '日志里要写些什么?',
|
||||
'assistant.journal.readOnly': '你只能查看这本日志,所以我可以回答问题,但不能做任何更改。',
|
||||
'assistant.change.verb.createJournal': '新建日志',
|
||||
'assistant.change.verb.renameJournal': '重命名日志',
|
||||
'assistant.change.verb.setJournalStatus': '更改日志状态',
|
||||
'assistant.change.verb.updateJournalSettings': '更改日志设置',
|
||||
'assistant.change.verb.deleteJournal': '删除日志',
|
||||
'assistant.change.verb.linkJournalTrip': '向日志添加旅行',
|
||||
'assistant.change.verb.unlinkJournalTrip': '从日志移除旅行',
|
||||
'assistant.change.verb.restoreJournalSuggestions': '恢复建议',
|
||||
'assistant.change.verb.setJournalPreferences': '更改你的日志视图',
|
||||
'assistant.card.journeyHeader.open': '打开日志',
|
||||
'assistant.card.journeyHeader.setting': '{name}:{value}',
|
||||
'assistant.card.journeyHeader.field.statusOverride': '显示为',
|
||||
'assistant.card.journeyHeader.field.coverGradient': '封面颜色',
|
||||
'assistant.card.journeyHeader.field.trip': '旅行',
|
||||
'assistant.card.journeyHeader.field.suggestions': '恢复的建议',
|
||||
'assistant.card.journeyHeader.tripLink': '将被添加',
|
||||
'assistant.card.journeyHeader.tripUnlink': '将被移除',
|
||||
'assistant.card.journeyHeader.deletes': '日志及其所有条目和照片都会被删除,且无法撤销。',
|
||||
'assistant.card.journeyHeader.entriesStay': '已写的 {count} 条条目会保留',
|
||||
'assistant.card.journeyHeader.suggestionsGo': '{count} 条未写的建议会被移除',
|
||||
'assistant.card.journeyHeader.suggestionsCome': '{count} 个地点会作为建议加入',
|
||||
};
|
||||
export default assistantJourney;
|
||||
|
||||
Reference in New Issue
Block a user