feat(mobile): icon-only action rows in the transport and booking sheets (#2585)

The transport sheet's row wrapped once a ticket and the wallet were there.
Its actions are icons now, named by their labels, and the attachments sit in
their own rows with a download, like the transports list. The wallet is a
black circle, beside the close button in the booking editor. A failed
download on the phone now says so.
This commit is contained in:
Maurice
2026-11-13 10:49:30 +01:00
parent 5137bcb86a
commit a08d4b944e
38 changed files with 103 additions and 95 deletions
@@ -85,6 +85,14 @@ describe('useReservationCard', () => {
expect(openFile).toHaveBeenCalledTimes(1);
});
it('FE-PLANNER-RESLISTFILTER-008b: a download that fails says so with a toast', async () => {
vi.mocked(downloadFile).mockRejectedValueOnce(new Error('offline'));
const opts = host();
const { result } = renderHook(() => useReservationCard(ann, opts));
result.current.downloadFile(file);
await vi.waitFor(() => expect(opts.toast.error).toHaveBeenCalledWith('files.downloadError'));
});
it('FE-PLANNER-RESLISTFILTER-009: the card knows whether deleting it takes a linked expense along (#2604)', () => {
resetAllStores();
seedStore(useTripStore, { budgetItems: [buildBudgetItem({ id: 1, reservation_id: 10 } as never)] });
@@ -19,9 +19,10 @@ export interface ReservationCardHost {
* opening: its booking code blurred until tapped while the "Blur booking codes"
* preference is on (off, the code is plain and has no toggle), the delete
* behind a question with a toast when it fails, and its attached files, which
* open or download from their own row. A file that cannot be opened or saved
* stays silent here; the desktop panel toasts it. The delete question names a
* linked expense, which goes with the booking.
* open or download from their own row. A download that fails says so with a
* toast, since a save button that does nothing reads as broken; opening stays
* silent as it always was. The delete question names a linked expense, which
* goes with the booking.
*/
export function useReservationCard(res: Reservation, host: ReservationCardHost) {
const blurCodes = host.settings.blur_booking_codes;
@@ -45,6 +46,6 @@ export function useReservationCard(res: Reservation, host: ReservationCardHost)
void confirmDelete();
},
openFile: (f: Attachment) => openAttachment(f),
downloadFile: (f: Attachment) => downloadAttachment(f),
downloadFile: (f: Attachment) => downloadAttachment(f, () => host.toast.error(host.t('files.downloadError'))),
};
}
@@ -66,9 +66,9 @@ describe('MWalletButton', () => {
wallet.addGoogle.mockResolvedValue('added');
const onError = vi.fn();
render(<MWalletButton tripId={42} reservationId={7} onError={onError} />);
// The label is short, the accessible name says which wallet.
expect(screen.getByRole('button')).toHaveTextContent(/^Wallet$/);
expect(screen.getByRole('button', { name: /Google Wallet/ })).toBeInTheDocument();
// An icon only: the name and the tooltip say which wallet.
expect(screen.getByRole('button')).toHaveTextContent('');
expect(screen.getByRole('button', { name: /Google Wallet/ })).toHaveAttribute('title', 'Add to Google Wallet');
fireEvent.click(screen.getByRole('button'));
await waitFor(() => expect(wallet.addGoogle).toHaveBeenCalledWith(42, 7, expect.any(String)));
expect(wallet.add).not.toHaveBeenCalled();
+21 -7
View File
@@ -17,15 +17,24 @@ interface MWalletButtonProps {
className?: string;
/** Shown when the pass could not be made (a toast in the sheet that hosts the button). */
onError: (message: string) => void;
/** 34px beside a sheet header's close button instead of 40px in an action row. */
compact?: boolean;
}
/**
* "Add to Apple Wallet" on a booking in the iOS app (feature wallet), "Add to
* Google Wallet" in the Android app (feature googleWallet): the booking becomes a
* TREK pass that shows up around its time and place. Black like both stores' own
* badges, so it reads as the wallet at a glance.
* TREK pass that shows up around its time and place. A black circle like both
* stores' own badges, so it reads as the wallet at a glance; the phone's action
* rows are icons only, the name is in the label and tooltip.
*/
export default function MWalletButton({ tripId, reservationId, onError, className }: MWalletButtonProps) {
export default function MWalletButton({
tripId,
reservationId,
onError,
className,
compact = false,
}: MWalletButtonProps) {
const { t, language } = useTranslation();
// Re-renders once the app binary has said what it can do.
useNativeCapabilities();
@@ -51,17 +60,22 @@ export default function MWalletButton({ tripId, reservationId, onError, classNam
}
};
const label = t(wallet === 'google' ? 'reservations.wallet.addGoogle' : 'reservations.wallet.add');
const button = (
<button
type="button"
onClick={() => void add()}
disabled={busy}
aria-busy={busy}
aria-label={t(wallet === 'google' ? 'reservations.wallet.addGoogle' : 'reservations.wallet.add')}
className="flex items-center gap-[6px] rounded-full bg-black px-3 py-[7px] text-[length:calc(12px*var(--fs-scale-body,1))] font-semibold text-white transition-transform active:scale-[.97] disabled:opacity-70" // theme-lint-disable: the wallets' badges are black in either theme
aria-label={label}
title={label}
className={`flex ${compact ? 'h-[34px] w-[34px]' : 'h-10 w-10'} flex-none items-center justify-center rounded-full bg-black text-white transition-transform active:scale-[.97] disabled:opacity-70 dark:ring-1 dark:ring-white/25`} // theme-lint-disable: the wallets' badges are black in either theme
>
{busy ? <Loader2 size={13} className="animate-spin" /> : <Wallet size={13} strokeWidth={2} />}
{t('reservations.wallet.short')}
{busy ? (
<Loader2 size={compact ? 15 : 16} className="animate-spin" />
) : (
<Wallet size={compact ? 15 : 16} strokeWidth={2} />
)}
</button>
);
return className ? <div className={className}>{button}</div> : button;
@@ -185,13 +185,13 @@ export default function MReservationSheet({ planner, onOpenExpense }: MReservati
<FormSheetHeader
icon={Ticket}
title={res ? t('reservations.editTitle') : t('reservations.newTitle')}
// A saved booking goes into the phone's wallet (in the iOS and Android apps).
trailing={<MWalletButton tripId={tripId} reservationId={savedId(res)} onError={toast.error} compact />}
onClose={handleClose}
closeLabel={t('common.close')}
/>
<FormSheetBody>
{/* A saved booking goes into Apple Wallet (iOS app only). */}
<MWalletButton tripId={tripId} reservationId={savedId(res)} onError={toast.error} className="mb-1 mt-2 flex" />
{/* BOOKING TYPE */}
<Eyebrow className="mb-[6px] mt-2 uppercase">{t('reservations.bookingType')}</Eyebrow>
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { ArrowRight, Footprints, Paperclip, Pencil, Route as RouteIcon, Trash2 } from 'lucide-react'
import { ArrowRight, Footprints, Pencil, Route as RouteIcon, Trash2 } from 'lucide-react'
import MSheet from '../../../components/MSheet'
import MWalletButton from '../../../components/MWalletButton'
import type { MTripSheetsProps } from '../MTripShell'
@@ -8,7 +8,6 @@ import { useSettingsStore } from '../../../../store/settingsStore'
import { RES_ICONS } from '../../../../components/Planner/DayPlanSidebar.constants'
import { splitReservationDateTime } from '../../../../utils/formatters'
import { getFlightLegs, getTrainLegs, usesStationRoute, type FlightLeg, type TrainLeg } from '../../../../utils/flightLegs'
import { openFile } from '../../../../utils/fileDownload'
import { runsOnDay } from '../../../../utils/reservationRoutes'
import { filesFor } from '../../../../utils/reservationFiles'
import { bookingDayLabel, segmentCodeLabel } from '../../../../components/Planner/transportDetailModel'
@@ -18,7 +17,9 @@ import { isTransportReservationType } from '@trek/shared'
import { useBookingLink } from '../../../../hooks/useBookingLink'
import { bookingFacts, stayStats } from '../../../../components/Planner/bookings/bookingFacts'
import { parseMeta } from '../../../../components/Planner/bookings/bookingsModel'
import { Eyebrow, INNER_CLS, StatBox, TileHeader, displayTime } from './MTripSheetUi'
import { ActionCircle, Eyebrow, INNER_CLS, StatBox, TileHeader, displayTime } from './MTripSheetUi'
import { CardFiles } from '../tabs/MReservationCardParts'
import { downloadAttachment, openAttachment } from '../../../../components/Planner/bookings/openAttachment'
import { flowText } from '../../../../utils/flowText'
interface TransportSheetPayload {
@@ -342,50 +343,31 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) {
</>
)}
{/* ── Actions ── */}
<div className="mt-3 flex flex-wrap items-center gap-[7px]">
<button
type="button"
onClick={showOnMap}
aria-pressed={onMap}
className={`flex items-center gap-[5px] rounded-full px-3 py-[7px] text-[0.75rem] font-semibold ${
onMap ? 'bg-m-act text-m-actfg' : INNER_CLS
}`}
>
<RouteIcon size={13} strokeWidth={2} />
{t('mobileTrip.onMap')}
</button>
{/* ── Files: their own rows, so a long name never pushes the actions onto a second line ── */}
<CardFiles
files={resFiles}
card={{
openFile: openAttachment,
downloadFile: f => downloadAttachment(f, () => planner.toast.error(t('files.downloadError'))),
}}
t={t}
/>
{/* ── Actions: icons only, the names are their labels ── */}
<div className="mt-[14px] flex items-center gap-[7px]">
<ActionCircle onClick={showOnMap} label={t('mobileTrip.onMap')} pressed={onMap}>
<RouteIcon size={15} strokeWidth={2} />
</ActionCircle>
<MWalletButton tripId={planner.tripId} reservationId={payload.reservationId} onError={planner.toast.error} />
{resFiles.map(f => (
<button
key={f.id}
type="button"
onClick={() => openFile(f.url, f.original_name)}
className={`flex max-w-[150px] items-center gap-[5px] rounded-full px-3 py-[7px] text-[0.75rem] font-semibold ${INNER_CLS}`}
>
<Paperclip size={13} strokeWidth={2} className="flex-none" />
<span dir="auto" className="truncate">{f.original_name}</span>
</button>
))}
{canEdit && (
<button
type="button"
onClick={editTransport}
className="ms-auto flex items-center gap-[5px] rounded-full bg-m-act px-3 py-[7px] text-[0.75rem] font-semibold text-m-actfg"
>
<Pencil size={13} strokeWidth={2} />
{t('common.edit')}
</button>
<ActionCircle onClick={editTransport} label={t('common.edit')} primary className="ms-auto">
<Pencil size={15} strokeWidth={2} />
</ActionCircle>
)}
{canEdit && (
<button
type="button"
onClick={deleteTransport}
aria-label={t('common.delete')}
className={`flex items-center rounded-full px-3 py-[7px] text-[color:var(--m-st-danger)] ${INNER_CLS}`}
>
<Trash2 size={14} strokeWidth={2} />
</button>
<ActionCircle onClick={deleteTransport} label={t('common.delete')} danger>
<Trash2 size={15} strokeWidth={2} />
</ActionCircle>
)}
</div>
</div>
@@ -160,23 +160,29 @@ interface ActionCircleProps {
label: string
primary?: boolean
danger?: boolean
/** A toggle (shown on the map): announced as pressed and filled while on. */
pressed?: boolean
className?: string
children: ReactNode
/** For callers that anchor a popup to the button. React 19 passes it through. */
ref?: Ref<HTMLButtonElement>
}
/** 38px round action button of the inspector footer rows. */
export function ActionCircle({ onClick, label, primary = false, danger = false, className = '', children, ref }: ActionCircleProps) {
/**
* 40px round action button of the sheets' action rows. The rows hold icons
* only, so a phone never wraps them; the name is the label and the tooltip.
*/
export function ActionCircle({ onClick, label, primary = false, danger = false, pressed, className = '', children, ref }: ActionCircleProps) {
return (
<button
ref={ref}
type="button"
onClick={onClick}
aria-label={label}
aria-pressed={pressed}
title={label}
className={`flex h-[38px] w-[38px] flex-none items-center justify-center rounded-full ${
primary
className={`flex h-10 w-10 flex-none items-center justify-center rounded-full ${
primary || pressed
? 'bg-m-act text-m-actfg'
: `border border-[color:var(--m-gbr)] bg-[color:var(--m-ic)] ${danger ? 'text-[color:var(--m-st-danger)]' : 'text-m-ink'}`
} ${className}`}
@@ -64,12 +64,14 @@ interface FormSheetHeaderProps {
subtitle?: ReactNode
/** Replaces the icon tile, e.g. a back button on the import sub-steps. */
leading?: ReactNode
/** A 34px action before the close button (the booking's Wallet pass). */
trailing?: ReactNode
onClose: () => void
closeLabel: string
}
/** Form-sheet header: optional tile, 17px/700 title, 34px close. No hairline. */
export function FormSheetHeader({ icon: Icon, title, subtitle, leading, onClose, closeLabel }: FormSheetHeaderProps) {
export function FormSheetHeader({ icon: Icon, title, subtitle, leading, trailing, onClose, closeLabel }: FormSheetHeaderProps) {
return (
<div className="m-fs-head flex flex-none items-center gap-3 px-[18px] pb-2 pt-4">
{leading}
@@ -84,6 +86,7 @@ export function FormSheetHeader({ icon: Icon, title, subtitle, leading, onClose,
<div className="line-clamp-2 break-words text-[1.0625rem] font-bold leading-tight text-m-ink">{title}</div>
{subtitle && <div className="truncate font-geist text-[0.71875rem] text-m-muted">{subtitle}</div>}
</div>
{trailing}
<MIconBtn ariaLabel={closeLabel} onClick={onClose} variant="neutral" size={34} className="m-fs-close">
<X size={15} strokeWidth={2.2} />
</MIconBtn>
@@ -168,7 +168,15 @@ export function CardNotes({ notes }: { notes: string | null | undefined }) {
* can hold two real buttons: the name opens the file, the icon at the end saves
* it (a PDF ticket that only opens in a viewer cannot be kept otherwise).
*/
export function CardFiles({ files, card, t }: { files: ReturnType<typeof filesFor>; card: CardState; t: T }) {
export function CardFiles({
files,
card,
t,
}: {
files: ReturnType<typeof filesFor>;
card: Pick<CardState, 'openFile' | 'downloadFile'>;
t: T;
}) {
if (files.length === 0) return null;
return (
<div className="mt-2">
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import MTransportSheet from '../../../../src/mobile/screens/trip/sheets/MTransportSheet'
import { openFile } from '../../../../src/utils/fileDownload'
import { downloadFile, openFile } from '../../../../src/utils/fileDownload'
import { useSettingsStore } from '../../../../src/store/settingsStore'
import type { Reservation, TripFile } from '../../../../src/types'
import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
@@ -9,7 +9,10 @@ import { fireEvent, render, screen } from '../../../helpers/render'
// FE-MOB-TRSH-001 to FE-MOB-TRSH-035
vi.mock('../../../../src/utils/fileDownload', () => ({ openFile: vi.fn() }))
vi.mock('../../../../src/utils/fileDownload', () => ({
openFile: vi.fn(() => Promise.resolve()),
downloadFile: vi.fn(() => Promise.resolve()),
}))
const FLIGHT = {
id: 7,
@@ -218,6 +221,17 @@ describe('MTransportSheet', () => {
fireEvent.click(screen.getByText('boardingpass.pdf'))
expect(openFile).toHaveBeenCalledWith('/api/trips/1/files/1/download', 'boardingpass.pdf')
fireEvent.click(screen.getByRole('button', { name: 'Download: invoice.pdf' }))
expect(downloadFile).toHaveBeenCalledWith('/api/trips/1/files/2/download', 'invoice.pdf')
})
it('FE-MOB-TRSH-016b: the action row holds icons only, named by their labels', () => {
render(<MTransportSheet planner={makePlanner()} shell={makeShell()} />)
for (const name of ['Map', 'Edit', 'Delete']) {
const btn = screen.getByRole('button', { name })
expect(btn).toHaveTextContent('')
expect(btn).toHaveAttribute('title', name)
}
})
it('FE-MOB-TRSH-017: opens the transport editor for the booking', () => {
-1
View File
@@ -301,7 +301,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'تصدير الصفوف المعروضة ({shown} من {total}) (CSV)',
'reservations.wallet.add': 'إضافة إلى Apple Wallet',
'reservations.wallet.addGoogle': 'إضافة إلى Google Wallet',
'reservations.wallet.short': 'المحفظة',
'reservations.wallet.failed': 'تعذّر إنشاء بطاقة Wallet. حاول مرة أخرى.',
'reservations.wallet.undated': 'حدّد تاريخًا للحجز أولًا: يعرض Wallet البطاقة قرب موعده.',
};
-1
View File
@@ -309,7 +309,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Göstərilən {shown}/{total} sətri ixrac et (CSV)',
'reservations.wallet.add': 'Apple Wallet-ə əlavə et',
'reservations.wallet.addGoogle': 'Google Wallet-ə əlavə et',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Wallet kartını yaratmaq alınmadı. Yenidən cəhd edin.',
'reservations.wallet.undated': 'Əvvəlcə rezervasiyaya tarix verin: Wallet kartı həmin vaxta yaxın göstərir.',
};
-1
View File
@@ -275,7 +275,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Ezporzhiañ ar {shown} diwar {total} renk skwer (CSV)',
'reservations.wallet.add': 'Adicionar à Apple Wallet',
'reservations.wallet.addGoogle': 'Adicionar à Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Não foi possível criar o passe da Wallet. Tente novamente.',
'reservations.wallet.undated': 'Primeiro dê uma data à reserva: a Wallet mostra o passe perto desse horário.',
};
-1
View File
@@ -278,7 +278,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Exporta les {shown} de {total} files mostrades (CSV)',
'reservations.wallet.add': 'Afegeix a Apple Wallet',
'reservations.wallet.addGoogle': 'Afegeix a Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'No s’ha pogut crear el passi de Wallet. Torna-ho a provar.',
'reservations.wallet.undated':
'Primer posa una data a la reserva: Wallet mostra el passi al voltant d’aquell moment.',
-1
View File
@@ -282,7 +282,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Exportovat zobrazené řádky, {shown} z {total} (CSV)',
'reservations.wallet.add': 'Přidat do Apple Wallet',
'reservations.wallet.addGoogle': 'Přidat do Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Kartu do Wallet se nepodařilo vytvořit. Zkuste to znovu.',
'reservations.wallet.undated': 'Nejdřív rezervaci zadejte datum: Wallet kartu zobrazí kolem toho času.',
};
-1
View File
@@ -280,7 +280,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Die {shown} von {total} angezeigten Zeilen exportieren (CSV)',
'reservations.wallet.add': 'Zu Apple Wallet hinzufügen',
'reservations.wallet.addGoogle': 'Zu Google Wallet hinzufügen',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Der Wallet-Pass konnte nicht erstellt werden. Bitte versuch es erneut.',
'reservations.wallet.undated': 'Gib der Buchung zuerst ein Datum: Wallet zeigt den Pass rund um diesen Zeitpunkt an.',
};
-1
View File
@@ -277,7 +277,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Export the {shown} of {total} shown rows (CSV)',
'reservations.wallet.add': 'Add to Apple Wallet',
'reservations.wallet.addGoogle': 'Add to Google Wallet',
'reservations.wallet.short': 'Wallet',
'reservations.wallet.failed': 'The Wallet pass could not be made. Please try again.',
'reservations.wallet.undated': 'Give the booking a date first: Wallet brings the pass up around its time.',
};
-1
View File
@@ -278,7 +278,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Exportar las {shown} de {total} filas mostradas (CSV)',
'reservations.wallet.add': 'Añadir a Apple Wallet',
'reservations.wallet.addGoogle': 'Añadir a Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'No se pudo crear el pase de Wallet. Inténtalo de nuevo.',
'reservations.wallet.undated': 'Primero ponle una fecha a la reserva: Wallet muestra el pase en torno a ese momento.',
};
-1
View File
@@ -277,7 +277,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Ekspordi näidatud read, {shown} / {total} (CSV)',
'reservations.wallet.add': 'Lisa Apple Walletisse',
'reservations.wallet.addGoogle': 'Lisa Google Walletisse',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Walleti passi ei õnnestunud luua. Proovi uuesti.',
'reservations.wallet.undated': 'Anna broneeringule kõigepealt kuupäev: Wallet näitab passi selle aja paiku.',
};
-1
View File
@@ -281,7 +281,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Exporter les {shown} lignes affichées sur {total} (CSV)',
'reservations.wallet.add': 'Ajouter à Apple Wallet',
'reservations.wallet.addGoogle': 'Ajouter à Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Impossible de créer la carte Wallet. Réessayez.',
'reservations.wallet.undated':
'Donnez d’abord une date à la réservation : Wallet affiche la carte autour de ce moment.',
-1
View File
@@ -280,7 +280,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Εξαγωγή των {shown} από {total} εμφανιζόμενων γραμμών (CSV)',
'reservations.wallet.add': 'Προσθήκη στο Apple Wallet',
'reservations.wallet.addGoogle': 'Προσθήκη στο Google Wallet',
'reservations.wallet.short': 'Πορτοφόλι',
'reservations.wallet.failed': 'Δεν ήταν δυνατή η δημιουργία της κάρτας Wallet. Δοκιμάστε ξανά.',
'reservations.wallet.undated':
'Δώστε πρώτα ημερομηνία στην κράτηση: το Wallet εμφανίζει την κάρτα γύρω από εκείνη την ώρα.',
-1
View File
@@ -279,7 +279,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'ייצוא {shown} מתוך {total} השורות המוצגות (CSV)',
'reservations.wallet.add': 'הוספה ל-Apple Wallet',
'reservations.wallet.addGoogle': 'הוספה ל-Google Wallet',
'reservations.wallet.short': 'ארנק',
'reservations.wallet.failed': 'לא ניתן היה ליצור את כרטיס ה-Wallet. אנא נסו שוב.',
'reservations.wallet.undated': 'קודם תנו להזמנה תאריך: Wallet מציג את הכרטיס סביב המועד שלה.',
};
-1
View File
@@ -279,7 +279,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'A megjelenített {shown} sor exportálása a(z) {total} közül (CSV)',
'reservations.wallet.add': 'Hozzáadás az Apple Wallethez',
'reservations.wallet.addGoogle': 'Hozzáadás a Google Wallethez',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Nem sikerült létrehozni a Wallet-kártyát. Próbáld újra.',
'reservations.wallet.undated': 'Előbb adj dátumot a foglalásnak: a Wallet az időpont körül jeleníti meg a kártyát.',
};
-1
View File
@@ -268,7 +268,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Ekspor {shown} dari {total} baris yang ditampilkan (CSV)',
'reservations.wallet.add': 'Tambahkan ke Apple Wallet',
'reservations.wallet.addGoogle': 'Tambahkan ke Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Pass Wallet tidak dapat dibuat. Silakan coba lagi.',
'reservations.wallet.undated': 'Beri tanggal pada pemesanan dulu: Wallet menampilkan pass di sekitar waktunya.',
};
-1
View File
@@ -276,7 +276,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Esporta le {shown} righe mostrate su {total} (CSV)',
'reservations.wallet.add': 'Aggiungi ad Apple Wallet',
'reservations.wallet.addGoogle': 'Aggiungi a Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Impossibile creare il pass di Wallet. Riprova.',
'reservations.wallet.undated': 'Prima dai una data alla prenotazione: Wallet mostra il pass intorno a quel momento.',
};
-1
View File
@@ -268,7 +268,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': '表示中の {shown}/{total} 行をエクスポート (CSV)',
'reservations.wallet.add': 'Apple Walletに追加',
'reservations.wallet.addGoogle': 'Google Walletに追加',
'reservations.wallet.short': 'ウォレット',
'reservations.wallet.failed': 'Walletパスを作成できませんでした。もう一度お試しください。',
'reservations.wallet.undated': '先に予約に日付を入れてください。Walletはその時刻の前後にパスを表示します。',
};
-1
View File
@@ -268,7 +268,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': '표시된 {total}행 중 {shown}행 내보내기 (CSV)',
'reservations.wallet.add': 'Apple Wallet에 추가',
'reservations.wallet.addGoogle': 'Google Wallet에 추가',
'reservations.wallet.short': '지갑',
'reservations.wallet.failed': 'Wallet 패스를 만들지 못했습니다. 다시 시도하세요.',
'reservations.wallet.undated': '먼저 예약에 날짜를 지정하세요. Wallet은 그 시간 즈음에 패스를 보여 줍니다.',
};
-1
View File
@@ -277,7 +277,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'De {shown} van {total} getoonde rijen exporteren (CSV)',
'reservations.wallet.add': 'Voeg toe aan Apple Wallet',
'reservations.wallet.addGoogle': 'Voeg toe aan Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'De Wallet-pas kon niet worden gemaakt. Probeer het opnieuw.',
'reservations.wallet.undated': 'Geef de boeking eerst een datum: Wallet toont de pas rond dat moment.',
};
-1
View File
@@ -291,7 +291,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Eksportuj wyświetlone wiersze: {shown} z {total} (CSV)',
'reservations.wallet.add': 'Dodaj do Apple Wallet',
'reservations.wallet.addGoogle': 'Dodaj do Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Nie udało się utworzyć karty Wallet. Spróbuj ponownie.',
'reservations.wallet.undated': 'Najpierw nadaj rezerwacji datę: Wallet pokazuje kartę w okolicach tego terminu.',
};
-1
View File
@@ -289,7 +289,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Экспортировать показанные строки: {shown} из {total} (CSV)',
'reservations.wallet.add': 'Добавить в Apple Wallet',
'reservations.wallet.addGoogle': 'Добавить в Google Wallet',
'reservations.wallet.short': 'Кошелёк',
'reservations.wallet.failed': 'Не удалось создать карту Wallet. Повторите попытку.',
'reservations.wallet.undated': 'Сначала укажите дату брони: Wallet показывает карту ближе к этому времени.',
};
-1
View File
@@ -285,7 +285,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Exportovať zobrazené riadky, {shown} z {total} (CSV)',
'reservations.wallet.add': 'Pridať do Apple Wallet',
'reservations.wallet.addGoogle': 'Pridať do Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Kartu do Wallet sa nepodarilo vytvoriť. Skúste to znova.',
'reservations.wallet.undated': 'Najprv rezervácii zadajte dátum: Wallet kartu zobrazí okolo toho času.',
};
-1
View File
@@ -276,7 +276,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Exportera de {shown} av {total} visade raderna (CSV)',
'reservations.wallet.add': 'Lägg till i Apple Wallet',
'reservations.wallet.addGoogle': 'Lägg till i Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Wallet-kortet kunde inte skapas. Försök igen.',
'reservations.wallet.undated': 'Ge bokningen ett datum först: Wallet visar kortet runt den tiden.',
};
-1
View File
@@ -270,7 +270,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'ส่งออก {shown} จาก {total} แถวที่แสดง (CSV)',
'reservations.wallet.add': 'เพิ่มลงใน Apple Wallet',
'reservations.wallet.addGoogle': 'เพิ่มลงใน Google Wallet',
'reservations.wallet.short': 'กระเป๋าเงิน',
'reservations.wallet.failed': 'สร้างบัตร Wallet ไม่สำเร็จ โปรดลองอีกครั้ง',
'reservations.wallet.undated': 'กำหนดวันที่ให้การจองก่อน Wallet จะแสดงบัตรในช่วงเวลานั้น',
};
-1
View File
@@ -277,7 +277,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Gösterilen {total} satırdan {shown} tanesini dışa aktar (CSV)',
'reservations.wallet.add': 'Apple Cüzdan’a Ekle',
'reservations.wallet.addGoogle': 'Google Cüzdan’a Ekle',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Cüzdan kartı oluşturulamadı. Lütfen tekrar deneyin.',
'reservations.wallet.undated': 'Önce rezervasyona bir tarih verin: Cüzdan kartı o zamana yakın gösterir.',
};
-1
View File
@@ -289,7 +289,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Експортувати показані рядки: {shown} із {total} (CSV)',
'reservations.wallet.add': 'Додати до Apple Wallet',
'reservations.wallet.addGoogle': 'Додати до Google Wallet',
'reservations.wallet.short': 'Гаманець',
'reservations.wallet.failed': 'Не вдалося створити картку Wallet. Повторіть спробу.',
'reservations.wallet.undated': 'Спершу вкажіть дату бронювання: Wallet показує картку ближче до цього часу.',
};
-1
View File
@@ -271,7 +271,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': 'Xuất {shown} trong {total} dòng đang hiển thị (CSV)',
'reservations.wallet.add': 'Thêm vào Apple Wallet',
'reservations.wallet.addGoogle': 'Thêm vào Google Wallet',
'reservations.wallet.short': 'Wallet', // same-as-en
'reservations.wallet.failed': 'Không thể tạo thẻ Wallet. Vui lòng thử lại.',
'reservations.wallet.undated': 'Hãy đặt ngày cho đặt chỗ trước: Wallet sẽ hiện thẻ vào khoảng thời gian đó.',
};
-1
View File
@@ -263,7 +263,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': '匯出顯示的 {shown}/{total} 列 (CSV)',
'reservations.wallet.add': '加入 Apple 錢包',
'reservations.wallet.addGoogle': '加入 Google 錢包',
'reservations.wallet.short': '錢包',
'reservations.wallet.failed': '無法產生錢包票卡。請重試。',
'reservations.wallet.undated': '請先為訂位設定日期:錢包會在那個時間前後顯示票卡。',
};
-1
View File
@@ -263,7 +263,6 @@ const reservations: TranslationStrings = {
'reservations.exportCsvFiltered': '导出显示的 {shown}/{total} 行 (CSV)',
'reservations.wallet.add': '添加到 Apple 钱包',
'reservations.wallet.addGoogle': '添加到 Google 钱包',
'reservations.wallet.short': '钱包',
'reservations.wallet.failed': '无法生成钱包卡券。请重试。',
'reservations.wallet.undated': '请先为预订设置日期:钱包会在那个时间前后显示卡券。',
};