diff --git a/client/src/components/Planner/bookings/bookingParts.test.tsx b/client/src/components/Planner/bookings/bookingParts.test.tsx index 48972ef90..8037b2bcf 100644 --- a/client/src/components/Planner/bookings/bookingParts.test.tsx +++ b/client/src/components/Planner/bookings/bookingParts.test.tsx @@ -1,8 +1,16 @@ -// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-002 -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-004 +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { buildTripFile } from '../../../../tests/helpers/factories'; +import { downloadFile, openFile } from '../../../utils/fileDownload'; import type { BookingFacts } from './bookingFacts'; -import { WhenFields } from './bookingParts'; +import { FileRows, WhenFields } from './bookingParts'; + +vi.mock('../../../utils/fileDownload', () => ({ + openFile: vi.fn(() => Promise.resolve()), + downloadFile: vi.fn(() => Promise.resolve()), +})); function facts(day: BookingFacts['day'], time: string | null = '15:35 → 09:55'): BookingFacts { return { @@ -47,3 +55,37 @@ describe('WhenFields', () => { expect(screen.getByText('Day 3')).toBeInTheDocument(); }); }); + +describe('FileRows', () => { + const file = buildTripFile({ id: 5, url: '/uploads/f/5', original_name: 'ticket.pdf' }); + + it('FE-PLANNER-BKPARTS-003: the name opens the file, the icon downloads it, and neither reaches the card (#2585)', () => { + vi.mocked(openFile).mockClear(); + vi.mocked(downloadFile).mockClear(); + const onCard = vi.fn(); + render( +
+ +
+ ); + + const open = screen.getByRole('button', { name: 'ticket.pdf' }); + const download = screen.getByRole('button', { name: /: ticket\.pdf$/ }); + // Two sibling buttons, never one inside the other. + expect(open.contains(download)).toBe(false); + expect(download.contains(open)).toBe(false); + + fireEvent.click(open); + expect(openFile).toHaveBeenCalledWith('/uploads/f/5', 'ticket.pdf'); + expect(downloadFile).not.toHaveBeenCalled(); + fireEvent.click(download); + expect(downloadFile).toHaveBeenCalledWith('/uploads/f/5', 'ticket.pdf'); + expect(openFile).toHaveBeenCalledOnce(); + expect(onCard).not.toHaveBeenCalled(); + }); + + it('FE-PLANNER-BKPARTS-004: no files render nothing', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/client/src/components/Planner/bookings/bookingParts.tsx b/client/src/components/Planner/bookings/bookingParts.tsx index 54bd1fa6b..58c287a9e 100644 --- a/client/src/components/Planner/bookings/bookingParts.tsx +++ b/client/src/components/Planner/bookings/bookingParts.tsx @@ -1,5 +1,5 @@ import type { CSSProperties, ReactNode } from 'react' -import { AlertCircle, ChevronDown, ChevronUp, FileText, Plane } from 'lucide-react' +import { AlertCircle, ChevronDown, ChevronUp, Download, FileText, Plane } from 'lucide-react' import type { Reservation, TripFile } from '../../../types' import type { ReservationTraveler } from '@trek/shared' import { useTranslation } from '../../../i18n' @@ -10,7 +10,7 @@ import { PILL } from '../../shared/DialogShell' import { useToast } from '../../shared/Toast' import { parseMeta, typeInfo } from './bookingsModel' import type { BookingFacts } from './bookingFacts' -import { openAttachment } from './openAttachment' +import { downloadAttachment, openAttachment } from './openAttachment' // The phone's card language on the desktop: small Geist eyebrows, framed value // boxes, count pills. Sizes scale with the user's text size setting. @@ -289,22 +289,46 @@ export function useOpenFile() { return (f: TripFile) => openAttachment(f, () => toast.error(t('files.openError'))) } -/** The attached files, one framed row each, opening the file. */ +/** Saves an attached file, and says so when it cannot. */ +function useDownloadFile() { + const { t } = useTranslation() + const toast = useToast() + return (f: TripFile) => downloadAttachment(f, () => toast.error(t('files.downloadError'))) +} + +/** + * The attached files, one framed row each: the name opens the file, the icon at + * the end saves it. Two sibling buttons rather than one row button, since + * buttons cannot nest; both stop the click so the card around them stays put. + */ export function FileRows({ files }: { files: TripFile[] }) { + const { t } = useTranslation() const open = useOpenFile() + const download = useDownloadFile() if (files.length === 0) return null return (
{files.map(f => ( - +
+ + + + +
))}
) diff --git a/client/src/components/Planner/bookings/openAttachment.test.ts b/client/src/components/Planner/bookings/openAttachment.test.ts index 847d3dfbb..dda4b16b5 100644 --- a/client/src/components/Planner/bookings/openAttachment.test.ts +++ b/client/src/components/Planner/bookings/openAttachment.test.ts @@ -1,10 +1,13 @@ -// FE-PLANNER-OPENATTACH-001 to -003: opening a booking's attached file. +// FE-PLANNER-OPENATTACH-001 to -006: opening and downloading a booking's attached file. import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { openFile } from '../../../utils/fileDownload'; -import { openAttachment } from './openAttachment'; +import { downloadFile, openFile } from '../../../utils/fileDownload'; +import { downloadAttachment, openAttachment } from './openAttachment'; -vi.mock('../../../utils/fileDownload', () => ({ openFile: vi.fn(() => Promise.resolve()) })); +vi.mock('../../../utils/fileDownload', () => ({ + openFile: vi.fn(() => Promise.resolve()), + downloadFile: vi.fn(() => Promise.resolve()), +})); const file = { url: '/uploads/f/1', original_name: 'voucher.pdf' }; @@ -12,6 +15,7 @@ describe('openAttachment', () => { // A block body: a hook that returns the mock would have it run again as teardown. beforeEach(() => { vi.mocked(openFile).mockReset(); + vi.mocked(downloadFile).mockReset(); }); it('FE-PLANNER-OPENATTACH-001: opens the file by its url and name', async () => { @@ -36,4 +40,31 @@ describe('openAttachment', () => { expect(openFile).toHaveBeenCalledTimes(1); expect(openFile).toHaveBeenCalledWith('/uploads/f/1', 'voucher.pdf'); }); + + it('FE-PLANNER-OPENATTACH-004: downloads the file under its original name, without opening it (#2585)', async () => { + vi.mocked(downloadFile).mockResolvedValue(undefined); + const onError = vi.fn(); + downloadAttachment(file, onError); + expect(downloadFile).toHaveBeenCalledWith('/uploads/f/1', 'voucher.pdf'); + expect(openFile).not.toHaveBeenCalled(); + await Promise.resolve(); + expect(onError).not.toHaveBeenCalled(); + }); + + it('FE-PLANNER-OPENATTACH-005: a failed download reaches onError when one is given', async () => { + vi.mocked(downloadFile).mockRejectedValue(new Error('offline')); + const onError = vi.fn(); + downloadAttachment(file, onError); + await vi.waitFor(() => expect(onError).toHaveBeenCalledTimes(1)); + }); + + it('FE-PLANNER-OPENATTACH-006: without onError a failure is swallowed, not left unhandled', async () => { + vi.mocked(openFile).mockRejectedValue(new Error('offline')); + vi.mocked(downloadFile).mockRejectedValue(new Error('offline')); + openAttachment(file); + downloadAttachment(file); + // A rejection nobody caught would fail the run; settling the queue proves both were caught. + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(downloadFile).toHaveBeenCalledTimes(1); + }); }); diff --git a/client/src/components/Planner/bookings/openAttachment.ts b/client/src/components/Planner/bookings/openAttachment.ts index 708917ec2..d876180dd 100644 --- a/client/src/components/Planner/bookings/openAttachment.ts +++ b/client/src/components/Planner/bookings/openAttachment.ts @@ -1,11 +1,26 @@ -import { openFile } from '../../../utils/fileDownload'; +import { downloadFile, openFile } from '../../../utils/fileDownload'; + +export type Attachment = { url: string; original_name: string }; /** * Opens a file attached to a booking. The desktop panel passes `onError` to say * so with a toast when the file cannot be opened; the phone's bookings and * transports cards pass nothing and stay silent, as they always have. */ -export function openAttachment(f: { url: string; original_name: string }, onError?: () => void): void { - const opening = openFile(f.url, f.original_name); - void (onError ? opening.catch(onError) : opening); +export function openAttachment(f: Attachment, onError?: () => void): void { + settle(openFile(f.url, f.original_name), onError); +} + +/** + * Saves a file attached to a booking under its original name, where opening it + * would only show it in a viewer (a PDF ticket on a phone, say). Failures are + * reported the same way as for opening. + */ +export function downloadAttachment(f: Attachment, onError?: () => void): void { + settle(downloadFile(f.url, f.original_name), onError); +} + +function settle(pending: Promise, onError?: () => void): void { + // Without a handler the rejection is still caught, so it never surfaces as unhandled. + void pending.catch(() => onError?.()); } diff --git a/client/src/components/Planner/bookings/useReservationCard.test.ts b/client/src/components/Planner/bookings/useReservationCard.test.ts index 6f3029bc1..004e14b2e 100644 --- a/client/src/components/Planner/bookings/useReservationCard.test.ts +++ b/client/src/components/Planner/bookings/useReservationCard.test.ts @@ -1,17 +1,19 @@ // FE-PLANNER-RESLISTFILTER-005 to -009: what one card of the phone's bookings // and transports tabs does. import { act, renderHook } from '@testing-library/react'; -import type { KeyboardEvent, MouseEvent } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { buildBudgetItem, buildReservation } from '../../../../tests/helpers/factories'; import { resetAllStores, seedStore } from '../../../../tests/helpers/store'; import { useTripStore } from '../../../store/tripStore'; import type { Reservation } from '../../../types'; -import { openFile } from '../../../utils/fileDownload'; +import { downloadFile, openFile } from '../../../utils/fileDownload'; import { useReservationCard } from './useReservationCard'; -vi.mock('../../../utils/fileDownload', () => ({ openFile: vi.fn(() => Promise.resolve()) })); +vi.mock('../../../utils/fileDownload', () => ({ + openFile: vi.fn(() => Promise.resolve()), + downloadFile: vi.fn(() => Promise.resolve()), +})); const traveler = (user_id: number) => ({ user_id, username: `u${user_id}` }); const ann = buildReservation({ @@ -36,7 +38,10 @@ describe('useReservationCard', () => { t: (key: string) => key, }); - beforeEach(() => vi.mocked(openFile).mockClear()); + beforeEach(() => { + vi.mocked(openFile).mockClear(); + vi.mocked(downloadFile).mockClear(); + }); it('FE-PLANNER-RESLISTFILTER-005: with the blur preference on the code is blurred until toggled', () => { const { result } = renderHook(() => useReservationCard(ann, host(true))); @@ -70,25 +75,14 @@ describe('useReservationCard', () => { await vi.waitFor(() => expect(opts.toast.error).toHaveBeenCalledWith('reservations.toast.deleteError')); }); - it('FE-PLANNER-RESLISTFILTER-008: a file chip opens its file on click, Enter or Space and stops there', () => { + it('FE-PLANNER-RESLISTFILTER-008: a file opens or downloads under its own name (#2585)', () => { const { result } = renderHook(() => useReservationCard(ann, host())); - const chip = result.current.fileChip(file); - const click = { stopPropagation: vi.fn() } as unknown as MouseEvent; - chip.onClick(click); - expect(click.stopPropagation).toHaveBeenCalled(); - const key = (k: string) => - ({ key: k, preventDefault: vi.fn(), stopPropagation: vi.fn() }) as unknown as KeyboardEvent; - const enter = key('Enter'); - chip.onKeyDown(enter); - chip.onKeyDown(key(' ')); - const tab = key('Tab'); - chip.onKeyDown(tab); - expect(enter.preventDefault).toHaveBeenCalled(); - expect(enter.stopPropagation).toHaveBeenCalled(); - expect(tab.preventDefault).not.toHaveBeenCalled(); - expect(tab.stopPropagation).not.toHaveBeenCalled(); - expect(openFile).toHaveBeenCalledTimes(3); + result.current.openFile(file); expect(openFile).toHaveBeenCalledWith('/uploads/f/1', 'voucher.pdf'); + expect(downloadFile).not.toHaveBeenCalled(); + result.current.downloadFile(file); + expect(downloadFile).toHaveBeenCalledWith('/uploads/f/1', 'voucher.pdf'); + expect(openFile).toHaveBeenCalledTimes(1); }); it('FE-PLANNER-RESLISTFILTER-009: the card knows whether deleting it takes a linked expense along (#2604)', () => { diff --git a/client/src/components/Planner/bookings/useReservationCard.ts b/client/src/components/Planner/bookings/useReservationCard.ts index f432a2133..c617bdde3 100644 --- a/client/src/components/Planner/bookings/useReservationCard.ts +++ b/client/src/components/Planner/bookings/useReservationCard.ts @@ -1,9 +1,9 @@ -import { useState, type KeyboardEvent, type MouseEvent } from 'react'; +import { useState } from 'react'; import { useTripStore } from '../../../store/tripStore'; import type { Reservation } from '../../../types'; import { deleteBodyKey } from './bookingsModel'; -import { openAttachment } from './openAttachment'; +import { type Attachment, downloadAttachment, openAttachment } from './openAttachment'; import { useReservationDelete } from './useReservationDelete'; /** What a phone card needs from the planner it sits in. */ @@ -19,10 +19,9 @@ 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 from a chip inside the card's own button (so a click or Enter/Space on - * the chip stops there). A file that cannot be opened 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 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. */ export function useReservationCard(res: Reservation, host: ReservationCardHost) { const blurCodes = host.settings.blur_booking_codes; @@ -45,18 +44,7 @@ export function useReservationCard(res: Reservation, host: ReservationCardHost) confirmDelete: () => { void confirmDelete(); }, - fileChip: (f: { url: string; original_name: string }) => ({ - onClick: (e: MouseEvent) => { - e.stopPropagation(); - openAttachment(f); - }, - onKeyDown: (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - e.stopPropagation(); - openAttachment(f); - } - }, - }), + openFile: (f: Attachment) => openAttachment(f), + downloadFile: (f: Attachment) => downloadAttachment(f), }; } diff --git a/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx b/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx index 87229b78b..c5e74c627 100644 --- a/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx @@ -185,9 +185,9 @@ function BookingCard({ res, planner, canEdit, compact, onView }: { )} - - + + )} {!compact && } diff --git a/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx b/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx index 3067d1fb0..221e1b336 100644 --- a/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx +++ b/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx @@ -29,7 +29,8 @@ function cardState(overrides: Partial> = { askDelete: vi.fn(), cancelDelete: vi.fn(), confirmDelete: vi.fn(), - fileChip: () => ({ onClick: vi.fn(), onKeyDown: vi.fn() }), + openFile: vi.fn(), + downloadFile: vi.fn(), ...overrides, } as unknown as ReturnType; } @@ -130,18 +131,30 @@ describe('MReservationCardParts', () => { expect(toggleCode).toHaveBeenCalledOnce(); }); - it('FE-COMP-MRESCARD-006: files render as button-role chips, and no files render nothing', () => { + it('FE-COMP-MRESCARD-006: each file row opens the file by its name and downloads it by the icon (#2585)', () => { const { container, rerender } = render(); expect(container).toBeEmptyDOMElement(); const files = [{ id: 5, original_name: 'ticket.pdf', url: '/uploads/f/5' }] as Parameters< typeof CardFiles >[0]['files']; - rerender(); + const card = cardState(); + rerender(); expect(screen.getByText('files.title')).toBeInTheDocument(); - const chip = screen.getByRole('button', { name: 'ticket.pdf' }); - expect(chip.tagName).toBe('SPAN'); - expect(chip).toHaveAttribute('tabindex', '0'); + + const open = screen.getByRole('button', { name: 'ticket.pdf' }); + const download = screen.getByRole('button', { name: 'files.download: ticket.pdf' }); + // Two sibling buttons, never one inside the other. + expect(open.tagName).toBe('BUTTON'); + expect(open.contains(download)).toBe(false); + expect(download.closest('button')).toBe(download); + + fireEvent.click(open); + expect(card.openFile).toHaveBeenCalledWith(files[0]); + expect(card.downloadFile).not.toHaveBeenCalled(); + fireEvent.click(download); + expect(card.downloadFile).toHaveBeenCalledWith(files[0]); + expect(card.openFile).toHaveBeenCalledOnce(); }); it('FE-COMP-MRESCARD-007: the delete sheet names the reservation and confirms through the card', () => { diff --git a/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx b/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx index 0696c9d3f..906e34c02 100644 --- a/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx +++ b/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx @@ -1,4 +1,4 @@ -import { FileText, type LucideIcon, Pencil, Trash2 } from 'lucide-react'; +import { Download, FileText, type LucideIcon, Pencil, Trash2 } from 'lucide-react'; import type { useReservationCard } from '../../../../components/Planner/bookings/useReservationCard'; import type { useTranslation } from '../../../../i18n'; @@ -163,7 +163,11 @@ export function CardNotes({ notes }: { notes: string | null | undefined }) { ); } -/** Attached files, listed inside the card body button. */ +/** + * Attached files, under the card body button rather than inside it, so each row + * 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; card: CardState; t: T }) { if (files.length === 0) return null; return ( @@ -172,21 +176,30 @@ export function CardFiles({ files, card, t }: { files: ReturnType
- {/* A span with a button role, not a + +
))} diff --git a/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx b/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx index 0f5ee6cbd..ccbd63e00 100644 --- a/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx @@ -194,9 +194,9 @@ function TransportCard({ res, planner, shell, canEdit, compact }: { - - + + )} {!compact && } diff --git a/shared/src/i18n/ar/files.ts b/shared/src/i18n/ar/files.ts index 840f92a2a..68b49bfec 100644 --- a/shared/src/i18n/ar/files.ts +++ b/shared/src/i18n/ar/files.ts @@ -17,6 +17,7 @@ const files: TranslationStrings = { 'files.subtitle.many': '{count} ملفًا لـ {trip}', 'files.download': 'تنزيل', 'files.openError': 'تعذر فتح الملف', + 'files.downloadError': 'تعذر تنزيل الملف', 'files.downloadPdf': 'تنزيل PDF', 'files.count': '{count} ملف', 'files.count.zero': 'لا توجد ملفات', diff --git a/shared/src/i18n/az/files.ts b/shared/src/i18n/az/files.ts index 6098027b3..a61a6edac 100644 --- a/shared/src/i18n/az/files.ts +++ b/shared/src/i18n/az/files.ts @@ -14,6 +14,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{trip} üçün {count} fayl', 'files.download': 'Endir', 'files.openError': 'Faylı açmaq mümkün olmadı', + 'files.downloadError': 'Faylı yükləmək mümkün olmadı', 'files.downloadPdf': 'PDF-i endir', 'files.count': '{count} fayl', 'files.count.one': '{count} fayl', diff --git a/shared/src/i18n/br/files.ts b/shared/src/i18n/br/files.ts index 851ce013b..169eb27f9 100644 --- a/shared/src/i18n/br/files.ts +++ b/shared/src/i18n/br/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} arquivo para {trip}', 'files.download': 'Baixar', 'files.openError': 'Não foi possível abrir o arquivo', + 'files.downloadError': 'Não foi possível baixar o arquivo', 'files.downloadPdf': 'Baixar PDF', 'files.count': '{count} arquivos', 'files.count.one': '{count} arquivo', diff --git a/shared/src/i18n/ca/files.ts b/shared/src/i18n/ca/files.ts index 897829d90..26ad3a1a4 100644 --- a/shared/src/i18n/ca/files.ts +++ b/shared/src/i18n/ca/files.ts @@ -7,6 +7,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} fitxer per a {trip}', 'files.download': 'Baixa', 'files.openError': "No s'ha pogut obrir el fitxer", + 'files.downloadError': "No s'ha pogut baixar el fitxer", 'files.downloadPdf': 'Baixa el PDF', 'files.count': '{count} fitxers', 'files.count.one': '{count} fitxer', diff --git a/shared/src/i18n/cs/files.ts b/shared/src/i18n/cs/files.ts index a4023a305..6db784d87 100644 --- a/shared/src/i18n/cs/files.ts +++ b/shared/src/i18n/cs/files.ts @@ -14,6 +14,7 @@ const files: TranslationStrings = { 'files.subtitle.few': '{count} soubory pro {trip}', 'files.download': 'Stáhnout', 'files.openError': 'Soubor nelze otevřít', + 'files.downloadError': 'Soubor nelze stáhnout', 'files.downloadPdf': 'Stáhnout PDF', 'files.count': '{count} souborů', 'files.count.one': '{count} soubor', diff --git a/shared/src/i18n/de/files.ts b/shared/src/i18n/de/files.ts index c350f31d6..74992507c 100644 --- a/shared/src/i18n/de/files.ts +++ b/shared/src/i18n/de/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} Datei für {trip}', 'files.download': 'Herunterladen', 'files.openError': 'Datei konnte nicht geöffnet werden', + 'files.downloadError': 'Datei konnte nicht heruntergeladen werden', 'files.downloadPdf': 'PDF herunterladen', 'files.count': '{count} Dateien', 'files.count.one': '{count} Datei', diff --git a/shared/src/i18n/en/files.ts b/shared/src/i18n/en/files.ts index 4f15e2d4a..baba37b45 100644 --- a/shared/src/i18n/en/files.ts +++ b/shared/src/i18n/en/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} file for {trip}', 'files.download': 'Download', 'files.openError': 'Could not open file', + 'files.downloadError': 'Could not download file', 'files.downloadPdf': 'Download PDF', 'files.count': '{count} files', 'files.count.one': '{count} file', diff --git a/shared/src/i18n/es/files.ts b/shared/src/i18n/es/files.ts index f8373ae77..d43d5114b 100644 --- a/shared/src/i18n/es/files.ts +++ b/shared/src/i18n/es/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} archivo para {trip}', 'files.download': 'Descargar', 'files.openError': 'No se pudo abrir el archivo', + 'files.downloadError': 'No se pudo descargar el archivo', 'files.downloadPdf': 'Descargar PDF', 'files.count': '{count} archivos', 'files.count.one': '{count} archivo', diff --git a/shared/src/i18n/et/files.ts b/shared/src/i18n/et/files.ts index a68501112..8d7662583 100644 --- a/shared/src/i18n/et/files.ts +++ b/shared/src/i18n/et/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} fail reisile {trip}', 'files.download': 'Laadi alla', 'files.openError': 'Faili avamine ebaõnnestus', + 'files.downloadError': 'Faili allalaadimine ebaõnnestus', 'files.downloadPdf': 'Laadi PDF alla', 'files.count': '{count} faili', 'files.count.one': '{count} fail', diff --git a/shared/src/i18n/fr/files.ts b/shared/src/i18n/fr/files.ts index a3d92020a..7947529cf 100644 --- a/shared/src/i18n/fr/files.ts +++ b/shared/src/i18n/fr/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} fichier pour {trip}', 'files.download': 'Télécharger', 'files.openError': "Impossible d'ouvrir le fichier", + 'files.downloadError': 'Impossible de télécharger le fichier', 'files.downloadPdf': 'Télécharger le PDF', 'files.count': '{count} fichiers', 'files.count.one': '{count} fichier', diff --git a/shared/src/i18n/gr/files.ts b/shared/src/i18n/gr/files.ts index efda3d335..7f76b6b5e 100644 --- a/shared/src/i18n/gr/files.ts +++ b/shared/src/i18n/gr/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} αρχείο για {trip}', 'files.download': 'Λήψη', 'files.openError': 'Δεν ήταν δυνατό το άνοιγμα του αρχείου', + 'files.downloadError': 'Δεν ήταν δυνατή η λήψη του αρχείου', 'files.downloadPdf': 'Λήψη PDF', 'files.count': '{count} αρχεία', 'files.count.one': '{count} αρχείο', diff --git a/shared/src/i18n/he/files.ts b/shared/src/i18n/he/files.ts index 4270876eb..39322e054 100644 --- a/shared/src/i18n/he/files.ts +++ b/shared/src/i18n/he/files.ts @@ -14,6 +14,7 @@ const files: TranslationStrings = { 'files.subtitle.two': 'שני קבצים עבור {trip}', 'files.download': 'הורדה', 'files.openError': 'לא ניתן לפתוח את הקובץ', + 'files.downloadError': 'לא ניתן להוריד את הקובץ', 'files.downloadPdf': 'הורדת PDF', 'files.count': '{count} קבצים', 'files.count.one': 'קובץ אחד', diff --git a/shared/src/i18n/hu/files.ts b/shared/src/i18n/hu/files.ts index 05ffa5910..abd1a1d52 100644 --- a/shared/src/i18n/hu/files.ts +++ b/shared/src/i18n/hu/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} fájl a következőhöz: {trip}', 'files.download': 'Letöltés', 'files.openError': 'A fájl megnyitása sikertelen', + 'files.downloadError': 'A fájl letöltése sikertelen', 'files.downloadPdf': 'PDF letöltése', 'files.count': '{count} fájl', 'files.count.one': '{count} fájl', diff --git a/shared/src/i18n/id/files.ts b/shared/src/i18n/id/files.ts index 3ccc88022..2ce2b8b8e 100644 --- a/shared/src/i18n/id/files.ts +++ b/shared/src/i18n/id/files.ts @@ -12,6 +12,7 @@ const files: TranslationStrings = { 'files.subtitle': '{count} file untuk {trip}', 'files.download': 'Unduh', 'files.openError': 'Tidak dapat membuka file', + 'files.downloadError': 'Tidak dapat mengunduh file', 'files.downloadPdf': 'Unduh PDF', 'files.count': '{count} file', 'files.uploaded': '{count} diunggah', diff --git a/shared/src/i18n/it/files.ts b/shared/src/i18n/it/files.ts index 71db217ab..05883dcc3 100644 --- a/shared/src/i18n/it/files.ts +++ b/shared/src/i18n/it/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} file per {trip}', 'files.download': 'Scarica', 'files.openError': 'Impossibile aprire il file', + 'files.downloadError': 'Impossibile scaricare il file', 'files.downloadPdf': 'Scarica PDF', 'files.count': '{count} file', 'files.count.one': '{count} file', // same-as-en diff --git a/shared/src/i18n/ja/files.ts b/shared/src/i18n/ja/files.ts index edbff0f52..9b60dcd2b 100644 --- a/shared/src/i18n/ja/files.ts +++ b/shared/src/i18n/ja/files.ts @@ -12,6 +12,7 @@ const files: TranslationStrings = { 'files.subtitle': '{trip} のファイル {count} 件', 'files.download': 'ダウンロード', 'files.openError': 'ファイルを開けませんでした', + 'files.downloadError': 'ファイルをダウンロードできませんでした', 'files.downloadPdf': 'PDFをダウンロード', 'files.count': '{count}件のファイル', 'files.uploaded': '{count}件アップロードしました', diff --git a/shared/src/i18n/ko/files.ts b/shared/src/i18n/ko/files.ts index 4cad27299..ec6c2b695 100644 --- a/shared/src/i18n/ko/files.ts +++ b/shared/src/i18n/ko/files.ts @@ -12,6 +12,7 @@ const files: TranslationStrings = { 'files.subtitle': '{trip}의 파일 {count}개', 'files.download': '다운로드', 'files.openError': '파일을 열 수 없습니다', + 'files.downloadError': '파일을 다운로드할 수 없습니다', 'files.downloadPdf': 'PDF 다운로드', 'files.count': '파일 {count}개', 'files.uploaded': '{count}개 업로드됨', diff --git a/shared/src/i18n/nl/files.ts b/shared/src/i18n/nl/files.ts index a66049f24..0f267f440 100644 --- a/shared/src/i18n/nl/files.ts +++ b/shared/src/i18n/nl/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} bestand voor {trip}', 'files.download': 'Downloaden', 'files.openError': 'Bestand kon niet worden geopend', + 'files.downloadError': 'Bestand kon niet worden gedownload', 'files.downloadPdf': 'PDF downloaden', 'files.count': '{count} bestanden', 'files.count.one': '{count} bestand', diff --git a/shared/src/i18n/pl/files.ts b/shared/src/i18n/pl/files.ts index 0aac29f72..eb7250090 100644 --- a/shared/src/i18n/pl/files.ts +++ b/shared/src/i18n/pl/files.ts @@ -15,6 +15,7 @@ const files: TranslationStrings = { 'files.subtitle.many': '{count} plików z podróży {trip}', 'files.download': 'Pobierz', 'files.openError': 'Nie można otworzyć pliku', + 'files.downloadError': 'Nie można pobrać pliku', 'files.downloadPdf': 'Pobierz PDF', 'files.count': '{count} pliku', 'files.count.one': '{count} plik', diff --git a/shared/src/i18n/ru/files.ts b/shared/src/i18n/ru/files.ts index df4d1bfcb..bd563fd47 100644 --- a/shared/src/i18n/ru/files.ts +++ b/shared/src/i18n/ru/files.ts @@ -15,6 +15,7 @@ const files: TranslationStrings = { 'files.subtitle.many': '{count} файлов для {trip}', 'files.download': 'Скачать', 'files.openError': 'Не удалось открыть файл', + 'files.downloadError': 'Не удалось скачать файл', 'files.downloadPdf': 'Скачать PDF', 'files.count': '{count} файла', 'files.count.one': '{count} файл', diff --git a/shared/src/i18n/sk/files.ts b/shared/src/i18n/sk/files.ts index df4f03c03..14763c418 100644 --- a/shared/src/i18n/sk/files.ts +++ b/shared/src/i18n/sk/files.ts @@ -14,6 +14,7 @@ const files: TranslationStrings = { 'files.subtitle.few': '{count} súbory pre {trip}', 'files.download': 'Stiahnuť', 'files.openError': 'Súbor sa nedá otvoriť', + 'files.downloadError': 'Súbor sa nedá stiahnuť', 'files.downloadPdf': 'Stiahnuť PDF', 'files.count': '{count} súborov', 'files.count.one': '{count} súbor', diff --git a/shared/src/i18n/sv/files.ts b/shared/src/i18n/sv/files.ts index bed794a24..eae80b2da 100644 --- a/shared/src/i18n/sv/files.ts +++ b/shared/src/i18n/sv/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{count} fil för {trip}', 'files.download': 'Ladda ner', 'files.openError': 'Kunde inte öppna fil', + 'files.downloadError': 'Kunde inte ladda ner filen', 'files.downloadPdf': 'Ladda ner PDF', 'files.count': '{count} filer', 'files.count.one': '{count} fil', diff --git a/shared/src/i18n/th/files.ts b/shared/src/i18n/th/files.ts index 99431145d..75441b622 100644 --- a/shared/src/i18n/th/files.ts +++ b/shared/src/i18n/th/files.ts @@ -12,6 +12,7 @@ const files: TranslationStrings = { 'files.subtitle': '{count} ไฟล์สำหรับ {trip}', 'files.download': 'ดาวน์โหลด', 'files.openError': 'ไม่สามารถเปิดไฟล์ได้', + 'files.downloadError': 'ไม่สามารถดาวน์โหลดไฟล์ได้', 'files.downloadPdf': 'ดาวน์โหลดไฟล์ PDF', 'files.count': '{count} ไฟล์', 'files.uploaded': 'อัปโหลดแล้ว {count} ไฟล์', diff --git a/shared/src/i18n/tr/files.ts b/shared/src/i18n/tr/files.ts index b900aeb09..5c9fb4538 100644 --- a/shared/src/i18n/tr/files.ts +++ b/shared/src/i18n/tr/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle.one': '{trip} için {count} dosya', 'files.download': 'İndir', 'files.openError': 'Dosya açılamadı', + 'files.downloadError': 'Dosya indirilemedi', 'files.downloadPdf': 'PDF İndir', 'files.count': '{count} dosya', 'files.count.one': '{count} dosya', diff --git a/shared/src/i18n/uk/files.ts b/shared/src/i18n/uk/files.ts index 2dceb8da3..18450d3fb 100644 --- a/shared/src/i18n/uk/files.ts +++ b/shared/src/i18n/uk/files.ts @@ -15,6 +15,7 @@ const files: TranslationStrings = { 'files.subtitle.many': '{count} файлів для {trip}', 'files.download': 'Завантажити', 'files.openError': 'Не вдалося відкрити файл', + 'files.downloadError': 'Не вдалося завантажити файл', 'files.downloadPdf': 'Завантажити PDF', 'files.count': '{count} файлу', 'files.count.one': '{count} файл', diff --git a/shared/src/i18n/vi/files.ts b/shared/src/i18n/vi/files.ts index 079b853c3..04d5e0bc4 100644 --- a/shared/src/i18n/vi/files.ts +++ b/shared/src/i18n/vi/files.ts @@ -13,6 +13,7 @@ const files: TranslationStrings = { 'files.subtitle': '{count} tệp cho {trip}', 'files.download': 'Tải xuống', 'files.openError': 'Không thể mở tập tin', + 'files.downloadError': 'Không thể tải xuống tập tin', 'files.downloadPdf': 'Tải xuống PDF', 'files.count': '{count} tập tin', 'files.uploaded': 'Đã tải lên {count} tệp', diff --git a/shared/src/i18n/zh-TW/files.ts b/shared/src/i18n/zh-TW/files.ts index cf0b369a0..efeb13edb 100644 --- a/shared/src/i18n/zh-TW/files.ts +++ b/shared/src/i18n/zh-TW/files.ts @@ -12,6 +12,7 @@ const files: TranslationStrings = { 'files.subtitle': '{trip} 的 {count} 個檔案', 'files.download': '下載', 'files.openError': '無法開啟檔案', + 'files.downloadError': '無法下載檔案', 'files.downloadPdf': '下載 PDF', 'files.count': '{count} 個檔案', 'files.uploaded': '已上傳 {count} 個檔案', diff --git a/shared/src/i18n/zh/files.ts b/shared/src/i18n/zh/files.ts index ce952c597..378197320 100644 --- a/shared/src/i18n/zh/files.ts +++ b/shared/src/i18n/zh/files.ts @@ -12,6 +12,7 @@ const files: TranslationStrings = { 'files.subtitle': '{trip} 的 {count} 个文件', 'files.download': '下载', 'files.openError': '无法打开文件', + 'files.downloadError': '无法下载文件', 'files.downloadPdf': '下载 PDF', 'files.count': '{count} 个文件', 'files.uploaded': '已上传 {count} 个文件',