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:
Maurice
2026-10-24 21:36:20 +02:00
parent cf35dc7afd
commit 48ff1b3a98
39 changed files with 2044 additions and 11 deletions
@@ -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>;
+24 -5
View File
@@ -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;
+36
View File
@@ -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;
+25
View File
@@ -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;
+24
View File
@@ -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;
+24
View File
@@ -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;
+27
View File
@@ -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;
+25
View File
@@ -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;
+24
View File
@@ -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;
+24
View File
@@ -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;
+24
View File
@@ -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;
+25
View File
@@ -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;
+25
View File
@@ -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;
+25
View File
@@ -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;
+21
View File
@@ -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;
+24
View File
@@ -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;
+21
View File
@@ -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;
+21
View File
@@ -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;
+25
View File
@@ -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;
+30
View File
@@ -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;
+30
View File
@@ -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
View File
@@ -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;
+24
View File
@@ -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;
+21
View File
@@ -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;
+24
View File
@@ -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;
+30
View File
@@ -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;
+21
View File
@@ -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;
+21
View File
@@ -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;
+21
View File
@@ -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;